Статьи

Избегайте оригинального расширения MySQL, часть 2

В первой части этой серии мы рассмотрели, что было не так с исходным MySQL API, и как мы можем перейти на более новый, многофункциональный API MySQLi. Во второй части мы рассмотрим расширение PDO, чтобы раскрыть некоторые функции, которые оно может предложить.

Альтернатива № 2: PDO — объекты данных PHP

Расширение PDO поддерживает двенадцать драйверов, что позволяет ему подключаться к различным реляционным базам данных, при этом разработчику не нужно изучать множество различных API. Это достигается путем абстрагирования вызовов взаимодействия с базой данных за общим интерфейсом, что позволяет разработчику использовать согласованный интерфейс для различных баз данных. Это, конечно, дает ему значительное преимущество перед расширениями MySQL и MySQLi, которые ограничены только одной базой данных.

Основы PDO

Подключение к базе данных через PDO — это быстрый и простой процесс, в котором необходимые детали передаются в качестве аргументов конструктору PDO. Затем мы получаем доступ к открытым методам и свойствам объекта для отправки запросов и получения данных.

<?php $dsn = 'mysql:host=localhost;dbname=database_name'; $username = 'user'; $password = 'password'; $db = new PDO($dsn, $username, $password); $numRows = $db->exec("INSERT INTO table VALUES ('val1', 'val2', 'val3')"); $result = $db->query('SELECT col1, col2, col3 FROM table', PDO::FETCH_ASSOC); foreach ($result as $row) { echo "{$row['col1']} - {$row['col2']} - {$row['col3']}n"; } $result->closeCursor(); unset($db); 

Сначала устанавливается соединение с базой данных, где DSN, имя пользователя и пароль передаются в качестве аргументов классу PDO . DSN — это строка подключения, которая будет изменяться от базы данных к базе данных, но обычно содержит имя драйвера базы данных, хост, имя базы данных и иногда номер порта.

Затем в этом примере выполняются два основных запроса: первый — методом exec() а второй — методом query() . Важное различие между этими двумя методами — успешное возвращаемое значение; exec() должна использоваться для операторов без выбора (INSERT, UPDATE или DELETE) и возвращает количество затронутых строк, а query() должна использоваться для операторов выбора и возвращает объект набора результатов при успехе. Они оба возвращают false, если произошел сбой.

Мы можем манипулировать способом возврата данных, передавая второй аргумент в query() . По умолчанию используется PDO::FETCH_BOTH которая возвращает дублированные данные в одном массиве для каждой строки; один будет ассоциативными данными, где имя столбца является ключом, а значение столбца — значением ( PDO::FETCH_ASSOC ), а другой — массивом с целочисленным индексом ( PDO::FETCH_NUM ). Поскольку это обычно не требуется, рекомендуется указать соответствующий режим выборки для экономии ресурсов.

Затем мы вызываем метод closeCursor() для очистки используемых ресурсов, хранящихся внутри объекта $result когда они больше не нужны. Более того, мы отключаем наш объект PDO, чтобы высвободить любые ресурсы, используемые, когда мы знаем, что сценарию больше не нужно будет взаимодействовать с базой данных.

Особенности ПДО

Как и в случае с MySQLi API, в расширении PDO появился ряд функций, которыми могут воспользоваться разработчики. Теперь, когда мы рассмотрели абсолютные основы использования PDO, мы можем перейти к рассмотрению некоторых функций, включая подготовленные операторы, транзакции, изменение поведения PDO по умолчанию и последствия абстракции.

Подготовленные заявления

Как и в MySQLi, PDO также поддерживает подготовленные операторы, в результате чего мы привязываем параметры к нашему запросу, чтобы предотвратить внедрение атак на базу данных. Подготовленные операторы также включают кэширование на стороне клиента и на сервере, что ускоряет время выполнения, когда один и тот же подготовленный запрос требует разных значений, связанных с ним и выполняемых. Однако PDO имеет несколько преимуществ по сравнению с параметризованными запросами MySQLi, которые стоит изучить.

Первое преимущество заключается в том, что PDO поддерживает использование именованных параметров, что дает нам возможность идентифицировать заполнители внутри наших запросов, давая им значимые имена. Это помогает нам отслеживать параметры, которые должны быть связаны с запросом, когда задействовано большое число, в отличие от наличия последовательности неназванных (или позиционных) заполнителей, использующих вопросительные знаки, которые зависят от порядка связывания.

Второе преимущество заключается в том, что мы имеем дополнительную гибкость привязки значений к нашим подготовленным запросам с помощью bindValue() . Это позволяет нам обойти ограничение возможности связывать только переменные, а затем оценивать их при вызове метода execute() , как и в bind_param() MySQLi.

Давайте подробнее рассмотрим, как мы можем явно связать переменные и значения с нашими подготовленными запросами на примере.

 <?php $insQuery = $db->prepare('INSERT INTO table VALUES (:col1, :col2, :col3)'); $insQuery->bindParam('col1', $val1, PDO::PARAM_INT); $insQuery->bindParam('col2', $val2, PDO::PARAM_STR); $insQuery->bindParam('col3', $val3, PDO::PARAM_INT); $insQuery->execute(); $selQuery = $db->prepare('SELECT col2, col3 FROM table WHERE col1 LIKE :val'); $selQuery->bindValue('val', "%{$val}%", PDO::PARAM_STR); $result = $selQuery->execute(); while ($row = $result->fetch(PDO::FETCH_ASSOC)) { echo "{$row['col2']} - {$row['col3']}n"; } 

Именованные заполнители начинаются с двоеточия, а затем используют те же соглашения об именах, что и переменные в PHP. При связывании наших параметров с подготовленным запросом мы должны предоставить bindParam() и bindValue() как минимум два аргумента, а также необязательный третий аргумент.

Первый аргумент — это имя заполнителя (который чувствителен к регистру), второй аргумент — это переменная или значение, которое мы хотим связать с запросом, а необязательный третий аргумент — это тип для привязки переменной / значения как ( по умолчанию используется PDO::PARAM_STR , однако я всегда указываю тип для ясности). Метод bindParam() также позволяет нам указать необязательный четвертый и пятый параметры, длину типа данных и любые дополнительные параметры драйвера соответственно.

bindParam() и bindValue() являются ортогональными, поэтому при привязке значений к запросу можно использовать любой из них или оба. Однако это не относится к именованным и неназванным заполнителям, где только один или другой могут использоваться в одном подготовленном запросе.

При неявном связывании мы отказываемся от использования bindParam() и bindValue() и просто передаем параметры для привязки в формате массива методу execute() . Если используются именованные заполнители, то для execute() необходимо будет передать ассоциативный массив; если используются позиционные заполнители, то индексированный массив может быть передан execute() .

Вот как мы можем использовать безымянные заполнители в сокращенном формате параметризованных запросов:

 <?php $updQuery = $db->prepare('UPDATE table SET col1 = ?, col2 = ? WHERE col3 = ?'); $updQuery->execute(array('val1', 'val2', 'val3')); if ($updQuery->rowCount() !== 0) { echo 'Success'; } 

Мы начинаем с подготовки нашего запроса и размещения неназванных заполнителей в позиции, а затем вызываем метод execute() с массивом, содержащим значения, которые должны быть связаны с нашим подготовленным запросом. Этот передаваемый массив может содержать (или обе) переменные и строки. Недостатком этого сокращенного метода является то, что мы не можем указать тип параметров, связанных с нашим подготовленным запросом. Далее мы rowCount() вопросом, обновлялись ли какие-либо строки с помощью возвращаемого значения из rowCount() , который будет содержать количество строк, затронутых предыдущей операцией. Если количество строк не равно нулю, то мы считаем это успешным.

Последний тип привязки, который мы рассмотрим с подготовленными запросами, предназначен не для целей безопасности, а для извлечения данных. Здесь мы привязываем имена столбцов к переменным с помощью bindColumn() . bindColumn() и bindParam() / bindValue() могут быть использованы в одном подготовленном запросе, что дает нам гибкость в извлечении данных путем непосредственного присвоения результатов переменным, невосприимчивым к атакам внедрения.

 <?php $preQuery = $db->prepare('SELECT col2, col3 FROM table WHERE col1 = :val'); $preQuery->bindParam('val', $value, PDO::PARAM_STR); $preQuery->execute(); $preQuery->bindColumn('col2', $OUTcol2); $preQuery->bindColumn('col3', $OUTcol3); while ($result = $preQuery->fetch(PDO::FETCH_BOUND)) { echo "{$OUTcol2} - {$OUTcol3}n"; } 

Сначала мы подготавливаем и привязываем значение к нашему запросу. Затем это выполняется, и мы вызываем метод bindColumn() ; первый параметр — это имя столбца (которое можно указать численно), а второй параметр — это переменная, к которой привязывается значение столбца. Для этого я использовал свое собственное соглашение об именах, чтобы помочь отличить создаваемые мной переменные (с известными, безопасными значениями) от переменных, содержащих значения из базы данных. Это помогает мне узнать, какие из них могут содержать испорченные данные, и поэтому необходимо будет экранировать их при выводе, чтобы предотвратить атаки XSS.

Затем мы перебираем извлеченные данные PDO::FETCH_BOUND , где метод извлечения ( PDO::FETCH_BOUND ) присваивает значения столбцов в нашем наборе результатов переменным, с которыми они были связаны выше ( $OUTcol2 и $OUTcol3 ) , В то время как значение $result остается истинным (есть строки, которые еще нужно пройти через цикл), цикл будет продолжать выполняться.

MySQLi API также предоставляет те же функциональные возможности (с синтаксисом, аналогичным приведенному выше), используя метод bind_result() .

операции

PDO также поддерживает транзакции, однако они создаются несколько иначе, чем в MySQLi API. Как указано в предыдущей статье, транзакции работают со свойствами ACID и используются для поддержания целостности данных (A C ID) в нескольких таблицах в реляционной базе данных. Если во время выполнения произойдет сбой одного из операторов, мы сможем откатить все эффекты операторов до окончательного принятия изменений (ACI D ); это связано с изолированным характером каждой транзакции (AC I D), прежде чем она считается успешной. Это особенно важно, когда заявления зависят от успеха друг друга, и поэтому они должны либо все успешно, либо все провалиться (CID).

Мы можем еще раз увидеть транзакции в форме, выполнив повторную вставку в таблицу с уникальным ключом ограничения, установленным для одного из столбцов:

 <?php $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); try { $db->beginTransaction(); $db->exec("INSERT INTO table VALUES (NULL, 'col1', 'col2')"); $db->exec("INSERT INTO table VALUES (NULL, 'col1', 'col2')"); $db->commit(); } catch(PDOException $e) { $db->rollBack(); } 

Сначала мы должны установить поведение обработки ошибок, чтобы генерировать исключения, когда они вызываются. Это потому, что настройки обработки ошибок PDO по умолчанию не вызывают исключений, что означает, что наш блок catch никогда не будет выполнен.

Чтобы начать транзакцию, мы вызываем метод beginTransaction() , а затем beginTransaction() выполнить два деструктивных запроса (где данные внутри нашей базы данных будут постоянно изменены с помощью операторов INSERT / UPDATE / DELETE). Затем мы вызываем метод commit() чтобы попытаться зафиксировать транзакцию, а затем вернуть обработку запроса обратно к автоматической фиксации. Вышеприведенное, конечно, нарушит правила целостности данных, установленные для таблицы, что приведет к выполнению блока catch и откат транзакции. Это означает, что новые данные не будут вставлены в базу данных.

Управление поведением по умолчанию

Изменить поведение PDO по умолчанию можно с помощью метода конструктора во время создания экземпляра класса или с помощью ранее существующего объекта, созданного после создания класса. Используя метод конструктора PDO, мы можем изменить любое количество настроек, передав их в формате массива в качестве четвертого (необязательного) параметра. Если вы хотите манипулировать поведением ранее существующего объекта, мы можем использовать метод setAttribute() .

 <?php $dsn = 'mysql:host=localhost;dbname=database_name'; $username = 'user'; $password = 'password'; $options = array(PDO::ATTR_PERSISTENT => TRUE, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC); $pdo = new PDO($dsn, $username, $password, $options); 

Вышеуказанное позволит включить постоянные соединения и изменить режим выборки по умолчанию с FETCH_BOTH на FETCH_ASSOC . Тем не менее, параметр постоянства соединения относится к меньшинству настроек, которые необходимо изменить перед созданием объекта, чтобы его влияние вступило в силу. С другой стороны, поведение выборки по умолчанию можно изменять для уже существующего объекта, что позволяет изменять его настройки в любое время во время выполнения сценария.

Изменяемые параметры объекта создаются с помощью setAttribute() , где поведение объекта по умолчанию обновляется до конца его использования. Мы также можем временно изменить некоторые настройки во время вызовов методов для некоторых операций; Одним из таких примеров является метод prepare() , который позволяет нам указывать дополнительные параметры драйвера при подготовке запроса (в качестве необязательного второго аргумента). Все это вместе дает нам дополнительную гибкость в свободной настройке поведения нашего объекта PDO на протяжении всего сценария.

Изменение параметров обработки ошибок — обычное явление для того, чтобы наш объект PDO реагировал на ошибки во время выполнения скрипта. Это можно сделать с помощью метода constructor или setAttribute() и работает в трех режимах: SILENT (по умолчанию), WARNING и EXCEPTION . Хотя мы всегда можем просматривать информацию об ошибках с помощью errorCode() и errorInfo() , настройки отчетов об ошибках позволяют нам выбирать, являются ли обнаруженные ошибки либо: полностью отключенным ( PDO::ERRMODE_SILENT ), и вызывается, если это предупреждение ( PDO::ERRMODE_WARNING ) или всегда выбрасывается ( PDO::ERRMODE_EXCEPTION ).

Последствия абстракции

Однако у уровня абстракции, который обеспечивает PDO, есть несколько недостатков. Одна из них — это проблемы совместимости между каждой из баз данных, когда PDO должен пытаться использовать только те функции, которые доступны для всех баз данных. Один из примеров этого можно увидеть в драйвере PDO_MYSQL, где для запросов по умолчанию задано небуферизованное из-за того, что оно не поддерживается другими драйверами. Это означает, что для оптимального использования этого уровня абстракции могут потребоваться изменения отдельных настроек при переключении с базы данных на базу данных.

Мы также должны внимательно следить за тем, какие методы мы выбираем для реализации при использовании PDO. Хотя большая часть функциональных возможностей, предлагаемых PDO с помощью своих методов, была нормализована для работы со всеми поддерживаемыми базами данных, все еще существуют методы, которые не полностью перенесены на все базы данных, поскольку они просто не поддерживаются. Одним из них является метод quote() , который не работает для драйвера PDO_ODBC. Для достижения переносимости всегда следует использовать общие методы.

Еще одно предостережение: следить за написанием несовместимого кода SQL. Это связано с тем, что PDO, хотя и может работать с несколькими базами данных, не является уровнем абстракции запросов. Один классический пример написания SQL-кода, зависящего от базы данных, — это использование символа обратной галочки, при котором он может поддерживаться вашей базой данных MySQL, но не другими базами данных, с которыми PDO может взаимодействовать. Другие базы данных будут иметь свои собственные определения для экранирования недопустимых имен таблиц и столбцов, такие как PostgreSQL и Oracle, которые используют двойные кавычки, или Microsoft SQL Server, который использует квадратные скобки.

Заключительные комментарии

В этой статье мы рассмотрели основы PDO и манипулировали его поведением по умолчанию, а также изучили подготовленные операторы для очистки входных данных (с явным и неявным связыванием), демонстрируя при этом другое их использование. Мы также рассмотрели создание транзакций и их соответствующее поведение (описано через свойства ACID). Однако в PDO есть еще много дополнительных функций, которые мы не имели возможности обсудить в этой статье, поэтому обязательно ознакомьтесь с руководством по PHP.net для получения дополнительной информации о PDO .

На этом мы завершаем серию из двух частей: почему мы должны избегать MySQL API и как мы можем избежать этого, введя две альтернативы. Я надеюсь, что теперь мы можем лучше избавиться от исходного MySQL API в пользу MySQLi или более заметного расширения PDO!

Изображение через Fotolia