Pdo запросы. Основы работы с расширением PDO
Термин PDO является сокращением понятия PHP Data Objects . Как можно судить по названию, эта технология позволяет работать с содержимым базы данных через объекты.
Почему не myqli или mysql?
Чаще всего, в отношении новых технологий, встает вопрос их преимуществ перед старыми-добрыми и проверенными инструментами, а также, перевода на них текущих и старых проектов.
Объектная ориентированность PDO
PHP развивается очень активно и стремится стать одним из лучших инструментов для быстрой разработки веб приложений как массового, так и корпоративного уровня.
Говоря о PHP , будем подразумевать современный объектно-ориентированный PHP , позволяющий писать универсальный код, удобный для тестирования и повторного использования.
Использование PDO позволяет вынести работу с базой данных на объектно-ориентированный уровень и улучшить переносимость кода. На самом деле, использование PDO не так сложно, как можно было бы подумать.
Абстракция
Представим, что мы уже продолжительное время разрабатываем приложение, с использованием MySQL . И вот, в один прекрасный момент, появляется необходимость заменить MySQL на PostgreSQL .
Как минимум, нам придется заменить все вызовы mysqli_connect() (mysql_connect()) на pg_connect() и, по аналогии, другие функции, используемые для запроса и обработки данных.
При использовании PDO , мы ограничимся изменением нескольких параметров в файлах конфигурации.
Связывание параметров
Использование связанных параметров предоставляет большую гибкость в составлении запросов и позволяет улучшить защиту от SQL инъекций.
Получение данных в виде объектов
Те, кто уже использует ORM (object-relational mapping — объектно-реляционное отображение данных), например, Doctrine , знают удобство представления данных из таблиц БД в виде объектов. PDO позволяет получать данные в виде объектов и без использования ORM .
Расширение mysql больше не поддерживается
Поддержка расширения mysql окончательно удалена из нового PHP 7 . Если вы планируете переносить проект на новую версию PHP , уже сейчас следует использовать в нем, как минимум, mysqli. Конечно же, лучше начинать использовать PDO , если вы еще не сделали этого.
Мне кажется, что этих причин достаточно для склонения весов в сторону использования PDO . Тем более, не нужно ничего дополнительно устанавливать.
Проверяем наличие PDO в системе
Версии PHP 5.5 и выше, чаще всего, уже содержать расширение для работы с PDO . Для проверки достаточно выполнить в консоли простую команду:
php -i | grep "pdo"
Теперь откроем его в любом браузере и найдем нужные данные поиском по строке PDO .
Знакомимся с PDO
Процесс работы с PDO не слишком отличается от традиционного. В общем случае, процесс использования PDO выглядит так:
- Подключение к базе данных;
- По необходимости, подготовка запроса и связывание параметров;
- Выполнение запроса.
Подключение к базе данных
Для подключения к базе данных нужно создать новый объект PDO и передать ему имя источника данных, так же известного как DSN .
В общем случае, DSN состоит из имени драйвера, отделенного двоеточием от строки подключения, специфичной для каждого драйвера PDO .
Для MySQL , подключение выполняется так:
$connection = new PDO("mysql:host=localhost;dbname=mydb;charset=utf8", "root", "root");
$connection = new PDO ("mysql:host=localhost;dbname=mydb;charset=utf8" , "root" , "root" ) ; |
В данном случае, DSN содержит имя драйвера mysql , указание хоста (возможен формат host=ИМЯ_ХОСТА:ПОРТ ), имя базы данных, кодировка, имя пользователя MySQL и его пароль.
Запросы
В отличие от mysqli_query() , в PDO есть два типа запросов:
- Возвращающие результат (select, show );
- Не возвращающие результат (insert , detele и другие).
Первым делом, рассмотрим второй вариант.
Выполнение запросов
Рассмотрим пример выполнения запроса на примере insert .
$connection->exec("INSERT INTO users VALUES (1, "somevalue"");
$connection -> exec () ; |
Конечно же, данный запрос возвращает количество затронутых строк и увидеть его можно следующим образом.
$affectedRows = $connection->exec("INSERT INTO users VALUES (1, "somevalue""); echo $affectedRows;
$affectedRows = $connection -> exec ("INSERT INTO users VALUES (1, "somevalue"" ) ; echo $affectedRows ; |
Получение результатов запроса
В случае использования mysqli_query () , код мог бы быть следующим.
$result = mysql_query("SELECT * FROM users"); while($row = mysql_fetch_assoc($result)) { echo $row["id"] . " " . $row["name"]; }
$result = mysql_query ("SELECT * FROM users" ) ; while ($row = mysql_fetch_assoc ($result ) ) { |
Для PDO , код будет проще и лаконичнее.
foreach($connection->query("SELECT * FROM users") as $row) { echo $row["id"] . " " . $row["name"]; }
foreach ($connection -> query ("SELECT * FROM users" ) as $row ) { echo $row [ "id" ] . " " . $row [ "name" ] ; |
Режимы получения данных
Как и в mysqli , PDO позволяет получать данные в разных режимах. Для определения режима, класс PDO содержит соответствующие константы.
- PDO:: FETCH_ASSOC — возвращает массив, индексированный по имени столбца в таблице базы данных;
- PDO:: FETCH_NUM — возвращает массив, индексированный по номеру столбца;
- PDO:: FETCH_OBJ — возвращает анонимный объект с именами свойств, соответствующими именам столбцов. Например, $row->id будет содержать значение из столбца id.
- PDO:: FETCH_CLASS — возвращает новый экземпляр класса, со значениями свойств, соответствующими данным из строки таблицы. В случае, если указан параметр PDO:: FETCH_CLASSTYPE (например PDO:: FETCH_CLASS | PDO:: FETCH_CLASSTYPE ), имя класса будет определено из значения первого столбца.
Примечание : это не полный список, все возможные константы и варианты их комбинации доступны в документации .
Пример получения ассоциативного массива:
$statement = $connection->query("SELECT * FROM users"); while($row = $statement->fetch(PDO::FETCH_ASSOC)) { echo $row["id"] . " " . $row["name"]; }
$statement = $connection -> while ($row = $statement -> fetch (PDO:: FETCH_ASSOC ) ) { echo $row [ "id" ] . " " . $row [ "name" ] ; |
Примечание : Рекомендуется всегда указывать режим выборки, так как режим PDO:: FETCH_BOTH потребует вдвое больше памяти — фактически, будут созданы два массива, ассоциативный и обычный.
Рассмотрим использование режима выборки PDO:: FETCH_CLASS . Создадим класс User :
class User { protected $id; protected $name; public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } }
class User protected $id ; protected $name ; public function getId () return $this -> id ; public function setId ($id ) $this -> id = $id ; public function getName () return $this -> name ; public function setName ($name ) $this -> name = $name ; |
Теперь выберем данные и отобразим данные при помощи методов класса:
$statement = $connection->query("SELECT * FROM users"); while($row = $statement->fetch(PDO::FETCH_CLASS, "User")) { echo $row->getId() . " " . $row->getName(); }
$statement = $connection -> query ("SELECT * FROM users" ) ; while ($row = $statement -> fetch (PDO:: FETCH_CLASS , "User" ) ) { echo $row -> getId () . " " . $row -> getName () ; |
Подготовленные запросы и связывание параметров
Для понимания сути и всех преимуществ связывания параметров нужно более подробно рассмотреть механизмы PDO . При вызове $statement -> query () в коде выше, PDO подготовит запрос, выполнит его и вернет результат.
При вызове $connection -> prepare () создается подготовленный запрос. Подготовленные запросы — это способность системы управления базами данных получить шаблон запроса, скомпилировать его и выполнить после получения значений переменных, использованных в шаблоне. Похожим образом работают шаблонизаторы Smarty и Twig .
При вызове $statement -> execute () передаются значения для подстановки в шаблон запроса и СУБД выполняет запрос. Это действие аналогично вызову функции шаблонизатора render () .
Пример использования подготовленных запросов в PHP PDO :
В коде выше подготовлен запрос выборки записи с полем id равным значению, которое будет подставлено вместо : id . На данном этапе СУБД выполнит анализ и компиляцию запроса, возможно с использованием кеширования (зависит от настроек).
Теперь нужно передать недостающий параметр и выполнить запрос:
$id = 5; $statement->execute([ ":id" => $id ]);
Преимущества использования связанных параметров
Возможно, после рассмотрения механизма работы подготовленных запросов и связанных параметров, преимущества их использования стали очевидными.
PDO предоставляет удобную возможность экранирования пользовательских данных, например, такой код больше не нужен:
Вместо этого, теперь целесообразно делать так:
Можно, даже, еще укоротить код, используя нумерованные параметры вместо именованных:
В тоже время, использование подготовленных запросов позволяет улучшить производительность при многократном использовании запроса по одному шаблону. Пример выборки пяти случайных пользователей из базы данных:
$numberOfUsers = $connection->query("SELECT COUNT(*) FROM users")->fetchColumn(); $users = ; $statement = $connection->prepare("SELECT * FROM users WHERE id = ? LIMIT 1"); for ($i = 1; $i <= 5; $i++) { $id = rand(1, $numberOfUsers); $users = $statement->execute([$id])->fetch(PDO::FETCH_OBJ); }
$numberOfUsers = $connection -> query ("SELECT COUNT(*) FROM users" ) -> fetchColumn () ; $users = ; for ($i = 1 ; $i <= 5 ; $i ++ ) { $id = rand (1 , $numberOfUsers ) ; $users = $statement -> execute ([ $id ] ) -> fetch (PDO:: FETCH_OBJ ) ; |
При вызове метода prepare () , СУБД проведет анализ и скомпилирует запрос, при необходимости использует кеширование. Позже, в цикле for , происходит только выборка данных с указанным параметром. Такой подход позволяет быстрее получить данные, уменьшив время работы приложения.
При получении общего количества пользователей в базе данных был использован метод fetchColumn () . Этот метод позволяет получить значение одного столбца и является полезным при получении скалярных значений, таких как количество, сумма, максимально или минимальное значения.
Связанные значения и оператор IN
Часто, при начале работы с PDO , возникают трудности с оператором IN . Например, представим, что пользователь вводит несколько имен, разделенных запятыми. Пользовательский ввод хранится в переменной $names .
- Перевод
Множество PHP-разработчиков привыкли использовать для работы с базами данных расширения mysql и mysqli. Но с версии 5.1 в PHP существует более удобный способ - PHP Data Objects . Этот класс, сокращенно именуемый PDO, предоставляет методы для работы с объектами и prepared statements , которые заметно повысят вашу продуктивность!
Введение в PDO
«PDO – PHP Data Objects – это прослойка, которая предлагает универсальный способ работы с несколькими базами данных.»
Заботу об особенностях синтаксиса различных СУБД она оставляет разработчику, но делает процесс переключения между платформами гораздо менее болезненным. Нередко для этого требуется лишь изменить строку подключения к базе данных.
Эта статья написана для людей, которые пользуются mysql и mysqli, чтобы помочь им в переходе на более мощный и гибкий PDO.
Поддержка СУБД
Это расширение может поддерживать любую систему управления базами данных, для которой существует PDO-драйвер. На момент написания статьи доступны следующие драйвера:- PDO_CUBRID (CUBRID)
- PDO_DBLIB (FreeTDS / Microsoft SQL Server / Sybase)
- PDO_FIREBIRD (Firebird/Interbase 6)
- PDO_IBM (IBM DB2)
- PDO_INFORMIX (IBM Informix Dynamic Server)
- PDO_MYSQL (MySQL 3.x/4.x/5.x)
- PDO_OCI (Oracle Call Interface)
- PDO_ODBC (ODBC v3 (IBM DB2, unixODBC and win32 ODBC))
- PDO_PGSQL (PostgreSQL)
- PDO_SQLITE (SQLite 3 and SQLite 2)
- PDO_SQLSRV (Microsoft SQL Server)
- PDO_4D (4D)
print_r(PDO::getAvailableDrivers());
Подключение
Способы подключения к разным СУБД могут незначительно отличаться. Ниже приведены примеры подключения к наиболее популярным из них. Можно заметить, что первые три имеют идентичный синтаксис, в отличие от SQLite.try { # MS SQL Server и Sybase через PDO_DBLIB $DBH = new PDO("mssql:host=$host;dbname=$dbname", $user, $pass); $DBH = new PDO("sybase:host=$host;dbname=$dbname", $user, $pass); # MySQL через PDO_MYSQL $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass); # SQLite $DBH = new PDO("sqlite:my/database/path/database.db"); } catch(PDOException $e) { echo $e->getMessage(); }
Пожалуйста, обратите внимание на блок try/catch – всегда стоит оборачивать в него все свои PDO-операции и использовать механизм исключений (об этом чуть дальше).
$DBH расшифровывается как «database handle» и будет использоваться на протяжении всей статьи.
Закрыть любое подключение можно путем переопределения его переменной в null.
# закрывает подключение
$DBH = null;
Больше информации по теме отличительных опций разных СУБД и методах подключения к ним можно найти на php.net .
Исключения и PDO
PDO умеет выбрасывать исключения при ошибках, поэтому все должно находиться в блоке try/catch. Сразу после создания подключения, PDO можно перевести в любой из трех режимов ошибок:$DBH->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); $DBH->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); $DBH->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Но стоит заметить, что ошибка при попытке соединения будет всегда вызывать исключение.
PDO::ERRMODE_SILENT
Это режим по умолчанию. Примерно то же самое вы, скорее всего, используете для отлавливания ошибок в расширениях mysql и mysqli. Следующие два режима больше подходят для DRY программирования.PDO::ERRMODE_WARNING
Этот режим вызовет стандартный Warning и позволит скрипту продолжить выполнение. Удобен при отладке.PDO::ERRMODE_EXCEPTION
В большинстве ситуаций этот тип контроля выполнения скрипта предпочтителен. Он выбрасывает исключение, что позволяет вам ловко обрабатывать ошибки и скрывать щепетильную информацию. Как, например, тут:# подключаемся к базе данных try { $DBH = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass); $DBH->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); # Черт! Набрал DELECT вместо SELECT! $DBH->prepare("DELECT name FROM people")->execute(); } catch(PDOException $e) { echo "Хьюстон, у нас проблемы."; file_put_contents("PDOErrors.txt", $e->getMessage(), FILE_APPEND); }
В SQL-выражении есть синтаксическая ошибка, которая вызовет исключение. Мы можем записать детали ошибки в лог-файл и человеческим языком намекнуть пользователю, что что-то случилось.
Insert и Update
Вставка новых и обновление существующих данных являются одними из наиболее частых операций с БД. В случае с PDO этот процесс обычно состоит из двух шагов. (В следующей секции все относится как к UPDATE, так и INSERT)Тривиальный пример вставки новых данных:
# STH означает "Statement Handle" $STH = $DBH->prepare("INSERT INTO folks (first_name) values ("Cathy")"); $STH->execute();
Вообще-то можно сделать то же самое одним методом exec(), но двухшаговый способ дает все преимущества prepared statements. Они помогают в защите от SQL-инъекций, поэтому имеет смысл их использовать даже при однократном запросе.
Prepared Statements
Использование prepared statements укрепляет защиту от SQL-инъекций.
Prepared statement - это заранее скомпилированное SQL-выражение, которое может быть многократно выполнено путем отправки серверу лишь различных наборов данных. Дополнительным преимуществом является невозможность провести SQL-инъекцию через данные, используемые в placeholder’ах.
Ниже находятся три примера prepared statements.
# без placeholders - дверь SQL-инъекциям открыта!
$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values ($name, $addr, $city)");
# безымянные placeholders
$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values (?, ?, ?)");
# именные placeholders
$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values (:name, :addr, :city)");
Первый пример здесь лишь для сравнения, его стоит избегать. Разница между безымянными и именными placeholder’ами в том, как вы будете передавать данные в prepared statements.
Безымянные placeholder’ы
# назначаем переменные каждому placeholder, с индексами от 1 до 3 $STH->bindParam(1, $name); $STH->bindParam(2, $addr); $STH->bindParam(3, $city); # вставляем одну строку $name = "Daniel" $addr = "1 Wicked Way"; $city = "Arlington Heights"; $STH->execute(); # вставляем еще одну строку, уже с другими данными $name = "Steve" $addr = "5 Circle Drive"; $city = "Schaumburg"; $STH->execute();Здесь два шага. На первом мы назначаем всем placeholder’ам переменные (строки 2-4). Затем назначаем этим переменным значения и выполняем запрос. Чтобы послать новый набор данных, просто измените значения переменных и выполните запрос еще раз.
Если в вашем SQL-выражении много параметров, то назначать каждому по переменной весьма неудобно. В таких случаях можно хранить данные в массиве и передавать его:
# набор данных, которые мы будем вставлять
$data = array("Cathy", "9 Dark and Twisty Road", "Cardiff");
$STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values (?, ?, ?)");
$STH->execute($data);
$data вставится на место первого placeholder’а, $data - на место второго, и т.д. Но будьте внимательны: если ваши индексы сбиты, это работать не будет.
Именные placeholder’ы
# первым аргументом является имя placeholder’а # его принято начинать с двоеточия # хотя работает и без них $STH->bindParam(":name", $name);Здесь тоже можно передавать массив, но он должен быть ассоциативным. В роли ключей должны выступать, как можно догадаться, имена placeholder’ов.
# данные, которые мы вставляем $data = array("name" => "Cathy", "addr" => "9 Dark and Twisty", "city" => "Cardiff"); $STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values (:name, :addr, :city)"); $STH->execute($data);
Одним из удобств использования именных placeholder’ов является возможность вставки объектов напрямую в базу данных, если названия свойств совпадают с именами параметров. Вставку данных, к примеру, вы можете выполнить так:
# класс для простенького объекта class person { public $name; public $addr; public $city; function __construct($n,$a,$c) { $this->name = $n; $this->addr = $a; $this->city = $c; } # так далее... } $cathy = new person("Cathy","9 Dark and Twisty","Cardiff"); # а тут самое интересное $STH = $DBH->prepare("INSERT INTO folks (name, addr, city) values (:name, :addr, :city)"); $STH->execute((array)$cathy);
Преобразование объекта в массив при execute() приводит к тому, что свойства считаются ключами массива.
Выборка данных
Данные можно получить с помощью метода ->fetch(). Перед его вызовом желательно явно указать, в каком виде они вам требуются. Есть несколько вариантов:
- PDO::FETCH_ASSOC: возвращает массив с названиями столбцов в виде ключей
- PDO::FETCH_BOTH (по умолчанию): возвращает массив с индексами как в виде названий стобцов, так и их порядковых номеров
- PDO::FETCH_BOUND: присваивает значения столбцов соответствующим переменным, заданным с помощью метода ->bindColumn()
- PDO::FETCH_CLASS: присваивает значения столбцов соответствующим свойствам указанного класса. Если для какого-то столбца свойства нет, оно будет создано
- PDO::FETCH_INTO: обновляет существующий экземпляр указанного класса
- PDO::FETCH_LAZY: объединяет в себе PDO::FETCH_BOTH и PDO::FETCH_OBJ
- PDO::FETCH_NUM: возвращает массив с ключами в виде порядковых номеров столбцов
- PDO::FETCH_OBJ: возвращает анонимный объект со свойствами, соответствующими именам столбцов
$STH->setFetchMode(PDO::FETCH_ASSOC);
Также можно задать его напрямую при вызове метода ->fetch().
FETCH_ASSOC
При этом формате создается ассоциативный массив с названиями столбцов в виде индексов. Он должен быть знаком тем, кто использует расширения mysql/mysqli.# поскольку это обычный запрос без placeholder’ов, # можно сразу использовать метод query() $STH = $DBH->query("SELECT name, addr, city from folks"); # устанавливаем режим выборки $STH->setFetchMode(PDO::FETCH_ASSOC); while($row = $STH->fetch()) { echo $row["name"] . "\n"; echo $row["addr"] . "\n"; echo $row["city"] . "\n"; }
Цикл while() переберет весь результат запроса.
FETCH_OBJ
Данный тип получения данных создает экземпляр класса std для каждой строки.# создаем запрос $STH = $DBH->query("SELECT name, addr, city from folks"); # выбираем режим выборки $STH->setFetchMode(PDO::FETCH_OBJ); # выводим результат while($row = $STH->fetch()) { echo $row->name . "\n"; echo $row->addr . "\n"; echo $row->city . "\n"; }
FETCH_CLASS
При использовании fetch_class данные заносятся в экземпляры указанного класса. При этом значения назначаются свойствам объекта ДО вызова конструктора. Если свойства с именами, соответствующими названиям столбцов, не существуют, они будут созданы автоматически (с областью видимости public).Если ваши данные нуждаются в обязательной обработке сразу после их получения из базы данных, ее можно реализовать в конструкторе класса.
Для примера возьмем ситуацию, когда вам нужно скрыть часть адреса проживания человека.
class secret_person {
public $name;
public $addr;
public $city;
public $other_data;
function __construct($other = "") {
$this->addr = preg_replace("//", "x", $this->addr);
$this->other_data = $other;
}
}
При создании объекта все латинские буквы в нижнем регистре должны замениться на x. Проверим:
$STH = $DBH->query("SELECT name, addr, city from folks");
$STH->setFetchMode(PDO::FETCH_CLASS, "secret_person");
while($obj = $STH->fetch()) {
echo $obj->addr;
}
Если в базе данных адрес выглядит как ’5 Rosebud’, то на выходе получится ’5 Rxxxxxx’.
Конечно, иногда будет требоваться, чтобы конструктор вызывался ПЕРЕД присваиванием значений. PDO такое тоже позволяет.
$STH->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, "secret_person");
Теперь, когда вы дополнили предыдущий пример дополнительной опцией (PDO::FETCH_PROPS_LATE), адрес видоизменяться не будет, так как после записи значений ничего не происходит.
Наконец, при необходимости можно передавать конструктору аргументы прямо при создании объекта:
$STH->setFetchMode(PDO::FETCH_CLASS, "secret_person", array("stuff"));
Можно даже передавать разные аргументы каждому объекту:
$i = 0;
while($rowObj = $STH->fetch(PDO::FETCH_CLASS, "secret_person", array($i))) {
// что-то делаем
$i++;
}
Другие полезные методы
Хотя эта статья не может (и не пытается) охватить все аспекты работы с PDO (это огромный модуль!), оставить без упоминания следующие несколько функций нельзя.$DBH->lastInsertId();
Метод ->lastInsertId() возвращает id последней вставленной записи. Стоит заметить, что он всегда вызывается у объекта базы данных (в статье он именуется $DBH), а не объекта с выражением ($STH).
$DBH->exec("DELETE FROM folks WHERE 1"); $DBH->exec("SET time_zone = "-8:00"");
Метод ->exec() используется для операций, которые не возвращают никаких данных, кроме количества затронутых ими записей.
$safe = $DBH->quote($unsafe);
Метод ->quote() ставит кавычки в строковых данных таким образом, что их становится безопасно использовать в запросах. Пригодится, если вы не используете prepared statements.
$rows_affected = $STH->rowCount();
Метод ->rowCount() возвращает количество записей, которые поучаствовали в операции. К сожалению, эта функция отказывалась работать с SELECT-запросами вплоть до PHP 5.1.6. Если обновить версию PHP не представляется возможным, количество записей можно получить так:
$sql = "SELECT COUNT(*) FROM folks"; if ($STH = $DBH->query($sql)) { # проверяем количество записей if ($STH->fetchColumn() > 0) { # делаем здесь полноценную выборку, потому что данные найдены! } else { # выводим сообщение о том, что удовлетворяющих запросу данных не найдено } }
Заключение
Надеюсь, этот материал поможет кому-то из вас осуществить миграцию с расширений mysql и mysqli.Настройка и использование PDO - расширения PHP Data Objects для работы с базами данных
Создание тестовой базы данных и таблицы
Для начала создадим базу данных для этого руководства:
CREATE DATABASE solar_system; GRANT ALL PRIVILEGES ON solar_system.* TO "testuser"@"localhost" IDENTIFIED BY "testpassword";
Пользователю с логином testuser и паролем testpassword предоставили полные права доступа к базе solar_system .
Теперь создадим таблицу и заполним данными, астрономическая точность которых не подразумевается:
USE solar_system; CREATE TABLE planets (id TINYINT(1) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), name VARCHAR(10) NOT NULL, color VARCHAR(10) NOT NULL); INSERT INTO planets(name, color) VALUES("earth", "blue"), ("mars", "red"), ("jupiter", "strange");
Описание соединения
Теперь, когда создана база, определим DSN () - сведения для подключения к базе, представленные в виде строки. Синтаксис описания отличается в зависимости от используемой СУБД. В примере работаем с MySQL/MariaDB, поэтому указываем:
- имя хоста, где расположена СУБД;
- порт (необязательно, если используется стандартный порт 3306);
- имя базы данных;
- кодировку (необязательно).
Строка DSN в этом случае выглядит следующим образом:
$dsn = "mysql:host=localhost;port=3306;dbname=solar_system;charset=utf8";
Первым указывается database prefix . В примере - mysql . Префикс отделяется от остальной части строки двоеточием, а каждый следующий параметр - точкой с запятой.
Создание PDO-объекта
Теперь, когда строка DSN готова, создадим PDO-объект. Конструктор на входе принимает следующие параметры:
- Строку DSN.
- Имя пользователя, имеющего доступ к базе данных.
- Пароль этого пользователя.
- Массив с дополнительными параметрами (необязательно).
Дополнительные параметры можно также определить после создания объекта с помощью метода SetAttribute:
$pdo->SetAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Определение метода выборки по умолчанию
PDO::DEFAULT_FETCH_MODE - важный параметр, который определяет метод выборки по умолчанию. Указанный метод используется при получении результата выполнения запроса.
PDO::FETCH_BOTH
Режим по умолчанию. Результат выборки индексируется как номерами (начиная с 0), так и именами столбцов:
$stmt = $pdo->query("SELECT * FROM planets"); $results = $stmt->fetch(PDO::FETCH_BOTH);
После выполнения запроса с этим режимом к тестовой таблице планет получим следующий результат:
Array ( => 1 => 1 => earth => earth => blue => blue)
PDO::FETCH_ASSOC
Результат сохраняется в ассоциативном массиве, в котором ключ - имя столбца, а значение - соответствующее значение строки:
$stmt = $pdo->query("SELECT * FROM planets"); $results = $stmt->fetch(PDO::FETCH_ASSOC);
В результате получим:
Array ( => 1 => earth => blue)
PDO::FETCH_NUM
При использовании этого режима результат представляется в виде массива, индексированного номерами столбцов (начиная с 0):
Array ( => 1 => earth => blue)
PDO::FETCH_COLUMN
Этот вариант полезен, если нужно получить перечень значений одного поля в виде одномерного массива, нумерация которого начинается с 0. Например:
$stmt = $pdo->query("SELECT name FROM planets");
В результате получим:
Array ( => earth => mars => jupiter)
PDO::FETCH_KEY_PAIR
Используем этот вариант, если нужно получить перечень значений двух полей в виде ассоциативного массива. Ключи массива - это данные первого столбца выборки, значения массива - данные второго столбца. Например:
$stmt = $pdo->query("SELECT name, color FROM planets"); $result = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
В результате получим:
Array ( => blue => red => strange)
PDO::FETCH_OBJECT
При использовании PDO::FETCH_OBJECT для каждой извлеченной строки создаётся анонимный объект. Его общедоступные (public) свойства - имена столбцов выборки, а результаты запроса используются в качестве их значений:
$stmt = $pdo->query("SELECT name, color FROM planets"); $results = $stmt->fetch(PDO::FETCH_OBJ);
В результате получим:
StdClass Object ( => earth => blue)
PDO::FETCH_CLASS
В этом случае, как и в предыдущем, значения столбцов становятся свойствами объекта. Однако требуется указать существующий класс, который будет использоваться для создания объекта. Рассмотрим это на примере. Для начала создадим класс:
Class Planet { private $name; private $color; public function setName($planet_name) { $this->name = $planet_name; } public function setColor($planet_color) { $this->color = $planet_color; } public function getName() { return $this->name; } public function getColor() { return $this->color; } }
Обратите внимание, что у класса Planet закрытые (private) свойства и нет конструктора. Теперь выполним запрос.
Если используется метод fetch с PDO::FETCH_CLASS , перед отправкой запроса на получение данных нужно применить метод setFetchMode:
$stmt = $pdo->query("SELECT name, color FROM planets"); $stmt->setFetchMode(PDO::FETCH_CLASS, "Planet");
Первый параметр, который передаем методу setFetchMode , - константа PDO::FETCH_CLASS . Второй параметр - имя класса, который будет использоваться при создании объекта. Теперь выполним:
$planet = $stmt->fetch(); var_dump($planet);
В результате получим объект Planet:
Planet Object ( => earth => blue)
Значения, полученные в результате запроса, назначены соответствующим свойствам объекта, даже закрытым.
Определение свойств после выполнения конструктора
В классе Planet нет явного конструктора, поэтому проблем при назначении свойств не будет. При наличии у класса конструктора, в котором свойство было назначено или изменено, они будут перезаписаны.
При использовании константы FETCH_PROPS_LATE значения свойств будут присваиваться после выполнения конструктора:
Class Planet { private $name; private $color; public function __construct($name = moon, $color = grey) { $this->name = $name; $this->color = $color; } public function setName($planet_name) { $this->name = $planet_name; } public function setColor($planet_color) { $this->color = $planet_color; } public function getName() { return $this->name; } public function getColor() { return $this->color; } }
Мы изменили класс Planet , добавив конструктор, который принимает на входе два аргумента: name (имя) и color (цвет). Значения этих полей по умолчанию: moon (луна) и gray (серый) соответственно.
Если не использовать FETCH_PROPS_LATE , при создании объекта свойства будут перезаписаны значениями по умолчанию. Проверим это. Сначала выполним запрос:
$stmt = $pdo->query("SELECT name, color FROM solar_system WHERE name = "earth""); $stmt->setFetchMode(PDO::FETCH_CLASS, "Planet"); $planet = $stmt->fetch(); var_dump($planet);
В результате получим:
Object(Planet)#2 (2) { ["name":"Planet":private]=> string(4) "moon" ["color":"Planet":private]=> string(4) "gray" }
Как и ожидалось, извлеченные из базы данных значения перезаписаны. Теперь рассмотрим решение задачи с помощью FETCH_PROPS_LATE (запрос аналогичный):
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, "Planet"); $planet = $stmt->fetch(); var_dump($planet);
В результате получим то, что нужно:
Object(Planet)#4 (2) { ["name":"Planet":private]=> string(5) "earth" ["color":"Planet":private]=> string(4) "blue" }
Если у конструктора класса нет значений по умолчанию, а они нужны, параметры конструктора задаются при вызове метода setFetchMode третьим аргументом в виде массива. Например:
Class Planet { private $name; private $color; public function __construct($name, $color) { $this->name = $name; $this->color = $color; } [...] }
Аргументы конструктора обязательны, поэтому выполним:
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, "Planet", ["moon", "gray"]);
Входящие параметры выступают также в роли значений по умолчанию, которые нужны для инициализации. В дальнейшем они будут перезаписаны значениями из базы данных.
Получение нескольких объектов
Множественные результаты извлекаются в виде объектов с помощью метода fetch внутри цикла while:
While ($planet = $stmt->fetch()) { // обработка результатов }
Или путём выборки всех результатов сразу. Во втором случае используется метод fetchAll , причём режим указывается в момент вызова:
$stmt->fetchAll(PDO::FETCH_CLASS|PDO_FETCH_PROPS_LATE, "Planet", ["moon", "gray"]);
PDO::FETCH_INTO
При выборе этого варианта выборки PDO не создаёт новый объект, а обновляет свойства существующего. Однако это возможно только для общедоступных (public) свойств или при использовании в объекте «магического» метода __set .
Подготовленные и прямые запросы
В PDO два способа выполнения запросов:
- прямой, который состоит из одного шага;
- подготовленный, который состоит из двух шагов.
Прямые запросы
Существует два метода выполнения прямых запросов:
- query используется для операторов, которые не вносят изменения, например SELECT . Возвращает объект PDOStatemnt , из которого с помощью методов fetch или fetchAll извлекаются результаты запроса;
- exec используется для операторов вроде INSERT , DELETE или UPDATE . Возвращает число обработанных запросом строк.
Прямые операторы используются только в том случае, если в запросе отсутствуют переменные и есть уверенность, что запрос безопасен и правильно экранирован.
Подготовленные запросы
PDO поддерживает подготовленные запросы (prepared statements), которые полезны для защиты приложения от : метод prepare выполняет необходимые экранирования.
Рассмотрим пример. Требуется вставить свойства объекта Planet в таблицу Planets . Сначала подготовим запрос:
$stmt = $pdo->prepare("INSERT INTO planets(name, color) VALUES(?, ?)");
Используем метод prepare , который принимает как аргумент SQL-запрос с псевдопеременными (placeholders). Псевдопеременные могут быть двух типов: неименнованые и именованные.
Неименованные псевдопеременные
Неименованные псевдопеременные (positional placeholders) отмечаются символом? . Запрос в результате получается компактным, но требуется предоставить значения для подстановки, размещенные в том же порядке. Они передаются в виде массива через метод execute:
$stmt->execute([$planet->name, $planet->color]);
Именованные псевдопеременные
При использовании именованных псевдопеременных (named placeholders) порядок передачи значений для подстановки не важен, но код в этом случае становится не таким компактным. В метод execute данные передаются в виде ассоциативного массива, в котором каждый ключ соответствует имени псевдопеременной, а значение массива - значению, которое требуется подставить в запрос. Переделаем предыдущий пример:
$stmt = $pdo->prepare("INSERT INTO planets(name, color) VALUES(:name, :color)"); $stmt->execute(["name" => $planet->name, "color" => $planet->color]);
Методы prepare и execute используются как при выполнении запросов на изменение, так и при выборке.
А информацию о количестве обработанных строк при необходимости предоставит метод rowCount .
Управление поведением PDO при ошибках
Параметр выбора режима ошибок PDO::ATTR_ERRMODE используется для определения поведения PDO в случае ошибок. Доступно три варианта: PDO::ERRMODE_SILENT , PDO::ERRMODE_EXCEPTION и PDO::ERRMODE_WARNING .
PDO::ERRMODE_SILENT
Вариант по умолчанию. PDO просто запишет информацию об ошибке, которую помогут получить методы errorCode и errorInfo .
PDO::ERRMODE_EXCEPTION
Это предпочтительный вариант, при котором в дополнение к информации об ошибке PDO выбрасывает исключение (PDOException). Исключение прерывает выполнение скрипта, что полезно при использовании транзакций PDO. Пример приведён при описании транзакций.
PDO::ERRMODE_WARNING
В этом случае PDO также записывает информацию об ошибке. Поток выполнения скрипта не прерывается, но выдаются предупреждения.
Методы bindValue и bindParam
Для подстановки значений в запросе можно также использовать методы bindValue и bindParam . Первый связывает значение переменной с псевдопеременной, которая использована при подготовке запроса:
$stmt = $pdo->prepare("INSERT INTO planets(name, color) VALUES(:name, :color)"); $stmt->bindValue("name", $planet->name, PDO::PARAM_STR);
Связали значение переменной $planet->name с псевдопеременной:name . Обратите внимание, что при использовании методов bindValue и bindParam как третий аргумент указывается тип переменной, используя соответствующие константы PDO. В примере - PDO::PARAM_STR .
Метод bindParam привязывает переменную к псевдопеременной. В этом случае переменная связана с псевдопеременной ссылкой, а значение будет подставлено в запрос только после вызова метода execute . Рассмотрим на примере:
$stmt->bindParam("name", $planet->name, PDO::PARAM_STR);
Транзакции в PDO
Представим необычный пример. Пользователю требуется выбрать список планет, причём каждый раз при выполнении запроса текущие данные удаляются из базы, а потом вставляются новые. Если после удаления произойдёт ошибка, то следующий пользователь получит пустой список. Чтобы этого избежать, используем транзакции:
$pdo->beginTransaction(); try { $stmt1 = $pdo->exec("DELETE FROM planets"); $stmt2 = $pdo->prepare("INSERT INTO planets(name, color) VALUES (?, ?)"); foreach ($planets as $planet) { $stmt2->execute([$planet->getName(), $planet->getColor()]); } $pdo->commit(); } catch (PDOException $e) { $pdo->rollBack(); }
Метод beginTransaction отключает автоматическое выполнение запросов, а внутри конструкции try-catch запросы выполняются в нужном порядке. Если не возникнет исключений PDOException , запросы выполнятся с помощью метода commit . В противном случае откатятся с помощью метода rollback , а автоматическое выполнение запросов восстановится.
Таким образом появилась согласованность выполнения запросов. Очевидно, что для этого параметру PDO::ATTR_ERRMODE необходимо установить значение PDO::ERRMODE_EXCEPTION .
Заключение
Теперь, когда работа с PDO описана, отметим его основные преимущества:
- с PDO легко перенести приложение на другие СУБД;
- поддерживаются все популярные СУБД;
- встроенная система управления ошибками;
- разнообразные варианты представления результатов выборки;
- поддерживаются подготовленные запросы, которые сокращают код и делают его устойчивым к SQL-инъекциям;
- поддерживаются транзакции, которые помогают сохранить целостность данных и согласованность запросов при параллельной работе пользователей.
PDO (PHP Data Objects) - расширение PHP, которое реализует взаимодействие с базами данных при помощи объектов. Профит в том, что отсутствует привязка к конкретной системе управления базами данных. PDO поддерживает СУБД: MySQL, PostgreSQL, SQLite, Oracle, Microsoft SQL Server и другие.
Почему стоит использовать PDO
Функции mysql в PHP для работы с БД давно уже устарели, на сегодняшний день желательно использовать mysqli или PDO (PHP Data Objects). Кроме того, mysqli - эта библиотека, которая по большому счёту, не предназначена для использования напрямую в коде. Она может послужить хорошим строительным материалом для создания библиотеки более высокого уровня. При работе с mysqli следует также помнить об обеспечении безопасности вашего приложения, в частности о защите от SQL-инъекций. В случае использования PDO (с его подготовленными запросами), такая защита идёт уже "из коробки", главное правильно применить необходимые методы.
Тестовая база данных с таблицей
// Из консоли Windows mysql> CREATE DATABASE `pdo-test` CHARACTER SET utf8 COLLATE utf8_general_ci; USE pdo-test; CREATE TABLE categories (id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY(id), name VARCHAR(255) NOT NULL); INSERT INTO `categories` (`name`) VALUES ("Ноутбуки и планшеты"), ("Компьютеры и периферия"), ("Комплектующие для ПК"), ("Смартфоны и смарт-часы"), ("Телевизоры и медиа"), ("Игры и приставки"), ("Аудиотехника"), ("Фото-видеоаппаратура"), ("Офисная техника и мебель"), ("Сетевое оборудование"), ("Крупная бытовая техника"), ("Товары для кухни"), ("Красота и здоровье"), ("Товары для дома"), ("Инструменты"), ("Автотовары");Установка PDO
// Установка в Linux sudo apt update sudo apt install php7.2-mysql sudo apt-get install pdo-mysql // В php.ini добавить extension=pdo.so extension=pdo_mysql.so // На Windows, как правило драйвер уже установлен, нужно просто проверить включен ли он в php.ini extension=php_pdo_mysql.dllПроверить доступные драйвера
print_r(PDO::getAvailableDrivers());Соединение с базой данных
Соединения устанавливаются автоматически при создании объекта PDO от его базового класса.
// Пример #1. Простое подключение $db = new PDO("mysql:host=localhost;dbname=pdo", "root", "password");
При ошибке подключения PHP выдаст ошибку:
Fatal error: Uncaught PDOException: ... // Пример #2. Обработка ошибок подключения try { $dbh = new PDO("mysql:host=localhost;dbname=pdo", "root", "password"); } catch (PDOException $e) { print "Error!: " . $e->getMessage(); die(); }
В этом примере подключения мы используем конструкцию try...catch . Многие спорят о целесообразности её использования. Лично я использую try...catch , она мне не мешает.
Подготовленные и прямые запросы
В PDO два способа выполнения запросов:
- Прямой - состоит из одного шага;
- Подготовленный - состоит из двух шагов.
Прямые запросы
- query() используется для операторов, которые не вносят изменения, например SELECT . Возвращает объект PDOStatemnt , из которого с помощью методов fetch() или fetchAll извлекаются результаты запроса. Можно его сравнить с mysql resource , который возвращала mysql_query() .
- exec() используется для операторов INSERT, DELETE, UPDATE . Возвращает число обработанных запросом строк.
Прямые запросы используются только в том случае, если в запросе отсутствуют переменные и есть уверенность, что запрос безопасен и правильно экранирован.
$stmt = $db->query("SELECT * FROM categories"); while ($row = $stmt->fetch()) { echo "
"; print_r($row); }Подготовленные запросы
Если же в запрос передаётся хотя бы одна переменная, то этот запрос в обязательном порядке должен выполняться только через подготовленные выражения. Что это значит? Это обычный SQL запрос, в котором вместо переменной ставится специальный маркер - плейсхолдер. PDO поддерживает позиционные плейсхолдеры (?), для которых важен порядок передаваемых переменных, и именованные (:name), для которых порядок не важен. Примеры:
$sql = "SELECT name FROM categories WHERE id = ?"; $sql = "SELECT name FROM categories WHERE name = :name";
Чтобы выполнить такой запрос, сначала его надо подготовить с помощью метода prepare() . Она также возвращает PDO statement , но ещё без данных. Чтобы их получить, надо исполнить этот запрос, предварительно передав в него наши переменные. Передать можно двумя способами: Чаще всего можно просто выполнить метод execute() , передав ему массив с переменными:
$stmt = $pdo->prepare("SELECT `name` FROM categories WHERE `id` = ?"); $stmt->execute([$id]); $stmt = $pdo->prepare("SELECT `name` FROM categories WHERE `name` = :name"); $stmt->execute(["name" => $name]);
Как видно, в случае именованных плейсхолдеров в execute() должен передаваться массив, в котором ключи должны совпадать с именами плейсхолдеров. После этого можно извлечь результаты запроса:
$id = 1; $stmt = $db->prepare("SELECT * FROM categories WHERE `id` = ?"); $stmt->execute([$id]); $category = $stmt->fetch(PDO::FETCH_LAZY); echo "
"; print_r($category);ВАЖНО! Подготовленные запросы - основная причина использовать PDO, поскольку это единственный безопасный способ выполнения SQL запросов, в которых участвуют переменные.
Получение данных. Метод fetch()
Мы уже выше познакомились с методом fetch() , который служит для последовательного получения строк из БД. Этот метод является аналогом функции mysq_fetch_array() и ей подобных, но действует по-другому: вместо множества функций здесь используется одна, но ее поведение задается переданным параметром. В подробностях об этих параметрах будет написано в , а в качестве краткой рекомендации посоветую применять fetch() в режиме FETCH_LAZY
$id = 1; $stmt = $db->prepare("SELECT * FROM categories WHERE `id` = ?"); $stmt->execute([$id]); while ($row = $stmt->fetch(PDO::FETCH_LAZY)) { echo "Category name: ".$row->name; }
В этом режиме не тратится лишняя память, и к тому же к колонкам можно обращаться любым из трех способов - через индекс, имя, или свойство (через ->). Недостатком же данного режима является то, что он не работает с fetchAll()
Получение данных. Метод fetchColumn()
Также у PDO statement есть метод для получения значения единственной колонки. Очень удобно, если мы запрашиваем только одно поле - в этом случае значительно сокращается количество кода:
$id = 1; $stmt = $db->prepare("SELECT `name` FROM categories WHERE `id` = ?"); $stmt->execute([$id]); $name = $stmt->fetchColumn(); echo "Category name: ".$name;
Получение данных. Метод fetchAll()
$data = $db->query("SELECT * FROM categories")->fetchAll(PDO::FETCH_ASSOC); foreach ($data as $k => $v){ echo "Category name: ".$v["name"]."
"; }PDO и оператор LIKE
Работая с подготовленными запросами, следует понимать, что плейсхолдер может заменять только строку или число. Ни ключевое слово, ни идентификатор, ни часть строки или набор строк через плейсхолдер подставить нельзя . Поэтому для LIKE необходимо сначала подготовить строку поиска целиком, а потом ее подставлять в запрос:
$search = "комп"; $query = "SELECT * FROM categories WHERE `name` LIKE ?"; $params = ["%$search%"]; $stmt = $db->prepare($query); $stmt->execute($params); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $i = 1; foreach ($data as $category){ echo $i++ . ". " . $category["name"]."
"; }Здесь может вознинуть проблема! Поиск может не работать, потому как из базы у вас приходят данные в неправильной кодировке. Необходимо добавить кодировку в подключение, если она там не указана!
$db = new PDO("mysql:host=localhost;dbname=pdo;charset=utf8", "root", "");
PDO и оператор LIMIT
Важно! Когда PDO работает в режиме эмуляции, все данные, которые были переданы напрямую в execute() , форматируются как строки. То есть, эскейпятся и обрамляются кавычками. Поэтому LIMIT ?,? превращается в LIMIT "10", "10" и очевидным образом вызывает ошибку синтаксиса и, соответственно, пустой массив данных.
Решение #1: Отключить режим эмуляции:
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Решение #2: Биндить эти цифры через bindValue() , принудительно выставляя им тип PDO::PARAM_INT:
$limit = 3; $stm = $db->prepare("SELECT * FROM categories LIMIT ?"); $stm->bindValue(1, $limit, PDO::PARAM_INT); $stm->execute(); $data = $stm->fetchAll(); echo "
"; print_r($data);PDO и оператор IN
При выборке из таблицы необходимо доставать записи, соответствующие всем значениям массива.
$arr = ; $in = str_repeat("?,", count($arr) - 1) . "?"; $sql = "SELECT * FROM categories WHERE `id` IN ($in)"; $stm = $db->prepare($sql); $stm->execute($arr); $data = $stm->fetchAll(); echo "
"; print_r($data);Добавление записей
$name = "Новая категория"; $query = "INSERT INTO `categories` (`name`) VALUES (:name)"; $params = [ ":name" => $name ]; $stmt = $pdo->prepare($query); $stmt->execute($params);Изменение записей
$id = 1; $name = "Изменённая запись"; $query = "UPDATE `categories` SET `name` = :name WHERE `id` = :id"; $params = [ ":id" => $id, ":name" => $name ]; $stmt = $pdo->prepare($query); $stmt->execute($params);Удаление записей
$id = 1; $query = "DELETE FROM `categories` WHERE `id` = ?"; $params = [$id]; $stmt = $pdo->prepare($query); $stmt->execute($params);Использование транзакций
try { // Начало транзакции $pdo->beginTransaction(); // ... code // Если в результате выполнения нашего кода всё прошло успешно, // то зафиксируем этот результат $pdo->commit(); } catch (Exception $e) { // Иначе, откатим транзакцию. $pdo->rollBack(); echo "Ошибка: " . $e->getMessage(); }Важно! Транзакции в PDO работают только с таблицами InnoDB
В данной заметке мы познакомились с основными понятиями PDO, его установкой, подключением к БД и самые простые возможности выборки, изменения и удаления данных. В следующих заметках мы рассмотрим ещё несколько тем, касающихся PDO.