Описание констант PDO::FETCH_*.

  1. Классика
  2. PDO::FETCH_LAZY
  3. Самое полезное
  4. Объектное ориентирование
  5. Разное
  6. Untested
Comments (1)

Как уже говорилось в статье Как работать с PDO?, константы, задающие режим получения данных (наряду с передачей параметров напрямую в execute()) делают PDO по-настоящему удобной библиотекой для работы с БД, а не просто еще одним (пусть и универсальным) API для доступа к базе данных. С помощью этих констант можно значительно сократить код многих рутинных операций, поскольку они позволяют получить данные сразу в нужном формате.

Хотя интересующие нас константы частично описываются на страницах документации, посвященных методам fetch() и fetchAll(), полный список приведен только на странице с общим списком констант PDO, что не кажется мне очень удобным, и может являться той причиной, по которой некоторые особенно интересные режимы получения данных оказались вне поля зрения большинства разработчиков. Я решил выделить интересующие нас константы из общего списка и разбить для удобства на несколько категорий, поскольку их количество впечатляет - ни много ни мало, а целых 25 штук!

Классика

Для начала перечислим константы, которые дублируют стандартное поведение теплых ламповых функций mysql. Все примеры даются с использованием функции var_export().

PDO::FETCH_BOTH

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


<?$user = $pdo->query("SELECT * from users LIMIT 1")->fetch(PDO::FETCH_BOTH);
/*
array (
  'id' => '104',
  0 => '104',
  'name' => 'John',
  1 => 'John',
  'sex' => 'male',
  2 => 'male',
  'car' => 'Toyota',
  3 => 'Toyota',
)*/?>

PDO::FETCH_NUM

снова старый знакомый, аналог mysql_fetch_row(). Только цифровые индексы:


<?$user = $pdo->query("SELECT * from users LIMIT 1")->fetch(PDO::FETCH_NUM);
/*
array (
  0 => '104',
  1 => 'John',
  2 => 'male',
  3 => 'Toyota',
)*/?>

PDO::FETCH_ASSOC

то же самое, аналог mysql_fetch_assoc(), только текстовые индексы.


<?$user = $pdo->query("SELECT * from users LIMIT 1")->fetch(PDO::FETCH_ASSOC);

/*
array (
  'id' => '104',
  'name' => 'John',
  'sex' => 'male',
  'car' => 'Toyota',
)*/?>

См. также PDO::FETCH_NAMED

PDO::FETCH_OBJ

аналог mysql_fetch_object() без указания имени класса, возвращает экземпляр stdClass


<?$user = $pdo->query("SELECT * from users LIMIT 1")->fetch(PDO::FETCH_OBJ);
/*
stdClass::__set_state(array(
   'id' => '104',
   'name' => 'John',
   'sex' => 'male',
   'car' => 'Toyota',
))*/?>

PDO::FETCH_LAZY

Эта константа настолько примечательна и ни на что не похожа, что я решил вынести её в отдельный раздел. Начнем с того, что возвращаемая с её помощью переменная является экземпляром особенного класса PDORow, причем этот объект наделён целым ворохом уникальных свойств:

Проведём пару экспериментов. Попробуем запросить относительно большой объём данных, и посмотрим, как меняется потребление памяти, и заодно проверим ещё пару утверждений:


<?$stmt = $pdo->query("SELECT *, REPEAT(' ', 1024 * 1024) big FROM users");
echo 'start       ', round(memory_get_usage() / 1024), PHP_EOL;
$lazy = $stmt->fetch(PDO::FETCH_LAZY);
echo 'lazy fetch  ', round(memory_get_usage() / 1024), PHP_EOL;
$big  = $lazy[3];
echo 'lazy assign ', round(memory_get_usage() / 1024), PHP_EOL;
echo 'lazy name   ', $lazy[0], PHP_EOL;
echo 'lazy undef  ', var_dump($lazy['undef']);

echo '------------', PHP_EOL;

$num = $stmt->fetch(PDO::FETCH_NUM);
echo 'num fetch   ', round(memory_get_usage() / 1024), PHP_EOL;
$big2 = $num[3];
echo 'num assign  ', round(memory_get_usage() / 1024), PHP_EOL;
$big2 .= ''; // чтобы вызвать copy-on-write
echo 'num assign2 ', round(memory_get_usage() / 1024), PHP_EOL;
echo 'lazy name   ', $lazy[0], PHP_EOL;
echo 'num undef  ', var_dump($num['undef']);?>

Вывод:


<?start       228
lazy fetch  228
lazy assign 1252
lazy name   John
lazy undef  NULL
------------
num fetch   2277
num assign  2277
num assign2 3301
lazy name   Mike
num undef  
Notice:  Undefined index: undef in pdo.php on line 48
NULL?>

Как видно, этот код опрашивает нашу таблицу users, добавляя к строке мегабайтное поле. После этого мы получаем строку через эту константу - объём памяти не поменялся. Только после того, как мы присваиваем значение мегабайтного поля переменной - он увеличивается. Для контроля выводим имя из таблицы users. Затем получаем ещё одну строку, уже в одном из стандартных режимов. Видим, что объем памяти тут же подскакивает, в отличие от предыдущего случая. Экономия в первом случае налицо! Так же мы видим отсутствие нотиса и смену состояния $lazy после вызова fetch().

Из всей этой информации можно сделать вывод, что объект PDORow - это канал прямой связи с астралом resultSet-ом драйвера БД. И, не имея собственного состояния, попросту читает данные с текущей позиции курсора. Учитывая всё вышесказанное можно только удивляться, почему эта константа до сих пор так редко используется.

Самое полезное

Здесь я решил собрать самые полезные, на мой взгляд, режимы, которые возвращают данные в наиболее востребованных в повседневной разработке форматах.

Примечание: здесь и далее все примеры даются при включенном по умолчанию формате вывода PDO::FETCH_ASSOC

PDO::FETCH_COLUMN

Вытаскивает только одну колонку из результата. Соответственно, имеет смысл только при использовании с fetchAll() - и в этом случае возвращает сразу одномерный массив. Очень удобно.


<?$data = $pdo->query('SELECT name FROM users')->fetchAll(PDO::FETCH_COLUMN);

/* array (
  0 => 'John',
  1 => 'Mike',
  2 => 'Mary',
  3 => 'Kathy',
) */?>

Дополнительным параметром можно указать номер выбираемой колонки. Если этот флаг используется сам по себе, то номер указывать нет смысла (надо сразу в запросе выбирать только одну колонку, которая и будет выведена), но если в комбинации с другими, то номер может оказаться полезным.

PDO::FETCH_KEY_PAIR

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


<?$data = $pdo->query('SELECT name, car FROM users')->fetchAll(PDO::FETCH_KEY_PAIR);

/* array (
  'John' => 'Toyota',
  'Mike' => 'Ford',
  'Mary' => 'Mazda',
  'Kathy' => 'Mazda',
)?>

Требователен к количеству колонок в запросе - их должно быть строго две

PDO::FETCH_UNIQUE

похож на предыдущий, но в качестве значения возвращает всю оставшуюся строку. C fetch() этот режим не возвращает ничего вразумительного, а вот с fetchAll() как раз получается такой, весьма востребованный режим. Главное, чтобы первой колонкой в запросе выбиралось уникальное поле - тогда оно будет использовано в качестве индекса возвращаемого массива, вместо обычной нумерации


<?$data = $pdo->query('SELECT * FROM users')->fetchAll(PDO::FETCH_UNIQUE);

/* array (
  'John' => array (
    'sex' => 'male',
    'car' => 'Toyota',
  ),
  'Mike' => array (
    'sex' => 'male',
    'car' => 'Ford',
  ),
  'Mary' => array (
    'sex' => 'female',
    'car' => 'Mazda',
  ),
  'Kathy' => array (
    'sex' => 'female',
    'car' => 'Mazda',
  ),
) */?>

PDO::FETCH_GROUP

Группирует значения по первой колонке. К примеру, нижеследующий код разобьёт пользователей на мальчиков и девочек, и положит их в разные массивы:


<?$data = $pdo->query('SELECT sex, name, car FROM users')->fetchAll(PDO::FETCH_GROUP);

/* array (
  'male' => array ( 0 => 
    array (
      'name' => 'John',
      'car' => 'Toyota',
    ),
    1 => array (
      'name' => 'Mike',
      'car' => 'Ford',
    ),
  ),
  'female' => array (
    0 => array (
      'name' => 'Mary',
      'car' => 'Mazda',
    ),
    1 => array (
      'name' => 'Kathy',
      'car' => 'Mazda',
    ),
  ),
) */?>

То есть, этот режим идеально подходит для классической задачи "вывести события сгруппированные по дням" (или "вывести товары, сгруппированные по категориям"). Также может комбинироваться с PDO::FETCH_COLUMN:


<?$sql = "SELECT sex, name FROM users";
$data = $pdo->query($sql)->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_COLUMN);
/*
array (
  'male' => 
  array (
    0 => 'John',
    1 => 'Mike',
  ),
  'female' => 
  array (
    0 => 'Mary',
    1 => 'Kathy',
  ),
)*/?>

Объектное ориентирование

Разумеется, простым stdObject возможности (я бы даже сказал - аппетиты) PDO по работе с объектами не исчерпываются. Дальше идет целая серия режимов для всевозможной манипуляции ими.

PDO::FETCH_CLASS

Создаёт объект указанного класса, заполняя его свойства данными из БД. Однако здесь, увы, начинаются неудобства и непоследовательность в работе вызывающих функций. Если для fetchAll() можно написать красиво и компактно


<?$data = $pdo->query('SELECT * FROM users LIMIT 1')->fetchAll(PDO::FETCH_CLASS, 'Foo');?>

то для fetch() приходится писать такую колбасу:


<?$stmt = $pdo->query('SELECT * FROM users LIMIT 1');
$stmt->setFetchMode( PDO::FETCH_CLASS, 'Foo');
$data = $stmt->fetch();?>

Из-за того что fetch() не позволяет передать имя класса, мы вынуждены пользоваться setFetchMode(). А учитывая, что эта функция возвращает булево значение, а не ссылку на объект, мы не можем использовать method chaining. Пичаль. Также следует помнить, что в этом режиме PDO будет вызывать магический метод __set() если свойство, совпадающее с именем поля, не найдено в объекте. Для PHP это означает, что если в объекте отсутствует такой метод, то все колонки строки, полученной из БД, будут назначены переменным класса. Если же мы хотим присвоить значения только существующим переменным, то этот момент надо контролировать с помощью метода __set(). Например


<?class Foo
{
    private $name;
    public function __set($name, $value) {}
}
$data = $pdo->query('SELECT * FROM users LIMIT 1')
            ->fetchAll(PDO::FETCH_CLASS, 'Foo');
array(1) {
  [0]=> object(Foo)#3 (1) {
    ["name":"Foo":private]=> string(4) "John"
  }
}?>

в то время как у класса с пустым __set() будут заполнены только существующие свойства:


<?class Foo {}
$data = $pdo->query('SELECT * FROM users LIMIT 1')
            ->fetchAll(PDO::FETCH_CLASS, 'Foo');?>

вернет


<?array(1) {
  [0]=> object(Foo)#3 (3) {
    ["name"] => string(4) "John"
    ["sex"]  => string(4) "male"
    ["car"]  => string(6) "Toyota"
  }
}?>

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

PDO::FETCH_CLASSTYPE

Очень интересная константа. Представляет собой не самостоятельный режим получения данных, а флаг-модификатор, изменяющий поведение других режимов. При её использовании PDO будет брать имя класса из первой колонки полученных из БД данных. То есть, с её помощью код для fetch() можно сделать короче


<?class Foo {}
$data = $pdo->query("SELECT 'Foo', name FROM users LIMIT 1")
            ->fetch(PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE);
/ *object(Foo)#3 (1) {
  ["name"]=> string(4) "John"
}?>

PDO::FETCH_PROPS_LATE

Ещё один флаг-модификатор. По умолчанию PDO присваивает значения свойствам класса до вызова конструктора. При помощи же данной константы это поведение можно изменить - сначала будет вызываться конструктор:


<?class Foo {
private $name;
public function __construct() {
    $this->name = NULL;
}
}
$data = $pdo->query('SELECT name FROM users LIMIT 1')
            ->fetchAll(PDO::FETCH_CLASS, 'Foo');
var_dump($data);
$data = $pdo->query('SELECT name FROM users LIMIT 1')
            ->fetchAll(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'Foo');
var_dump($data);

/* array(1) {
  [0]=> object(Foo)#3 (1) {
    ["name":"Foo":private]=>
    NULL
  }
}
array(1) {
  [0]=> object(Foo)#4 (1) {
    ["name":"Foo":private]=> string(4) "John"
  }
} */?>

PDO::FETCH_INTO

в отличие от PDO::FETCH_CLASS не создаёт новый объект, а обновляет существующий. Соответственно, в качестве параметра передается переменная с объектом. По очевидным причинам имеет смысл только с fetch()


<?class Foo
{
    public $name;
    public $state;

    public function __construct()
    {
        $this->name = NULL;
    }
}
$foo = new Foo;
$foo->state = "up'n'running";
var_dump($foo);

$stmt = $pdo->query('SELECT name FROM users LIMIT 1');
$stmt->setFetchMode(PDO::FETCH_INTO, $foo);
$data = $stmt->fetch();
var_dump($data, $foo);
/*
object(Foo)#2 (2) {
  ["name"]  => NULL
  ["state"] => string(12) "up'n'running"
}
object(Foo)#2 (2) {
  ["name"]  => string(4) "John"
  ["state"] => string(12) "up'n'running"
}
object(Foo)#2 (2) {
  ["name"]  => string(4) "John"
  ["state"] => string(12) "up'n'running"
} */    ?>

Как видно, fetch() возвращает тот же объект, что представляется мне несколько избыточным. Также, с сожалением приходится констатировать, что в отличие от PDO::FETCH_CLASS, этот режим не присваивает значения приватным свойствам.

PDO::FETCH_SERIALIZE

ещё один флаг для PDO::FETCH_CLASS. Должен возвращать объект, который хранился в БД в сериализованном виде. Конструктор не вызывается. На данный момент не работает. Должно быть что-то вроде такого


<?class foo {}
$foo = new foo;
$foo->status="up'n'running";
$sFoo = serialize($foo);
// записываем $sFoo в БД
// и потом что-то вроде
$stmt = $pdo->query('SELECT sFoo FROM table');
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_SERIALIZE, 'foo');
$foo = $stmt->fetch();?>

Этот режим попил у меня крови изрядно. Описание у него самое невинное - "то же самое, что и PDO::FETCH_INTO, но объект передается в сериализованном массиве". А куда передавать-то? В параметры что ли? пробовал и так, и сяк - ничего не получалось. Только когда нашел юнит-тест, посвященный этому режиму, стало понятно, что объект должен придти из БД. Но всё равно не получается - пишет, "cannot unserialize class".

В общем, предполагается, что этот режим должен создавать объект из сериализованного представления, хранящегося в БД. Но это не работает, поскольку при использовании этого метода из базы возвращается не то, что туда клали! Вместо исходного объекта возвращается внонимный класс, который содержит два свойства - одно с именем объекта, и второе с исходным объектом. В общем, мы с zerkms-ом покопались, и в итоге он накатал баг-репорт, https://bugs.php.net/bug.php?id=68802 но воз и ныне там.

Разное

PDO::FETCH_FUNC

для любителей замыканий. Работает только внутри fetchAll(). В параметры функции PDO передаёт переменные для каждого полученного поля, что может быть неудобным - нет доступа к именам полей, а только к значениям. К примеру, эмуляция работы PDO::FETCH_COLUMN:


<?$data = $pdo
    ->query('SELECT name FROM users')
    ->fetchAll(PDO::FETCH_FUNC, function($first) {return $first;});?>

PDO::FETCH_NAMED

почти то же самое, что PDO::FETCH_ASSOC, но с одним отличием. Много раз я встречал на форумах вопросы о том, как получить значения полей с одинаковыми именами из разных таблиц при джойне. Всегда ответ был один - писать алиасы руками в запросе или использовать цифровые индексы. А вот и ответ от PDO: получение данных в этом режиме аналогично PDO::FETCH_ASSOC, но если встречаются поля с одинаковыми именами, то все значения по очереди записываются во вложенный массив. Допустим, у нас есть таблицы users и companies, причем в обеих есть поле name. Если получать данные традиционным путём, то одно из полей будет съедено:


<?$data = $pdo->query("SELECT * FROM users, companies WHERE users.name=username")->fetch();
/*
array(3) {
  ["name"]     => string(10) "ACME, Inc."
  ["sex"]      => string(4) "male"
  ["username"] => string(4) "John"
}*/?>

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


<?$data = $pdo->query("SELECT * FROM users, companies WHERE users.name=username")
            ->fetch(PDO::FETCH_NAMED);
/*
array(3) {
  ["name"]=> array(2) {
    [0]=> string(4) "John"
    [1]=> string(10) "ACME, Inc."
  }
  ["sex"]      => string(4) "male"
  ["username"] => string(4) "John"
}?>

Уж не знаю, насколько это востребованная фича, и насколько легче разбирать массив, нежели прописать алиасы в запросе.

PDO::FETCH_BOUND

очень интересный режим, сильно отличающийся от других. В отличие от всех остальных, он не возвращает не массив или объект. Вместо этого он присваивает значения переменным, предварительно указанным с помощью bindColumn() - режим, аналогичный тому, что используется в mysqli при получении данных из подготовленного запроса. Пример есть в документации

Untested

За неимением подходящей БД я не смог проверить работу еще нескольких констант. Но, на мой взгляд, их поведение достаточно очевидно и не нуждается в особом описании.