Справочники, инструменты, документация

SQL: Режимы получения данных, константы PDO::FETCH_*

В этой статье мы научимся управлять режимами получения данных.

PDO::FETCH_BOTH

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

$stm = $db->query('SELECT * FROM categories LIMIT 1')->fetch(PDO::FETCH_BOTH);

// Результат
Array
(
 [id] => 1
 [0] => 1
 [name] => Ноутбуки и планшеты
 [1] => Ноутбуки и планшеты
)

PDO::FETCH_NUM

Аналог mysql_fetch_row() - только цифровые индексы:

$stm = $db->query('SELECT * FROM categories LIMIT 1')->fetch(PDO::FETCH_NUM);

// Результат
Array
(
 [0] => 1
 [1] => Ноутбуки и планшеты
)

PDO::FETCH_ASSOC

Аналог mysql_fetch_assoc() - только текстовые индексы.

$stm = $db->query('SELECT * FROM categories LIMIT 1')->fetch(PDO::FETCH_ASSOC);

// Результат
Array
(
 [id] => 1
 [name] => Ноутбуки и планшеты
)

PDO::FETCH_OBJ

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

$stm = $db->query('SELECT * FROM categories LIMIT 1')->fetch(PDO::FETCH_OBJ);

// Результат
stdClass Object
(
 [id] => 1
 [name] => Ноутбуки и планшеты
)

PDO::FETCH_LAZY

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

$stm = $db->query('SELECT `name` FROM categories')->fetch(PDO::FETCH_LAZY);

// Результат
PDORow Object
(
 [queryString] => SELECT `name` FROM categories
 [name] => Ноутбуки и планшеты
)

PDO::FETCH_COLUMN

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

$stm = $db->query('SELECT `name` FROM categories')->fetchAll(PDO::FETCH_COLUMN);

// Результат
Array
(
 [0] => Ноутбуки и планшеты
 [1] => Компьютеры и периферия
 [2] => Комплектующие для ПК
 [3] => Смартфоны и смарт-часы
 [4] => Телевизоры и медиа
 [5] => Игры и приставки
 [6] => Аудиотехника
 [7] => Фото-видеоаппаратура
 [8] => Офисная техника и мебель
 [9] => Сетевое оборудование
 [10] => Крупная бытовая техника
 [11] => Товары для кухни
 [12] => Красота и здоровье
 [13] => Товары для дома
 [14] => Инструменты
 [15] => Автотовары
)

PDO::FETCH_KEY_PAIR

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

$stm = $db->query('SELECT `name`, `id` FROM categories')->fetchAll(PDO::FETCH_KEY_PAIR);

// Результат
Array
(
 [Ноутбуки и планшеты] => 1
 [Компьютеры и периферия] => 2
 [Комплектующие для ПК] => 3
 [Смартфоны и смарт-часы] => 4
 [Телевизоры и медиа] => 5
 [Игры и приставки] => 6
 [Аудиотехника] => 7
 [Фото-видеоаппаратура] => 8
 [Офисная техника и мебель] => 9
 [Сетевое оборудование] => 10
 [Крупная бытовая техника] => 11
 [Товары для кухни] => 12
 [Красота и здоровье] => 13
 [Товары для дома] => 14
 [Инструменты] => 15
 [Автотовары] => 16
)

PDO::FETCH_UNIQUE

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

$stm = $db->query('SELECT * FROM categories LIMIT 3')->fetchAll(PDO::FETCH_UNIQUE);

// Результат
Array
(
 [1] => Array
 (
 [name] => Ноутбуки и планшеты
 [0] => Ноутбуки и планшеты
 )
    [2] => Array
     (
     [name] => Компьютеры и периферия
     [0] => Компьютеры и периферия
     )

    [3] => Array
 (
 [name] => Комплектующие для ПК
 [0] => Комплектующие для ПК
 )

)

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',
 ),
)

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_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 при получении данных из подготовленного запроса. Пример есть в документации.