Tag Archives: mysql

webmastering

MySQL i automatyczne tworzenie historii rekordu w bazie [cz. 2/2]

Zgod­nie z obiet­nicą dzisiaj druga część. Zwięk­szamy poziom trud­no­ści o rela­cję wiele-do-wielu.

Na począ­tek dorzu­camy tabelę Ficzer zawie­ra­jącą dodat­kowe bajery, o które ma być wzbo­ga­cony artykuł:

CREATE TABLE IF NOT EXISTS `Ficzer` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`nazwa` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
INSERT INTO `Ficzer` (`id`, `nazwa`) VALUES
(1, 'Podświetlenie'),
(3, 'Pogrubienie'),
(5, 'Pochylenie');

Zaraz za nią tworzymy tabelę pośred­ni­czącą Ogloszenie_Ficzer:

CREATE TABLE IF NOT EXISTS `Ogloszenie_Ficzer` (
  `ogloszenie_id` INT(11) NOT NULL,
  `ficzer_id` INT(11) NOT NULL,
  PRIMARY KEY (`ogloszenie_id`,`ficzer_id`),
  KEY `ficzer_id` (`ficzer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
ALTER TABLE `Ogloszenie_Ficzer`
  ADD CONSTRAINT `Ogloszenie_Ficzer_ibfk_4` FOREIGN KEY (`ficzer_id`) REFERENCES `Ficzer` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  ADD CONSTRAINT `Ogloszenie_Ficzer_ibfk_3` FOREIGN KEY (`ogloszenie_id`) REFERENCES `Ogloszenie` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

Proce­dura skła­do­wana ogloszenie_history musi zostać rozbu­do­wana o obsługę doda­nych tabel (a raczej tabeli, bo wystar­czy nam tabela pośred­ni­cząca). Doko­namy „spłasz­cze­nia” struk­tury bazy w wier­szu histo­rii wymie­nia­jąc wszyst­kie ficzery rozdzie­lone śred­ni­kami. Dorzu­camy pole ficzers do OgloszenieHistory:

ALTER TABLE `OgloszenieHistory` ADD COLUMN `ficzers` text DEFAULT NULL;

Poni­żej uaktu­al­niony kod proce­dury składowanej:

DELIMITER $$
CREATE PROCEDURE `ogloszenie_history`(IN `id` INT, IN `change_type` ENUM('created','modified','deleted')) 
MODIFIES SQL DATA 
BEGIN 
  DECLARE p_user_id INT; 
  DECLARE p_data TEXT;
 
  # Ciag tekstowy zawierajacy ficzer_id;ficzer_id;...
  DECLARE p_ficzers TEXT DEFAULT NULL;
 
  # Pojedynczy wiersz kursora.
  DECLARE p_ficzer INT;
 
  # Blokada kursora gdy braknie wynikow.
  DECLARE p_last_ficzer INT DEFAULT FALSE;
 
  # Deklaracja kursora przechodzacego po wszystkich ficzerach ogloszenia.
  DECLARE cur_ficzer CURSOR FOR SELECT ficzer_id FROM Ogloszenie_Ficzer WHERE ogloszenie_id=id;
 
  # Ustawienie blokady kursora.
  DECLARE continue handler FOR NOT found SET p_last_ficzer = TRUE;
 
  # Wyciagam dane ze zmienianego wiersza i wrzucam do zadeklarowanych wczesniej zmiennych.
  SELECT modified_by, DATA INTO p_user_id, p_data FROM Ogloszenie WHERE id=id LIMIT 1;
 
  OPEN cur_ficzer;
  ficzer_loop: LOOP
    FETCH cur_ficzer INTO p_ficzer;
    IF p_last_ficzer THEN
      LEAVE ficzer_loop;
    END IF;
 
    # Sklejam kolejne wartosci.
    SET p_ficzers = CONCAT_WS(';', p_ficzers, p_ficzer);
  END LOOP;
  CLOSE cur_ficzer;
 
  # Wrzucam wiersz do historii.
  INSERT INTO OgloszenieHistory (ogloszenie_id, change_type, user_id, DATA, ficzers) 
    VALUES (id, change_type, p_user_id, p_data, p_ficzers);
 
END$$
DELIMITER ;

Dzięki zasto­so­wa­niu kursora zbie­ramy sobie każdo­ra­zowo bieżące warto­ści tabeli pośred­ni­czą­cej. Ostat­nią rzeczą jest doda­nie wyzwalaczy:

DROP TRIGGER IF EXISTS `new_ficzer`;
DELIMITER //
CREATE TRIGGER `new_ficzer` AFTER INSERT ON `Ogloszenie_Ficzer`
  FOR EACH ROW BEGIN
    CALL ogloszenie_history (NEW.ogloszenie_id, 'modified');
END//
DELIMITER ;
DROP TRIGGER IF EXISTS `delete_ficzer`;
DELIMITER //
CREATE TRIGGER `delete_ficzer` BEFORE DELETE ON `Ogloszenie_Ficzer`
  FOR EACH ROW BEGIN
    CALL ogloszenie_history (OLD.ogloszenie_id, 'modified');
END//
DELIMITER ;

Stosu­jąc takie rozwią­za­nie każda zmiana jest reje­stro­wana w dzien­niku zmian. Minu­sem jest to, że nastę­puje gwał­towny przy­rost wier­szy (doda­nie 10 ficze­rów do ogło­sze­nia powo­duje doda­nie 10 wier­szy histo­rii, skaso­wa­nie to samo). Jeśli bardzo zależy nam na ogra­ni­cze­niu liczby wier­szy trzeba zasta­no­wić się nad jakimś auto­ma­tem odpa­la­nym cyklicz­nie z CRONa kasu­ją­cym starą histo­rię lub bardziej wyra­fi­no­wa­nym — wyła­pu­ją­cym „stany pośred­nie” (co zresztą też jest niezłym pretek­stem do napi­sa­nia na blogu ;-) ).

PS.: Inte­re­su­je­cie się DARTem? Jesz­cze trochę ponad 100 defek­tów i będzie mile­stone 1.

webmastering

MySQL i automatyczne tworzenie historii rekordu w bazie [cz. 1/2]

Ponie­waż dawno nie pisa­łem o bazach danych, dzisiaj coś dla miło­śni­ków wyzwa­la­czy i proce­dur skła­do­wa­nych w MySQL. Ile razy myśle­li­ście, że fajnie by było żeby coś „samo się robiło”? Taką samo­się można napi­sać dosyć łatwo jeśli chcemy stwo­rzyć mecha­nizm śledze­nia histo­rii rekordu. Wpis rozbiję na dwie części z uwagi na to, że w następ­nej skom­pli­kuję całość o tworze­nie histo­rii rekordu z wielu tabel.

Załóżmy, że mamy tabelę Ogloszenie, w której trzy­mamy dane, które chcemy śledzić. Ma nastę­pu­jącą strukturę:

CREATE TABLE IF NOT EXISTS `Ogloszenie` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `modified_by` INT(11) NOT NULL,
  `data` TEXT NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Tabela śledząca ogło­sze­nie będzie wyglą­dała mniej więcej tak (nie śmiać mi się z polsko-angielskich hybryd nazewniczych):

CREATE TABLE IF NOT EXISTS `OgloszenieHistory` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `ogloszenie_id` INT(11) NOT NULL,
  `change_type` enum('created','modified','deleted') NOT NULL,
  `user_id` INT(11) NOT NULL,
  `event_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `data` TEXT NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Wiemy, że możemy podcze­piać się pod zmiany w bazie danych za pomocą wyzwa­la­czy (trig­ge­rów). Tworząc histo­rię wystar­czy śledzić zmiany w postaci zapy­tań typu insert, update i delete. Wyzwa­la­cze będą różnić się właści­wie tylko typem wyko­na­nego zapy­ta­nia. Nie wiem jak wam, ale mnie na kilo­metr „zaje­chało” redun­dan­cją kodu wyzwa­la­czy, więc napi­szemy sobie proce­durę skła­do­waną, którą każdy z wyzwa­la­czy będzie odpa­lał. Jest to najtrud­niej­sze zada­nie (kod proce­dur wykrza­cza się jak szalony).

DELIMITER $$
CREATE PROCEDURE `ogloszenie_history`(IN `id` INT, IN `change_type` ENUM('created','modified','deleted')) 
MODIFIES SQL DATA 
BEGIN 
  DECLARE p_user_id INT; 
  DECLARE p_data TEXT; 
 
  # Wyciagam dane ze zmienianego wiersza i wrzucam do zadeklarowanych wczesniej zmiennych.
  SELECT modified_by, DATA INTO p_user_id, p_data FROM Ogloszenie WHERE id=id LIMIT 1;
 
  # Wrzucam wiersz do historii.
  INSERT INTO OgloszenieHistory (ogloszenie_id, change_type, user_id, DATA) 
    VALUES (id, change_type, p_user_id, p_data);
 
END$$
DELIMITER ;

Zwróć­cie uwagę na pole change_type, którego dopusz­czalne warto­ści to created, modi­fieddele­ted, czyli dokład­nie takie jak w tabeli OgloszenieHistory. Skutecz­nie zawęzi nam to możli­wość „zatru­cia” histo­rii niezro­zu­mia­łymi typami zmian.
Pozo­stało zrobie­nie wyzwalaczy:

DROP TRIGGER IF EXISTS `inserted_ogloszenie`;
DELIMITER //
CREATE TRIGGER `inserted_ogloszenie` AFTER INSERT ON `Ogloszenie`
 FOR EACH ROW BEGIN
  # Podpinamy sie po insercie dysponujac swiezo auto inkrementowanym ID wiersza.
  CALL ogloszenie_history(NEW.id, 'created');
END//
DELIMITER ;
DROP TRIGGER IF EXISTS `updated_ogloszenie`;
DELIMITER //
CREATE TRIGGER `updated_ogloszenie` AFTER UPDATE ON `Ogloszenie`
 FOR EACH ROW BEGIN
  # Podajemy id zmienionego rekordu.
  CALL ogloszenie_history(NEW.id, 'modified');
END//
DELIMITER ;
DROP TRIGGER IF EXISTS `deleted_ogloszenie`;
DELIMITER //
CREATE TRIGGER `deleted_ogloszenie` BEFORE DELETE ON `Ogloszenie`
 FOR EACH ROW BEGIN
  # Zanim baza skasuje wiersz zapiszemy sobie TO wydarzenie.
  CALL ogloszenie_history(OLD.id, 'deleted');
END//
DELIMITER ;

Śledze­nie histo­rii przed­sta­wione powy­żej jest o tyle dobre, że zwal­nia nas z pisa­nia jakie­go­kol­wiek kodu po stro­nie apli­ka­cji. Wszystko robi za nas baza. Dodat­kowo mamy pełny wgląd kto i kiedy zmie­niał dane. Przy­kła­dowe wpisy w tabeli OgloszenieHistory:

id, ogloszenie_id, change_type, user_id, event_date,           data
1   1              'created'    1        '2012-09-05 18:09:03' 'First data'
2   1              'modified'   2        '2012-09-05 18:09:16' 'Modified data'
3   1              'modified'   1        '2012-09-05 18:09:29' 'Modified data drugi raz'
4   2              'created'    1        '2012-09-05 18:09:37' 'Second data'
5   1              'deleted'    2        '2012-09-05 18:42:42' 'Modified data drugi raz'

W zasto­so­wa­niach prak­tycz­nych nie mamy właści­wie do czynie­nia z bazami bez rela­cji, więc taka prosta histo­ria na niewiele się zda. W następ­nej części wpro­wa­dzę śledze­nie rela­cji typu wiele-do-wielu. Kiedy będzie następna część? Wtedy kiedy będzie mi się chciało pisać ;-)

webmastering

MySQL, PDO i procedury składowane

Trochę nie pisa­łem. Mam nadzieję, że dzisiej­szy wpis wszyst­kim wyna­gro­dzi moją nieobec­ność. Ostat­nio staną­łem przed wyzwa­niem zrobie­nia gale­rii zdjęć, których kolej­ność dałoby się dowol­nie mody­fi­ko­wać za pomocą prze­cią­ga­nia i upusz­cza­nia. Dzisiaj opiszę opera­cje bazo­da­nowe, a na następny raz jQuery. Będę maksy­mal­nie uprasz­czał aby nie zaciem­niać meritum.

1. Przy­go­to­wa­nia

Powiedzmy, że mamy 2 tabele rela­cyjne odpo­wie­dzialne za prze­cho­wy­wa­nie gale­rii i obra­zów. Np. takie:

CREATE TABLE `galleries` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
  `title` VARCHAR(250) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;
 
CREATE TABLE IF NOT EXISTS `images` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `gallery_id` INT(10) UNSIGNED NOT NULL,
  `filename` VARCHAR(50) NOT NULL,
  `ordr` INT(10) UNSIGNED NOT NULL DEFAULT '1',
  `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `image_to_gallery` (`gallery_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 ;

Od razu widać dwa dziwac­twa: dlaczego pole nazywa się ordr a nie order? Z czystego leni­stwa. Order jest słowem zare­zer­wo­wa­nym w SQL (ORDER BY coś tam). Każdo­ra­zowo nazwa pola musia­łaby być w nawia­sach. Dlaczego pole upda­ted ma domyślną wartość 0000−00−00 00:00:00? Ano dlatego, że CURRENT_TIMESTAMP można użyć tylko raz w tabeli. Wobec tego stwo­rzymy od razu wyzwa­lacz (trig­ger), który przed każdym zapy­ta­niem typu UPDATE poprawi wartość na taką jak trzeba.

DELIMITER $$
CREATE TRIGGER `updated_current_timestamp` BEFORE UPDATE ON `galleries`
   FOR EACH ROW BEGIN
      SET NEW.updated = NOW();
END$$

Na koniec trzeba stwo­rzyć rela­cję 1 gale­ria do wielu zdjęć.

ALTER TABLE `images`
   ADD CONSTRAINT `image_to_gallery` FOREIGN KEY (`gallery_id`) REFERENCES `galleries` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

Ozna­cza to, że kasu­jąc gale­rię od razu pozbę­dziemy się również wszyst­kich powią­za­nych z nią zdjęć. Pliki z dysku oczy­wi­ście nie znikną. Można napi­sać funk­cję, która przed usunię­ciem gale­rii z bazy najpierw wyrzuca wszyst­kie powią­zane z nią pliki, a dopiero potem wyko­nuje pole­ce­nie DELETE.

2. Decy­zje

Teraz nadszedł czas na poważne decy­zje. Chodzi o sposób mani­pu­la­cji wier­szami doty­czą­cymi zdjęć. Można to zrobić za pomocą PHP. Jest to rozwią­za­nie prost­sze. Powo­duje jednak spory narzut komu­ni­ka­cji PHP<—>SQL. W przy­padku zwale­nia wszyst­kiego na bazę danych, pchamy logikę wyżej i bliżej mody­fi­ko­wa­nych danych. Minu­sem jest cholerna skład­nia SQLowa i później­sze proble­ma­tyczne utrzy­ma­nie kodu.
Problem oczy­wi­ście nie istnieje gdy robimy gale­ryjkę na 10 obraz­ków i prze­sta­wimy sobie kolej­ność ostat­niego na przed­ostatni. Ja raczej podcho­dzę do rzeczy poważ­nie i wolę od początku zrobić to tak jak powinno być. Poza tym wyzwa­la­cze i proce­dury skła­do­wane to jest to, co bazo­da­nowe tygry­ski lubią najbardziej :-)

3. Wyko­na­nie

Każdy nowy rekord tabeli images musi mieć nadany odpo­wiedni iden­ty­fi­ka­tor pozy­cji ordr o 1 więk­szy od ostat­niego w danej gale­rii. Mamy trzy rozwią­za­nia: czysty PHP, wyzwa­lacz wywo­ły­wany przed INSERTem lub proce­dura skła­do­wana. Zapy­ta­nie w PHP może wyglą­dać tak:

$q = $pdo->prepare('INSERT INTO images (filename,gallery_id,ordr) (SELECT ?,?,MAX(ordr)+1 FROM images WHERE gallery_id=? LIMIT 1)');
$q->bindValue(1, 'obrazek.jpg', PDO::PARAM_STR);
$q->bindParam(2, $galleryId, PDO::PARAM_INT);
$q->bindParam(3, $galleryId, PDO::PARAM_INT);
$q->execute();

Wspo­mi­nam o tym rozwią­za­niu dlatego, że ma ciekawą konstruk­cję INSERT SELECT. Zapewne więk­szość z was po kilku­na­sto­krot­nej próbie wywo­ła­nia pole­ce­nia INSERT INTO VALUES i gdzieś tam SELECT dosta­nie cholery i rozbije zapy­ta­nie na 2: pierw­sze spraw­dza ostatni ordr, a następne doda 1 i umie­ści INSERTem pozo­stałe dane.
Bazo­da­nowe tygry­ski wybiorą jednak co innego. Proce­dury składowane!

DELIMITER $$
CREATE PROCEDURE `insert_image`(IN image_filename VARCHAR(50), IN image_gallery_id INT, OUT last_inserted_id INT)
   MODIFIES SQL DATA
   COMMENT 'Inserts new image at the end of given gallery.'
   BEGIN
      DECLARE max_order INT;
 
      # Zamiast SET zmienna= uzywam SELECT INTO just FOR fun
      SELECT MAX(ordr) INTO max_order FROM images WHERE gallery_id=image_gallery_id LIMIT 1;
 
      # Gdy obrazek jest pierwszy w galerii
      IF max_order IS NULL THEN 
         SET max_order = 1;
      ELSE
         SET max_order = max_order + 1;
      END IF;
      INSERT INTO images (filename, gallery_id, ordr) VALUES (image_filename, image_gallery_id, max_order);
      SELECT LAST_INSERT_ID() INTO last_inserted_id;
END$$

Próba wywo­ła­nia $pdo->lastInsertId() zakoń­czy się niepo­wo­dze­niem (a raczej zerem :-) ). Dlatego potrze­bu­jemy para­me­tru wyjścio­wego. Poni­żej poka­zuję jak całość wywo­łać w PDO:

$q = $pdo->prepare('CALL insert_image(?,?,@lastInsertId)');
$q->bindValue(1, 'obrazek.jpg', PDO::PARAM_STR);
$q->bindParam(2, $galleryId, PDO::PARAM_INT);
$q->execute();
$outputArray = $pdo->query('select @lastInsertId')->fetch(PDO::FETCH_ASSOC);
$lastInsertId = $outputArray['@lastInsertId'];

Ktoś może się zapy­tać po co te numery ze zmienną wyjściową. PDO i sterow­nik MySQL w PHP ma szpetny błąd doty­czący obsługi para­me­trów wyjścio­wych z proce­dur skła­do­wa­nych. Podobno w nowszych wersjach jest OK. Trik podany wyżej u mnie działa i oszczę­dza trochę nerwów.

Proce­dura kasu­jąca obrazki również jest raczej prosta. Rzuć­cie okiem:

DELIMITER $$
CREATE PROCEDURE `delete_image`(IN image_id INT)
   MODIFIES SQL DATA
   COMMENT 'Deletes and reorders if there is a gap.'
   BEGIN
      DECLARE image_gallery_id, image_order, max_order INT;
      SELECT gallery_id, ordr INTO image_gallery_id, image_order FROM images WHERE id=image_id LIMIT 1;
      DELETE FROM images WHERE id=image_id LIMIT 1;
      SELECT MAX(ordr) INTO max_order FROM images WHERE gallery_id=image_gallery_id LIMIT 1;
 
      # Sprawdzamy czy istnieja jakies obrazki za skasowanym
      IF max_order IS NOT NULL AND max_order > image_order THEN
         WHILE image_order < max_order DO
 
            # Cofamy w petli wypelniajac luke po skasowanym obrazku
            UPDATE images SET ordr=image_order WHERE ordr=image_order+1 AND gallery_id=image_gallery_id;
            SET image_order = image_order + 1;
       END WHILE;
   END IF;
END$$

Wywo­ła­nie jest super proste. Wpisu­jemy i zapominamy:

$q = $pdo->prepare('CALL delete_image(?)');
$q->bindParam(1, $imageId, PDO::PARAM_INT);
$q->execute();

Najlep­sze zosta­wi­łem na koniec. Proce­dura prze­sta­wia­jąca pozy­cję obrazka. Spotka­łem się w necie z rozwią­za­niem typu „bierzesz sobie wszyst­kie id obraz­ków w kolej­no­ści, prze­jeż­dżasz fore­achem, który nadaje kolej­ność i na każdym obrazku wyko­nu­jesz update”. No dobra, ale co jeżeli prze­sta­wiam kolej­ność tylko ostat­niego i przed­ostat­niego, a w gale­rii mam 1000 zdjęć? Powyż­sze rozwią­za­nie orze całą gale­rię, a userzy czekają 5 sek. na zała­do­wa­nie się strony. A co jeżeli kilka osób na raz coś prze­sta­wia w swoich gale­riach? Wtedy na serwe­rze włącza się na kilka minut turbo hard­core i poja­wia się „Pan Gąbka” :-D
Moje rozwią­za­nie polega na wyła­pa­niu wyłącz­nie tego co wymaga zmian. Dałem kilka komen­ta­rzy dla jasności.

DELIMITER $$
CREATE PROCEDURE `reorder_image`(IN image_id INT, IN new_image_order INT)
   MODIFIES SQL DATA
   COMMENT 'Reorders images. Does nothing if given order is out of scope.'
   BEGIN
      DECLARE current_image_order, image_gallery_id, is_destination_order_exists INT;
 
      # Lapie obecne polozenie obrazka i przy okazji id galerii
      SELECT ordr, gallery_id INTO current_image_order, image_gallery_id FROM images WHERE id=image_id LIMIT 1;
 
      # Sprawdzam czy punkt docelowy w ogole istnieje
      SELECT ordr INTO is_destination_order_exists FROM images WHERE gallery_id=image_gallery_id AND ordr=new_image_order LIMIT 1;
 
      # Jezeli punkt docelowy istnieje i jest inny od obecnego TO rozpoczynam dzialanie
      IF is_destination_order_exists IS NOT NULL AND current_image_order <> new_image_order THEN
 
         # Jezeli przestawiam obrazek do gory
         IF current_image_order >  new_image_order THEN 
            WHILE current_image_order >=  new_image_order DO
               UPDATE images SET ordr=current_image_order+1 WHERE gallery_id=image_gallery_id AND ordr= current_image_order LIMIT 1;
               SET current_image_order = current_image_order - 1;
            END WHILE;
 
         # Jezeli przestawiam obrazek w dol
         ELSEIF  current_image_order <  new_image_order THEN 
            WHILE current_image_order <=  new_image_order DO
               UPDATE images SET ordr=current_image_order-1 WHERE gallery_id=image_gallery_id AND ordr= current_image_order LIMIT 1;
               SET current_image_order = current_image_order + 1;
            END WHILE;
         END IF;
 
         # Mam juz miejsce, wrzucam obrazek tam gdzie ma byc
         UPDATE images SET ordr=new_image_order WHERE id=image_id LIMIT 1;
      END IF;
END$$

Użycie prze­sta­wia­nia kolej­no­ści jest również proste:

$q = $pdo->prepare('CALL reorder_image(?,?)');
$q->bindParam(1, $imageId, PDO::PARAM_INT);
$q->bindParam(2, $wantedImageOrder, PDO::PARAM_INT);
$q->execute();

Jeśli ktoś dobrnął aż tutaj to proszę „lajk­nąć” i „plusnąć”. Niedzielne pozdro4all.