12. Використання SGBD MySQL

Тепер ми напишемо скрипти PHP, використовуючи базу даних MySQL:

У наведеній вище архітектурі скрипт PHP (1) не взаємодіє безпосередньо з SGBD (системою управління базами даних) (3). Він взаємодіє через посередника, який називається драйвером SGBD або драйвером SGBD. PHP надає стандартний інтерфейс для цих драйверів — інтерфейс PDO (PHP Data Objects). Цей інтерфейс реалізовано за допомогою різних класів, адаптованих до кожного SGBD: один клас для SGBD MySQL, інший — для SGBD PostgreSQL… Щоб змінити SGBD, потрібно змінити драйвер:

Драйвер PDO ізолює скрипт PHP (1) від SGBD (3, 6). Оскільки ці драйвери реалізують стандартний інтерфейс, можна очікувати, що скрипт PHP (1) не зміниться при переході від SGBD, MySQL (3) на SGBD або PostgreSQL (6). Насправді такого ідеалу не існує. Адже для взаємодії з SGBD скрипт PHP надсилає команди SQL (Standard Query Language). Ця мова реалізована у всіх SGBD, але є неповною. Тому SGBD додали до неї власні команди. Це є першою причиною несумісності між SGBD. Крім того, типи даних, які можна використовувати в базах даних, можуть відрізнятися від одного SGBD до іншого. Так, PostgreSQL підтримує набагато більшу кількість типів даних, ніж SGBD та MySQL. Це друга причина несумісності. Ще однією причиною є управління автоматичними первинними ключами (генерованими SGBD): практично кожен SGBD має власну політику. І т. д. Причин несумісності є багато.
Якщо ми хочемо уникнути переписання скрипта PHP (1), переходячи від MySQL (3) до PostgreSQL (6), зазвичай доводиться вставляти новий рівень між скриптом PHP (1) та драйвером PDO (2, 5), роль якого полягатиме в усуненні несумісностей між цими двома SGBD. Однак у простих випадках, з якими ми матимемо справу, цей додатковий рівень не буде потрібним.
Тепер ми будемо використовувати SGBD MySQL. Він входить до складу пакета Laragon (див. параграф «Посилання»).
Якщо читач ще не знайомий з поняттями бази даних та мови SQL, він може ознайомитися з документом [http://sergetahe.com/cours-tutoriels-de-programmation/cours-tutoriel-sql-avec-le-sgbd-firebird/]. У цьому документі використовується SGBD Firebird, а не MySQL, але він містить основи баз даних та мови SQL. Як і MySQL, Firebird має версію, яку можна використовувати безкоштовно і яка займає мало пам’яті.
12.1. Створення бази даних
Тепер ми покажемо, як створити базу даних та користувача MySQL за допомогою інструменту Laragon.

- після запуску Laragon [1] можна керувати через меню [2];
- у [3-5] встановлюється інструмент [phpMyAdmin] для управління MySQL, якщо він ще не встановлений;

- у [6] запускається веб-сервер Apache, а також SGBD та MySQL;
- у [7] запускається сервер Apache;
- у [8] запускаються SBD та MySQL;

- у [8-10] створюється база даних, яку називають [dbpersonnes] [11]. Ми створимо базу даних про осіб;

- у [11] ми будемо керувати базою даних, яку щойно створили;

- операція [Bases de données] надсилає веб-запит до URL [http://localhost/phpmyadmin]. Відповідає веб-сервер Apache від Laragon. URL [http://localhost/phpmyadmin] — це URL утиліти [phpMyAdmin], яку ми встановили раніше [5]. Ця утиліта дозволяє керувати базами даних MySQL;
- за замовчуванням облікові дані адміністратора бази такі: root [13] без пароля [14];

- у [16], базу даних, яку ми створили раніше;

- наразі маємо базу [dbpersonnes] [17], яка є порожньою [18];
Створюємо користувача [admpersonnes] з паролем [nobody], який матиме всі права на базу даних [dbpersonnes]:

- у [19] ми перебуваємо в базі [dbpersonnes];
- у [20] вибираємо вкладку [Privileges];
- у [21-22] бачимо, що користувач [root] має всі права на базу даних [dbpersonnes];
- у [23] створюється новий користувач;

- у [25-26] користувач матиме ідентифікатор [admdbpersonnes];
- у [27-29] його пароль буде [nobody];
- у [30] значення phpMyAdmin вказує на те, що пароль є дуже слабким (його легко зламати). У виробничому середовищі краще генерувати надійний пароль із значенням [31];
- у [32] вказується, що користувач [admdbpersonnes] повинен мати всі права на базу даних [dbpersonnes];
- у файлі [33] підтверджується надана інформація;

- у [35], phpMyAdmin вказує, що користувач було створено;
- у [36] — на основі запиту SQL, який було видано;
- у [37] користувач [admpersonnes] має всі права на базу даних [dbpersonnes];
Тепер ми маємо:
- базу даних MySQL [dbpersonnes];
- користувач [admpersonnes/nobody], який має всі права на цю базу даних;
Ми напишемо скрипти PHP для роботи з базою даних. PHP має різні бібліотеки для управління базами даних. Ми будемо використовувати бібліотеку PDO (PHP Data Objects), яка вставляється між кодом PHP та SGBD:

Бібліотека PDO дозволяє скрипту PHP абстрагуватися від точної природи використовуваного SGBD. Таким чином, у наведеному вище прикладі SGBD MySQL можна замінити на SGBD PostgreSQL з мінімальним впливом на код скрипта PHP. Ця бібліотека за замовчуванням недоступна. Перевірити її наявність можна таким чином:

- у [1-4] перевіряються активні розширення PDO;
- у файлі [5] видно, що розширення PDO для SGBD та MySQL є активним. Інші — ні. Щоб активувати їх, достатньо просто натиснути на них;
Інший спосіб активувати розширення — безпосередньо змінити файл [php.ini] (параграф «посилання»), який налаштовує PHP:

- у [1] розширення PDO для MySQL увімкнено;
- у файлі [2] розширення PDO для Firebird вимкнено;
Після внесення змін до файлу [php.ini] необхідно перезапустити PHP від Laragon, щоб зміни набули чинності.
12.2. Підключення до бази даних MySQL
Підключення до SGBD здійснюється шляхом створення об’єкта PDO. Конструктор приймає різні параметри:
Значення параметрів такі:
$dsn | (Data Source Name) — це рядок, що визначає тип SGBD та його розташування в Інтернеті. Рядок «mysql:host=localhost» вказує, що мова йде про SGBD MySQL, що працює на локальному сервері. Цей рядок може містити й інші параметри, зокрема порт, на якому SGBD приймає з’єднання, та ім’я бази даних, до якої потрібно підключитися: «mysql:host=localhost:port=3306:dbname=dbpersonnes»; |
$user | ідентифікатор користувача, що підключається; |
$passwd | його пароль; |
$driver_options | масив параметрів для драйвера SGBD; |
Обов’язковим є лише перший параметр. Створений таким чином об’єкт надалі слугуватиме основою для всіх операцій, що виконуються з базою даних, до якої відбулося підключення. Якщо об’єкт PDO не вдалося створити, генерується виняток типу PDOException.
Ось приклад підключення [mysql-01.php]:
<?php
// підключення до локальної бази даних MySql
// ідентифікатор користувача — (admpersonnes,nobody)
const ID = "admpersonnes";
const PWD = "nobody";
const HOTE = "localhost";
try {
// підключення
$dbh = new PDO("mysql:host=".HOTE, ID, PWD);
print "Connexion réussie\n";
// закриття з'єднання
$dbh = NULL;
} catch (PDOException $e) {
print "Erreur : " . $e->getMessage() . "\n";
exit();
}
Результати:
Коментарі
- рядок 11: підключення до SGBD здійснюється шляхом створення об’єкта PDO. Конструктор тут використовується з такими параметрами:
- рядок, що визначає тип SGBD та його розташування в Інтернеті. Рядок «mysql:host=localhost» вказує, що мова йде про SGBD MySQL, що працює на локальному сервері. Порт не вказано. У такому разі за замовчуванням використовується порт 3306. Назва бази даних також не вказана. У такому разі відбудеться підключення до SGBD MySQL, а вибір конкретної бази даних відбудеться пізніше;
- ідентифікатор користувача;
- його пароль;
- рядок 14: закриття з’єднання здійснюється шляхом видалення об’єкта PDO, створеного спочатку;
- рядок 15: підключення до SGBD може завершитися невдачею. У цьому випадку генерується виняток типу PDOException. Він походить від винятків PHP та [RuntimeException];
- рядок 16: виводиться повідомлення про помилку, пов’язане з цим винятком;
Давайте запустимо скрипт ще раз, вказавши в рядку 6 неправильний пароль. Результат буде таким:
Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes'@'localhost' (using password: YES)
12.3. Створення таблиці
Скрипт [mysql-02.php] демонструє створення таблиці в базі даних:
<?php
// ідентифікатор бази даних
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// ідентифікатори користувача
const ID = "admpersonnes";
const PWD = "nobody";
try {
// підключення до бази MySql
$connexion = new PDO(DSN, ID, PWD);
// видалення таблиці «особи», якщо вона існує
$sql = "drop table personnes";
$connexion->exec($sql);
// створення таблиці «особи»
$sql = "create table personnes (prenom varchar(30) NOT NULL, nom varchar(30) NOT NULL, age integer NOT NULL, primary key(nom,prenom))";
$connexion->exec($sql);
} catch (PDOException $ex) {
// виведення повідомлення про помилку
print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// у разі потреби відбувається відключення
$connexion = NULL;
}
// кінець
print "Terminé\n";
exit;
Коментарі
- рядок 11: підключення до бази даних. Це завжди перше, що потрібно зробити. Результатом підключення є об’єкт [PDO], через який будуть виконуватися операції з базою даних;
- рядок 13: команда SQL [drop table personnes] видалить таблицю [personnes] з бази даних [dbpersonnes]. Якщо таблиця [personnes] не існує, це не спричиняє помилки;
- рядок 14: виконання попередньої команди SQL у базі даних [dbpersonnes]. Це виконання може запустити [PDOException], яка буде перехоплена у рядку 18;
- рядок 16: ця команда SQL створює таблицю [personnes]. Таблиця містить рядки та стовпці. Стовпці утворюють так звану структуру таблиці. Рядки утворюють вміст таблиці. База даних може містити одну або кілька таблиць. Таблиця [personnes] матиме тут три стовпці:
- ім’я: ім’я особи у вигляді рядка довжиною не більше 30 символів;
- прізвище: прізвище цієї ж особи у вигляді рядка, що містить не більше 30 символів;
- вік: вік особи у вигляді цілого числа;
- атрибут NOT NULL для стовпця вимагає, щоб стовпець мав значення. Відсутність значення призведе до помилки [PDOException];
- [primary key(nom,prenom)] встановлює первинний ключ для таблиці [personnes]. Первинний ключ має унікальне значення для кожного рядка таблиці. У даному випадку первинний ключ буде отримано шляхом об’єднання стовпців [nom] та [prenom] у рядку. Ця обмеження гарантує, що в таблиці не може бути двох осіб з однаковими ім’ям та прізвищем, тобто двох одноіменних осіб. Створення однофамільника особи в таблиці викликає помилку [PDOException];
- рядок 17: виконання команди SQL над базою даних [dbpersonnes];
- рядок 20: якщо виникає помилка [PDOException], виводиться відповідне повідомлення про помилку;
- рядки 21–24: у будь-якому випадку, незалежно від наявності винятку, виконується блок [finally] для закриття з’єднання з базою даних (рядок 23);
Результати:
Якщо виконання скрипта проходить без помилок, у phpMyAdmin можна побачити наявність таблиці:


- у [3] — база даних;
- у [4] — представлена таблиця;
- у [5] структура таблиць представлена на вкладці [Structure];
- у [6-8] — три стовпці таблиці;
- у [9] жоден із трьох стовпців не може бути порожнім;

- у [10] — список індексів таблиці. Індекс дозволяє знаходити в таблиці рядки з певним індексом швидше, ніж при послідовному перегляді рядків таблиці. Первинний ключ завжди є частиною індексів, але індекс не обов’язково є первинним ключем;
- у [11] індекс тут є первинним ключем;
- у [12] індекс складається зі стовпців [nom, prenom] кожного рядка;
Тепер давайте подивимося, що станеться, якщо ми створимо помилки, відповідно, у назві бази даних, імені користувача та його паролі:
Якщо вказати ім’я бази даних, якої не існує:
Erreur : SQLSTATE[HY000] [1044] Access denied for user 'admpersonnes'@'%' to database 'dbpersonnes2'
Якщо вказати ім’я користувача, якого не існує:
Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes2'@'localhost' (using password: YES)
Якщо ввести неправильний пароль:
Erreur : SQLSTATE[HY000] [1045] Access denied for user 'admpersonnes'@'localhost' (using password: YES)
12.4. Заповнення таблиці
Ми напишемо скрипт PHP, який виконує команди SQL, що містяться в наступному текстовому файлі [creation.txt]:
Коментарі
- мова SQL (Structured Query Language) не розрізняє регістр (великі та малі літери) команд SQL;
- рядок 1: видаляється таблиця [personnes], якщо вона існує;
- рядок 2: серверу MySQL вказується, що йому будуть надіслані символи, закодовані у форматі UTF-8. Ця команда SQL, характерна для MySQL, необхідна тут, наприклад, для того, щоб у базі даних з’явився рядок 7 — «é» у імені Géraldine. Якщо не вказати рядок 2, «é» буде перетворено на послідовність із двох дивних символів. Клієнтом є скрипт PHP, написаний у NetBeans. Він кодує файли у формат UTF-8 та [1-4], наведені нижче:

- рядок 3: створення таблиці [personnes] із трьома стовпцями (ім’я, прізвище, вік) та первинним ключем (прізвище, ім’я);
- рядки 4–10: вставлення 7 рядків у таблицю [personnes];
- рядок 6: ця команда вставки має завершитися невдачею, оскільки вона намагається виконати ту саму вставку, що й у рядку 5. Обмеження первинного ключа має запобігти цій вставці: не може бути двох осіб з однаковим ім’ям та прізвищем;
- рядок 10: ця команда вставки повинна завершитися невдачею, оскільки вона намагається виконати ту саму вставку, що й у рядку 9;
Скрипт PHP, який відповідає за виконання команд SQL з цього текстового файлу, має такий вигляд: [mysql-03.php]:
<?php
// ідентифікатор бази даних
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// ідентифікатори користувача
const ID = "admpersonnes";
const PWD = "nobody";
// ідентифікатор текстового файлу команд SQL, що підлягають виконанню
const SQL_COMMANDS_FILENAME = "creation.txt";
// відкриття з'єднання з базою даних MySql
try {
$connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
// виведення повідомлення про помилку
print "Erreur : " . $ex->getMessage() . "\n";
exit;
}
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// виконання файлу команд SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE, FALSE);
// закриття з'єднання
$connexion = NULL;
// виведення кількості помилок
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
print "$erreurs[$i]\n";
}
// завершено
print "Terminé\n";
exit;
// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE, bool $arrêt = TRUE): array {
// використовує з'єднання $connexion
// виконує команди SQL, що містяться у текстовому файлі SQLFileName
// цей файл є файлом команд SQL, які слід виконувати по одній на рядок
// якщо $suivi=1, то після кожного виконання команди SQL на екрані з’являється повідомлення про успіх або невдачу
// якщо $arrêt=1, функція зупиняється при першій виявленій помилці, інакше вона виконує всі команди SQL
// функція повертає масив (кількість помилок, помилка1, помилка2…)
// перевіряється наявність файлу SQLFileName
if (!file_exists($SQLFileName)) {
return ["Le fichier [$SQLFileName] n'existe pas"];
}
// виконуються запити SQL, що містяться у файлі SQLFileName
// їх поміщають у масив
$requêtes = file($SQLFileName);
// помилка?
if ($requêtes === FALSE) {
return ["Erreur lors de l'exploitation du fichier SQL [$SQLFileName]"];
}
// запити виконуються по черзі — спочатку помилок немає
$erreurs = [];
$i = 0;
$fini = FALSE;
while ($i < count($requêtes) && !$fini) {
// отримуємо текст запиту
// trim видалить символ кінця рядка
$requête = trim($requêtes[$i]);
// порожній запит?
if (strlen($requête) == 0) {
// запит ігнорується, і виконується наступний запит
$i++;
continue;
}
try {
// виконання запиту — може бути згенеровано виняток
$connexion->exec($requête);
// відстежувати екран чи ні?
if ($suivi) {
print "$requête : Exécution réussie\n";
}
} catch (PDOException $ex) {
// сталася помилка
addError($erreurs, $requête, $ex->getMessage(), $suivi);
// чи зупинятися?
$fini = $arrêt;
}
// наступний запит
$i++;
}
// результат
return $erreurs;
}
function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
// додаємо повідомлення про помилку
$msg = "$requête : Erreur (" . $msg . ")";
$erreurs[] = $msg;
// відстежувати екран чи ні?
if ($suivi) {
print "$msg\n";
}
}
Коментарі
- функція [exécuterCommandes] (рядки 36–89) відповідає за виконання команд SQL, які вона знаходить у текстовому файлі [$SQLFileName] (параметр 2). Для їх виконання вона використовує відкрите з’єднання [$connexion] (параметр 1) із сервером MySQL. Третій параметр [$suivi] — це логічне значення, яке керує відображенням на екрані: у TRUE виконана команда SQL відображається на екрані з зазначенням успіху або невдачі, в іншому випадку виконання команди SQL відбувається без відображення на екрані. Четвертий параметр [$arrêt] визначає, що робити у разі невдалого виконання команди SQL: у TRUE вказано, що виконання команд SQL має зупинитися; в іншому випадку воно продовжується. Функція [exécuterCommandes] повертає масив повідомлень про помилки, який є порожнім, якщо помилок не було;
- рядки 11–18: відкривається з’єднання з базою даних MySQL [dbpersonnes]. Якщо відкриття не вдається, виводиться повідомлення про помилку і виконання зупиняється (рядки 14–18);
- рядок 22: відкрите з’єднання передається функції [exécuterCommandes]. Воно буде закрите після повернення з функції (рядок 24);
- рядок 20: перед передачею з’єднання до функції [exécuterCommandes] його налаштовують. У разі помилки операції SQL з об’єктом [PDO] можуть або повернути булеве значення FALSE (значення за замовчуванням), або викликати виняток. У рядку 20 обрано саме другий варіант. Адже легко «забути» перевірити булеве значення результату виконання команди SQL. Це призведе до помилки пізніше, але в іншому місці коду, що ускладнить визначення її першоджерела. У разі некерованого винятку (відсутність catch) виняток буде просуватися вгору по коду, доки не зустріне catch або не дійде до інтерпретатора PHP, який перехопить цей виняток. У цьому випадку відображаються характер винятку та місце його виникнення в коді;
- рядок 22: викликається функція [exécuterCommandes] для виконання файлу команд SQL [$SQLFileName];
- рядки 45–47: перевіряється, чи дійсно існує файл команд SQL. Якщо ні, фіксується помилка та повертається цей результат;
- рядок 51: заносимо накази SQL у масив [$requêtes]. Рядки 53–55: якщо операція завершилася невдало, повертаємо масив помилок з єдиним повідомленням;
- рядок 57: помилки накопичуються в масиві [$erreurs];
- рядок 58: номер запиту;
- рядок 59: логічна змінна [$fini] контролює виконання команд SQL з масиву [$requêtes]. Коли виконання доходить до TRUE, воно зупиняється;
- рядок 60: виконується цикл по всіх запитах;
- рядок 63: витягується текст команди SQL № i. Функція [trim] видалить пробіли, що передують і слідують за текстом команди SQL. Під «пробілами» маються на увазі символи пробілу \b, повернення каретки \r, кінцевий символ рядка \n, перехід на нову сторінку \f, табуляція \t… Для нас тут важливо, що кінцевий символ рядка тексту SQL зникне;
- рядки 65–69: якщо текст SQL порожній, то запит ігнорується і переходимо до наступного;
- рядок 72: на сервер надсилається запит SQL. Метод [PDO::exec] викличе виняток, якщо виконання завершиться невдало. Нагадаємо, що така поведінка зумовлена налаштуванням, зробленим у рядку 20;
- рядок 79: повідомлення про помилку додається до масиву помилок;
- рядок 81: встановлюється логічна змінна [$fini], яка керує циклом. Якщо параметр [$arrêt] (рядок 36) дорівнює TRUE, цикл слід зупинити;
- рядки 74–76: якщо виконання команди SQL завершилося успішно, її виводиться на екран, якщо значення параметра [$suivi] (рядок 36) дорівнює TRUE;
- рядок 87: після виконання всіх команд SQL повертається масив помилок [$erreurs];
Функція [adError] у рядках 90–97 дозволяє додати помилку до масиву помилок [$erreurs]:
- рядок 90: функція отримує 4 параметри:
- параметр [$erreurs] передається за посиланням. Адже ми хочемо оперувати масивом, який передається як параметр, а не його копією;
- параметр [$requête] — це текст SQL із замовлення, яке завершилося невдачею;
- параметр [$msg] — це повідомлення про помилку, пов’язане з замовленням, виконання якого завершилося невдало;
- логічне значення [$suivi] вказує, чи слід відображати повідомлення про помилку ($suivi=TRUE) чи ні ($suivi=FALSE) на консолі;
Функція [exécuterCommandes] викликається скриптом у рядках 3–33:
- рядки 11–18: встановлюється з’єднання з базою даних MySQL [dbpersonnes];
- рядок 20: налаштовується з’єднання;
- рядок 22: потім виконується файл команд SQL;
- рядок 24: з’єднання закривається;
- рядки 26–29: відображаються помилки, повернуті функцією [exécuterCommandes];
Результати на екрані:
drop table if exists personnes : Exécution réussie
SET NAMES 'utf8' : Exécution réussie
create table personnes (prenom varchar(30) not null, nom varchar(30) not null, age integer not null, primary key (nom,prenom)) : Exécution réussie
insert into personnes (prenom, nom, age) values('Paul','Langevin',48) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Lefur-Sylvie' for key 'PRIMARY')
insert into personnes (prenom, nom, age) values ('Pierre','Nicazou',35) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Géraldine','Colou',26) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Exécution réussie
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Girond-Paulette' for key 'PRIMARY')
-----------------------
Il y a eu 2 erreur(s)
insert into personnes (prenom, nom, age) values ('Sylvie','Lefur',70) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Lefur-Sylvie' for key 'PRIMARY')
insert into personnes (prenom, nom, age) values ('Paulette','Girond',56) : Erreur (SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'Girond-Paulette' for key 'PRIMARY')
Terminé
Внесені зміни можна переглянути за допомогою phpMyAdmin:

12.5. Виконання довільних команд SQL
Наступний скрипт демонструє виконання команд SQL із наступного текстового файлу [sql.txt]:
Серед цих команд SQL є команда select, яка повертає результати з бази даних, команди insert, update, delete, які змінюють базу даних, не повертаючи результатів, і, нарешті, помилкові команди, такі як остання (xselect). Скрипт [mysql-04.php] має такий вигляд:
<?php
// ідентифікатор бази даних
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// ідентифікатори користувача
const ID = "admpersonnes";
const PWD = "nobody";
// ідентифікатор текстового файлу команд SQL, що підлягають виконанню
const SQL_COMMANDS_FILENAME = "sql.txt";
try {
// підключення до бази даних MySql
$connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
// виведення повідомлень про помилки
print "Erreur : " . $ex->getMessage() . "\n";
exit;
}
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// виконання файлу команд SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE, FALSE);
// закриття з'єднання
$connexion = NULL;
// виведення кількості помилок
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
print "$erreurs[$i]\n";
}
// завершено
print "Terminé\n";
exit;
// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE, bool $arrêt = TRUE): array {
………………………………………………………….
// запити виконуються по черзі — спочатку помилок немає
$erreurs = [];
$i = 0;
$fini = FALSE;
while ($i < count($requêtes) && !$fini) {
// отримано текст запиту
// команда trim видалить символ кінця рядка
$requête = trim($requêtes[$i]);
// порожній запит?
if (strlen($requête) == 0) {
// запит ігнорується, і виконується наступний запит
$i++;
continue;
}
// виконання запиту
// отримано його ім'я
$commande = "";
if (preg_match("/^\s*(\S+)/", $requête, $champs)) {
$commande = strtolower($champs[0]);
}
try {
// чи це команда SELECT?
if ($commande === "select") {
$résultat = $connexion->query($requête);
} else {
$résultat = $connexion->exec($requête);
}
// відстежувати на екрані чи ні?
if ($suivi) {
print "[$requête] : Exécution réussie\n";
}
// відображення результату виконання
afficherInfos($commande, $résultat);
} catch (PDOException $ex) {
// сталася помилка
addError($erreurs, $requête, $ex->getMessage(), $suivi);
// чи зупиняємося?
$fini = $arrêt;
}
// наступний запит
$i++;
}
// результат
return $erreurs;
}
function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
…
}
// ---------------------------------------------------------------------------------
function afficherInfos(string $commande, $résultat): void {
// виводить результат $résultat запиту SQL
// чи це був запит SELECT?
switch ($commande) {
case "select" :
// відображаються назви полів
$titre = "";
$nbColonnes = $résultat->columnCount();
for ($i = 0; $i < $nbColonnes; $i++) {
$infos = $résultat->getColumnMeta($i);
$titre .= $infos['name'] . ",";
}
// видаляється останній символ ,
$titre = substr($titre, 0, strlen($titre) - 1);
// виводиться список полів
print "$titre\n";
// розділювальний рядок
$séparateurs = "";
for ($i = 0; $i < strlen($titre); $i++) {
$séparateurs .= "-";
}
print "$séparateurs\n";
// дані
foreach ($résultat as $ligne) {
$data = "";
for ($i = 0; $i < $nbColonnes; $i++) {
$data .= $ligne[$i] . ",";
}
// видаляється останній символ,
$data = substr($data, 0, strlen($data) - 1);
// виводиться
print "$data\n";
}
break;
case "update":
case "insert":
case "delete";
print " $résultat lignes(s) a (ont) été modifiée(s)\n";
break;
}
}
Коментарі
- рядки 36–83: функція [exécuterCommandes] дещо змінена: команда SQL [select] виконується не так само, як інші команди SQL. Ця команда є єдиною, яка повертає у результаті таблицю, тобто набір рядків і стовпців бази даних;
- рядки 55–57: виділяємо перше слово з команди SQL за допомогою регулярного виразу;
- рядки 60–64: якщо команда SQL дорівнює [select], для виконання команди SQL використовується метод [PDO::query], інакше — метод [PDO::exec]. В обох випадках, якщо виконання завершиться невдало, буде згенеровано виняток, який перехоплюється в рядках 71–77. Якщо виконання завершиться успішно, у рядку 70 відображається його результат;
- рядки 90–130: функція afficherInfos виводить інформацію про результат виконання команди SQL;
- рядок 94: обробляється випадок [select]. Його результатом є об’єкт типу [PDOStatement];
- рядок 96: метод [PDOStatement::getColumnCount()] повертає кількість стовпців таблиці, отриманої в результаті виконання select;
- рядки 98–99: метод [PDOStatement::getMeta(i)] повертає словник з інформацією про стовпець № i з таблиці результатів методу select.. У цьому словнику значенням, пов’язаним із ключем «name», є назва стовпця;
- рядки 97–102: імена стовпців таблиці результатів select об’єднуються в один рядок;
- рядки 105–110: створюється розділовий рядок, довжина якого дорівнює довжині створеного раніше символьного рядка;
- рядки 112–121: об’єкт типу PDOStatement можна обходити за допомогою циклу foreach. На кожній ітерації отриманим елементом є рядок таблиці результатів select у вигляді масиву значень, що представляють значення різних стовпців цього рядка. Усі ці значення виводяться за допомогою циклу for (рядки 114–116);
- рядки 123–127: результатом виконання команд insert, update, delete є кількість рядків, змінених цими командами;
Результати на екрані:
[set names 'utf8'] : Exécution réussie
[select * from personnes] : Exécution réussie
prenom,nom,age
--------------
Géraldine,Colou,26
Paulette,Girond,56
Paul,Langevin,48
Sylvie,Lefur,70
Pierre,Nicazou,35
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom,prenom
----------
Colou,Géraldine
Girond,Paulette
Langevin,Paul
Lefur,Sylvie
Nicazou,Pierre
[select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
prenom,nom,age
--------------
Pierre,Nicazou,35
Géraldine,Colou,26
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,47
[delete from personnes where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
-----------------------
Il y a eu 1 erreur(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
Terminé
12.6. Використання підготовлених команд SQL
12.6.1. Приклад 1
Розглянемо наступний скрипт [mysql-05.php]:
<?php
// ідентифікатор бази даних
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// ідентифікатори користувача
const ID = "admpersonnes";
const PWD = "nobody";
try {
// підключення до бази MySql
$connexion = new PDO(DSN, ID, PWD);
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// таблицю осіб очищають
$connexion->exec("delete from personnes");
// список осіб
$personnes = [];
$personnes[] = ["nom" => "Langevin", "prenom" => "Paul", "age" => 47];
$personnes[] = ["nom" => "Lefur", "prenom" => "Sylvie", "age" => 28];
// ми внесемо цих осіб до бази даних
$statement = $connexion->prepare("insert into personnes (nom, prenom, age) values (:nom, :prenom, :age)");
for ($i = 0; $i < count($personnes); $i++) {
$statement->execute($personnes[$i]);
}
} catch (PDOException $ex) {
// виведення повідомлення про помилку
print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// закриття з'єднання
$connexion = NULL;
}
// все закінчено
print "Terminé\n";
exit;
Коментарі
Нас тут цікавлять рядки 16–24, які вставляють двох осіб у таблицю осіб бази даних [dbpersonnes].
- рядок 21: «готується» наказ SQL із заданими параметрами. Перед параметрами стоїть символ : :nom, :prenom, :age. Щоб «підготувати» замовлення SQL, використовується метод [PDO::prepare]. Результатом є тип [PDOStatement]. «Підготовка» не є виконанням: нічого не виконується;
- рядок 23: виконання «підготовленого» наказу за допомогою методу [PDOStatement::execute]. Для цього потрібно присвоїти значення параметрам :nom, :prenom та :age. Зробити це можна кількома способами. Тут використовується словник, ключами якого є параметри підготовленого замовлення, що передаються методу [PDOStatement::execute]. Інший спосіб — присвоїти значення параметрам за допомогою методу [PDOStatement::bindValue($paramètre,$valeur)]. Наприклад, ось тут:
$statement→bindValue(“nom”,”Langevin”);
$statement→bindValue(“prenom”,”Paul”);
$statement→bindValue(“age”,47);
$statement→execute();
Недоліком є те, що цю інструкцію потрібно повторювати для кожного параметра. У такому разі метод зі словником може виявитися зручнішим. Метод [PDOStatement::execute] повертає FALSE, якщо виконання завершується з помилкою;
- метод, який тут використовується для вставлення:
- підготовлений метод;
- n виконань підготовленого оператора;
є більш економічною за часом виконання, ніж виконання n різних команд SQL. Тому цей метод слід вважати пріоритетним. Його можна використовувати для команд SQL select, update, delete, insert. У випадку запиту SQL select після його виконання за допомогою [PDOStatement::execute] рядки результату отримують за допомогою методу [PDOStatement::fetchAll];
12.6.2. Приклад 2
Наступний скрипт [mysql-06.php] демонструє використання підготовленого запиту для операції SQL select, а також різні способи отримання рядків, повернутих цією операцією:
<?php
// ідентифікатор бази даних
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// ідентифікатори користувача
const ID = "admpersonnes";
const PWD = "nobody";
try {
// підключення до бази даних MySql
$connexion = new PDO(DSN, ID, PWD);
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// таблицю осіб очищають
$connexion->exec("delete from personnes");
// ці особи будуть внесені до бази даних
$statement = $connexion->prepare("insert into personnes (nom, prenom, age) values (:nom, :prenom, :age)");
for ($i = 0; $i < 10; $i++) {
$statement->execute(["nom" => "nom" . $i, "prenom" => "prenom" . $i, "age" => $i * 10]);
}
// здійснюється запит до бази даних
$statement = $connexion->prepare("select nom, prenom, age from personnes");
$statement->execute();
// 1-й рядок
$ligne = $statement->fetch();
var_dump($ligne);
// 2-й рядок
$ligne = $statement->fetch(PDO::FETCH_ASSOC);
var_dump($ligne);
// 3-й рядок
$ligne = $statement->fetch(PDO::FETCH_OBJ);
var_dump($ligne);
// 4-й рядок
$statement->setFetchMode(PDO::FETCH_CLASS, "Person");
$ligne = $statement->fetch();
var_dump($ligne);
// послідовне зчитування всіх рядків
$statement = $connexion->prepare("select nom, prenom, age from personnes");
$statement->execute();
$statement->setFetchMode(PDO::FETCH_CLASS, "Person");
while ($personne = $statement->fetch()) {
print "$personne\n";
}
} catch (PDOException $ex) {
// відображення помилки
print "Erreur : " . $ex->getMessage() . "\n";
} finally {
// закриття з'єднання
$connexion = NULL;
}
// завершено
print "Terminé\n";
exit;
class Person {
private $nom;
private $prenom;
private $age;
public function __toString() {
return "Personne[$this->nom,$this->prenom,$this->age]";
}
}
Коментарі
- рядки 17–20: в таблицю [personnes] бази даних [admpersonnes] вставляється 10 рядків:

- рядок 22: «готується» замовлення SQL [select], яке виконується в рядку 23;
- рядок 25: за допомогою методу [PDOStatement::fetch] отримуємо рядок з результату виконаної операції SQL [select]. Метод [PDOStatement::fetch] може різними способами отримувати рядки-результати підготовленої операції SQL [select]. У цьому скрипті наведено декілька з них. Метод [PDOStatement::fetch] без параметрів повертає поточний рядок з [select] у вигляді словника, індексованого як за номерами стовпців, так і за їхніми назвами;
- рядок 26: відображає такий результат:
array(6) {
["nom"]=>
string(4) "nom0"
[0]=>
string(4) "nom0"
["prenom"]=>
string(7) "prenom0"
[1]=>
string(7) "prenom0"
["age"]=>
string(1) "0"
[2]=>
string(1) "0"
}
- рядки 28–29: параметр [PDO::FETCH_ASSOC] забезпечує, що повернений рядок є словником, індексованим за назвами стовпців таблиці:
- рядки 31–32: параметр [PDO::FETCH_OBJ] робить так, що повернений рядок є об’єктом типу [stdclass], атрибутами якого є імена стовпців таблиці:
- рядок 34: режим пошуку методу [fetch] встановлюється за допомогою методу [PDOStatement::setFetchMode]. Цей режим стає режимом за замовчуванням, доки його не буде змінено або за допомогою іншої операції [PDOStatement::setFetchMode], або шляхом передачі режиму як параметра методу [PDOStatement::fetch], як це було зроблено раніше. Операція [setFetchMode(PDO::FETCH_CLASS, "Person")] вказує, що прочитаний рядок має бути розміщений в об’єкті типу [Person]. Цей клас повинен мати серед своїх атрибутів такі, що мають імена стовпців прочитаного рядка. Це стосується класу [Person], визначеного в рядках 56–63;
- у рядку 36 відображається такий результат:
- рядки 38–43: показують, як послідовно використовувати результати [select];
- рядок 42: для відображення [$personne] буде використано метод [__toString] класу [Person];
12.7. Використання транзакцій
Транзакція дозволяє об’єднати послідовність команд SQL в одну одиницю виконання: або всі команди виконуються успішно, або одна з них завершується з помилкою, і тоді всі команди SQL, що передували цій, скасовуються. Інакше кажучи, коли для виконання команд SQL використовується транзакція, після її виконання база даних перебуває у стабільному стані:
- або у новому стані, створеному успішним виконанням усіх команд SQL транзакції;
- або у стані, в якому вона перебувала до початку виконання транзакції;
Ми повернемося до прикладу виконання команд SQL, що містяться у текстовому файлі, розглянутому в розділі «Посилання». Ми включимо це виконання до транзакції. Команди SQL міститимуться у наступному файлі [sql2.txt]:
set names 'utf8'
select * from personnes
select nom,prenom from personnes order by nom asc, prenom desc
select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
insert into personnes values('Josette','Bruneau',46)
update personnes set age=47 where nom='Bruneau'
select * from personnes where nom='Bruneau'
delete from personnes where nom='Bruneau'
select * from personnes where nom='Bruneau'
insert into personnes values('Josette','Bruneau',46)
select * from personnes where nom='Bruneau'
xselect * from personnes where nom='Bruneau'
Помилковий запит у рядку 12 призведе до зриву всієї транзакції. Отже, база даних має повернутися до стану, в якому вона перебувала до початку транзакції. У наведеному вище прикладі ми не повинні бачити в таблиці рядок, вставлений у рядку 10 вище. Скрипт майже не змінився. Проте наведемо повний код [mysql-07.php]:
<?php
// ідентифікатор бази даних
const DSN = "mysql:host=localhost;dbname=dbpersonnes";
// ідентифікатори користувача
const ID = "admpersonnes";
const PWD = "nobody";
// ідентифікатор текстового файлу команд SQL, що підлягають виконанню
const SQL_COMMANDS_FILENAME = "sql2.txt";
try {
// підключення до бази даних MySql
$connexion = new PDO(DSN, ID, PWD);
} catch (PDOException $ex) {
// виведення повідомлень про помилки
print "Erreur : " . $ex->getMessage() . "\n";
exit;
}
// потрібно, щоб при кожній помилці SGBD генерувалося виключення
$connexion->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// виконання файлу команд SQL
$erreurs = exécuterCommandes($connexion, SQL_COMMANDS_FILENAME, TRUE);
// закриття з'єднання
$connexion = NULL;
// виведення кількості помилок
printf("\n-----------------------\nIl y a eu %d erreur(s)\n", count($erreurs));
for ($i = 0; $i < count($erreurs); $i++) {
print "$erreurs[$i]\n";
}
// завершено
print "Terminé\n";
exit;
// ---------------------------------------------------------------------------------
function exécuterCommandes(PDO $connexion, string $SQLFileName, bool $suivi = FALSE): array {
// використовує з'єднання $connexion
// виконує команди SQL, що містяться у текстовому файлі SQLFileName
// цей файл є файлом команд SQL, які виконуються по одній на рядок
// команди SQL виконуються в рамках однієї транзакції
// якщо хоча б одна з команд завершиться з помилкою, транзакція скасовується, і база даних повертається до стану, в якому вона перебувала до початку транзакції
// якщо $suivi=1, то після кожного виконання команди SQL відображається повідомлення про її успіх або невдачу
// функція повертає масив (кількість помилок, помилка1, помилка2…)
//
// перевіряється наявність файлу SQLFileName
if (!file_exists($SQLFileName)) {
return ["Le fichier [$SQLFileName] n'existe pas"];
}
// виконуються запити SQL, що містяться у файлі SQLFileName
// їх поміщають у масив
$requêtes = file($SQLFileName);
// помилка?
if ($requêtes === FALSE) {
return ["Erreur lors de l'exploitation du fichier SQL [$SQLFileName]"];
}
// запити будуть поміщені в транзакцію
$connexion->beginTransaction();
// запити виконуються по черзі — спочатку помилок немає
$erreurs = [];
$i = 0;
$fini = FALSE;
while ($i < count($requêtes) && !$fini) {
// отримуємо текст запиту
// функція trim видалить символ кінця рядка
$requête = trim($requêtes[$i]);
// запит порожній?
if (strlen($requête) == 0) {
// запит ігнорується, і виконується наступний запит
$i++;
continue;
}
// виконання запиту
// отримано його ім'я
$commande = "";
if (preg_match("/^\s*(\S+)/", $requête, $champs)) {
$commande = strtolower($champs[0]);
}
try {
// чи це замовлення SELECT?
if ($commande === "select") {
$résultat = $connexion->query($requête);
} else {
$résultat = $connexion->exec($requête);
}
// відстежувати на екрані чи ні?
if ($suivi) {
print "[$requête] : Exécution réussie\n";
}
// відображення результату виконання
afficherInfos($commande, $résultat);
} catch (PDOException $ex) {
// сталася помилка
addError($erreurs, $requête, $ex->getMessage(), $suivi);
// зупинка на наступному циклі
$fini = TRUE;
}
// наступний запит
$i++;
}
// кінець транзакції
if (!$fini) {
// помилок не було: транзакція підтверджена
$connexion->commit();
} else {
// сталися помилки: транзакція скасована
$connexion->rollBack();
// додано помилку
addError($erreurs, "", "Transaction annulée", $suivi);
}
// результат
return $erreurs;
}
function addError(array &$erreurs, string $requête, string $msg, bool $suivi): void {
…
}
// ---------------------------------------------------------------------------------
function afficherInfos(string $commande, $résultat): void {
…
}
Коментарі
Ми виділили зміни в оригінальному скрипті [mysql-04.php].
- рядки 22, 36: функція [exécuterCommandes] втратила свій четвертий параметр [$arrêt=TRUE]. Оскільки команди SQL виконуються в рамках транзакції, будь-яка помилка призведе до її припинення;
- рядки 40–41: виклик функції в рамках транзакції;
- рядок 57: запускається транзакція. Відтепер будь-яка команда SQL, що виконується у циклі рядків 62–99, виконується в межах цієї транзакції;
- рядки 101–109: булеве значення [$fini] дорівнює TRUE, якщо сталася помилка (рядок 95). Коли його значення дорівнює FALSE, помилок не було, і транзакція підтверджується (рядок 103). Якщо його значення дорівнює TRUE, то сталися помилки, і транзакцію скасовують (рядок 106), а помилку транзакції додають до списку помилок (рядок 108);
Результати
Перед виконанням скрипта база даних [admpersonnes] перебуває в такому стані:

Виконується скрипт [mysql-07.php]. При цьому на екрані з’являються такі повідомлення:
[set names 'utf8'] : Exécution réussie
[select * from personnes] : Exécution réussie
prenom,nom,age
--------------
prenom0,nom0,0
prenom1,nom1,10
prenom2,nom2,20
prenom3,nom3,30
prenom4,nom4,40
prenom5,nom5,50
prenom6,nom6,60
prenom7,nom7,70
prenom8,nom8,80
prenom9,nom9,90
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom,prenom
----------
nom0,prenom0
nom1,prenom1
nom2,prenom2
nom3,prenom3
nom4,prenom4
nom5,prenom5
nom6,prenom6
nom7,prenom7
nom8,prenom8
nom9,prenom9
[select * from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
prenom,nom,age
--------------
prenom4,nom4,40
prenom3,nom3,30
prenom2,nom2,20
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,47
[delete from personnes where nom='Bruneau'] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
[insert into personnes values('Josette','Bruneau',46)] : Exécution réussie
1 lignes(s) a (ont) été modifiée(s)
[select * from personnes where nom='Bruneau'] : Exécution réussie
prenom,nom,age
--------------
Josette,Bruneau,46
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
[] : Erreur (Transaction annulée)
-----------------------
Il y a eu 2 erreur(s)
[xselect * from personnes where nom='Bruneau'] : Erreur (SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xselect * from personnes where nom='Bruneau'' at line 1)
[] : Erreur (Transaction annulée)
Terminé
- рядок 53: сталася помилка під час виконання команди [xselect];
- рядок 54: транзакція скасовується;
Якщо перевірити стан бази даних, то вона перебуває в тому ж стані, що й до виконання скрипта. Зокрема, у ній відсутній рядок [Josette, Bruneau, 46] із рядка 52 наведених вище результатів.

Підсумок
- транзакція починається з методу [PDO::beginTransaction];
- вона успішно завершується методом [PDO::commit];
- вона завершується з помилкою за допомогою методу [PDO::rollback];
Під час роботи з базою даних рекомендується включати всі операції SQL у транзакцію, щоб ізолюватися від інших користувачів бази (це також є її функцією). Транзакція має бути якомога коротшою. Тому не слід забувати завершувати її командою [commit] або [rollback], залежно від ситуації.