PHP Data Objects (PDO)

Расширение Объекты данных PHP (PDO) определяет простой и согласованный интерфейс для доступа к базам данных в PHP. Каждый драйвер базы данных, в котором реализован этот интерфейс, может представить специфичный для базы данных функционал в виде стандартных функций расширения. Но надо заметить, что само по себе расширение PDO не позволяет манипулировать доступом к базе данных. Чтобы воспользоваться возможностями PDO, необходимо использовать соответствующий конкретной базе данных PDO драйвер.

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

Расширение PDO внедрено в PHP 5.1, но также доступно в 5.0 в виде PECL расширения; PDO использует новый OO функционал из ядра PHP 5, соответственно оно не будет работать с ранними версиями PHP.

Подключения и Управление подключениями

Соединения устанавливаются автоматически при создании объекта PDO от его базового класса. Не имеет значения, какой драйвер вы хотите использовать; все что требуется, это имя базового класса. Конструктор класса принимает аргументы для задания источника данных (DSN Data Source Name), а также необязательные имя пользователя и пароль (если есть).

<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
?>

В случае ошибки при подключении будет выброшено исключение PDOException. Его можно перехватить и обработать, либо оставить на откуп глобальному обработчику ошибок, который вы задали функцией set_exception_handler().

<?php
try {
    $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
    foreach($dbh->query('SELECT * from FOO') as $row) {
        print_r($row);
    }
    $dbh = null;
} catch (PDOException $e) {
    print "Error!: " . $e->getMessage() . "<br/>";
    die();
}
?>

При успешном подключении к базе данных в скрипт будет возвращен созданный PDO объект. Соединение остается активным на протяжении всего времени жизни объекта. Чтобы закрыть соединение, необходимо уничтожить объект путем удаления всех ссылок на него (этого можно добиться, присваивая NULL всем переменным, указывающим на объект). Если не сделать этого явно, PHP автоматически закроет соединение по окончании работы скрипта.

<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
// здесь мы каким-то образом используем соединение


// соединение больше не нужно, закрываем
$dbh = null;
?>

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

// Постоянное соединение
<?php
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, array(
    PDO::ATTR_PERSISTENT => true
));
?>

Замечание: Чтобы использовать постоянные соединения, необходимо добавить константу PDO::ATTR_PERSISTENT в массив параметров драйвера, который передается конструктору PDO. Если просто задать этот атрибут функцией PDO::setAttribute() уже после создания объекта, драйвер не будет использовать постоянные соединения.

Запросы

Чтобы получить результирующий набор (объект PDOStatement) от базы данных следует выполнить PDO::query() метод. А для экранирования значений - PDO::quote() (не все базы данных поддерживают метод quote()).

// Filter input from $_GET
$author = "";
if (ctype_alpha($_GET['author'])) {
    $author = $_GET['author'];
}

// Escape the value of $author with quote()
$sql = 'SELECT author.*, book.* FROM author
        LEFT JOIN book ON author.id = book.author_id
        WHERE author.last_name = ' . $dbh->quote($author);

// Execute the statement and echo the results
$results = $dbh->query($sql);
foreach ($results as $row) {
    echo "{$row['title']}, {$row['last_name']}\n";
}

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

$results = $dbh->query($sql);
$results->setFetchMode(PDO::FETCH_OBJ);
foreach ($results as $row) {
  echo "{$row->title}, {$row->last_name}\n";
}

Чтобы выполнять запросы INSERT, UPDATE или DELETE следует использовать PDO::exec() метод, который возвращает кол-во затронутых запросом строк.

$sql = "
INSERT INTO book (isbn, title, author_id, publisher_id)
  VALUES ('0395974682', 'The Lord of the Rings', 1, 3)";

$affected = $dbh->exec($sql);
echo "Records affected: {$affected}";

Транзакции и автоматическая фиксация изменений

Теперь вы знаете, как подключаться к базам данных посредством PDO. Но перед тем как выполнять запросы, вам необходимо понять, как PDO управляет транзакциями. Если вы прежде не сталкивались с транзакциями, они обладают четырьмя главными свойствами, это Атомарность, Согласованность, Изолированность и Долговечность (ACID). Говоря простым языком, любые действия, совершенные в рамках транзакции, гарантированно будут выполнены безопасно для базы данных, и на них не повлияют другие подключения к этой базе, даже если эти действия совершаются в несколько этапов. Транзакционные операции можно отменять по запросу (если транзакция еще не зафиксирована), что упрощает обработку ошибок в скрипте.

Механизм транзакций реализован путем "временного сохранения" всех изменений и дальнейшего применения этих изменений, как единого целого. Это позволяет добиться резкого увеличения эффективности подобных изменений. Другими словами, транзакции могут сделать ваш скрипт более быстрым и потенциально более стабильным (но для этого необходимо корректно использовать этот механизм).

К сожалению, не все базы данных поддерживают транзакции, поэтому PDO при создании подключения работает в режиме так называемой "автоматической фиксации". Режим автофиксации означает, что каждый запрос к базе данных, который вы выполняете, неявно заключается в транзакцию, если СУБД их поддерживает. Если база данных не поддерживает этот механизм, запрос обрабатывается без транзакции. Чтобы явно обозначить начало транзакции, вы должны использовать метод PDO::beginTransaction(). Однако, в этом случае, если драйвер не поддерживает механизм транзакций, будет выброшено исключение PDOException (вне зависимости от выбранного способа обработки ошибок: подобные ситуации - это всегда серьезная недоработка). Будучи внутри границ транзакции, ее можно зафиксировать методом PDO::commit() или откатить методом PDO::rollBack(), в зависимости от того, успешно выполнен ваш код внутри транзакции или нет.

Внимание: PDO проверяет возможность использования транзакций только на уровне драйвера. Если по каким-то причинам механизм транзакций недоступен, но сервер баз данных принял запрос на открытие транзакции, PDO::beginTransaction() вернет TRUE без ошибок. В качестве примера можно попробовать использовать транзакции для изменения MyISAM таблиц базы данных MySQL.

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

Внимание: Изменения будут откачены автоматически, только если транзакция открыта методом PDO::beginTransaction(). Если транзакцию открыть вручную в тексте запроса, PDO об этом никак не узнает, и, соответственно, не сможет принять мер, если произойдет что-то плохое.

Выполнение пакета изменений в рамках транзакции

В следующем примере предположим, что мы создаем несколько записей для нового сотрудника с номером ID 23. Помимо ввода основной информации необходимо записать его зарплату. Довольно просто сделать два отдельных обновления таблиц, однако путем заключения этих запросов в рамки PDO::beginTransaction() и PDO::commit() мы сможем гарантировать, что никто не увидит этих изменений, пока все они не завершатся. Если что-то пойдет не так, catch-блок откатит все изменения с начала транзакции и напечатает сообщение об ошибке.

<?php
try {
  $dbh = new PDO('odbc:SAMPLE', 'db2inst1', 'ibmdb2', 
      array(PDO::ATTR_PERSISTENT => true));
  echo "Подключились\n";
} catch (Exception $e) {
  die("Не удалось подключиться: " . $e->getMessage());
}

try {  
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

  $dbh->beginTransaction();
  $dbh->exec("insert into staff (id, first, last) values (23, 'Joe', 'Bloggs')");
  $dbh->exec("insert into salarychange (id, amount, changedate) 
      values (23, 50000, NOW())");
  $dbh->commit();

} catch (Exception $e) {
  $dbh->rollBack();
  echo "Ошибка: " . $e->getMessage();
}
?>

Вы никак не ограничены в количестве запросов в рамках транзакции; вы также можете выполнять сложные запросы, чтобы извлечь данные, а затем использовать их для создания других запросов на обновление и извлечение данных; если транзакция активна, вы можете быть уверены, что никто не сможет изменить ваши данные, пока вы с ними работаете.