Константи методу fetch з PDO
Хоча цікаві для нас константи частково описуються на сторінках документації, присвячених методам fetch()
та fetchAll()
, повний список наведено лише на сторінці із загальним списком констант PDO, що не здається мені дуже зручним, і може бути тією причиною, з якої деякі особливо цікаві режими отримання даних опинилися поза увагою більшості розробників. Найбільш затребуванні константи методу fetch
із загального списку і робиті на кілька категорій.
2.Найкорисніше
3.Об'єктне орієнтування
4.Різне
Класика
Для початку перерахуємо константи, які дублюють стандартну поведінку теплих лампових функцій mysql. Всі приклади надаються з використанням функції var_export()
.
PDO::FETCH_BOTH
аналог mysql_fetch_array()
. Усі дані повертаються у дубльованому вигляді - і з текстовими індексами, і з цифровими. Цей режим включено до стандартного PDO.TCH_NUM
знов старий знайомий, аналог mysql_fetch_row()
. Лише цифрові індекси:
<?$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_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
, причому цей об'єкт наділений цілою купою унікальних властивостей:
- По-перше, ця змінна не містить самі запитані дані, а віддає їх на запит (на що натякає назву)
- По-друге, крім запитаних даних, в об'єкті є загадкова змінна, яка називається queryString і містить SQL запит(!) (що натякає про спорідненість цього класу PDOStatement:
-
<?$lazy = $pdo->query("SELECT name FROM users")->fetch(PDO::FETCH_LAZY); /* object(PDORow)#3 (2) { ["queryString"] => string(22) "SELECT name FROM users" ["name"] => string(4) "John" }*/?>
-
Для допитливих - так, перезаписати queryString можна :)
По-третє, цю змінну неможливо зберегти у сесії (або, іншими словами – серіалізувати)
По-четверте, отримувати з неї дані можна як завгодно, будь-яким із трьох способів: через числовий індекс, асоціативний, або звертаючись до якості класу через ->
По-п'яте, звернення до неіснуючих властивостей не викликає нотису Undefined property/index. Мовчки повертається NULL.
По-шосте, ця змінна змінює свій стан від подальших викликів fetch() з іншими константами. Плюс до всього, ця константа не працює з fetchAll(), а лише з fetch().
Проведемо кілька експериментів. Спробуємо запитати щодо великий обсяг даних, і подивимося, як змінюється споживання пам'яті, і заразом перевіримо ще пару тверджень:
<?$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, додаючи до рядка мегабайтне поле. Після цього ми отримуємо рядок через цю константу – обсяг пам'яті не змінився. Тільки після того, як ми привласнюємо значення мегабайтного поля змінної – він збільшується. Для контролю виводимо ім'я з таблиці користувачів. Потім отримуємо ще один рядок, вже в одному із стандартних режимів. Бачимо, що обсяг пам'яті відразу підскакує, на відміну від попереднього випадку. Економія в першому випадку очевидна! Також ми бачимо відсутність нотису та зміну стану
$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()
. А враховуючи, що ця функція повертає булеве значення, а не посилання на об'єкт, ми не можемо використовувати метод 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-ом покопалися, і в результаті він накотив баг-репорт, але віз і нині там.
Різне
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
За відсутністю відповідної БД я не зміг перевірити роботу ще кількох констант. Але, на мій погляд, їхня поведінка досить очевидна і не потребує особливого опису.
- PDO::ATTR_FETCH_CATALOG_NAMES - повертає імена колонок у форматі "каталог.ім'я_колонки". Підтримується не всіма драйверами.
- PDO::ATTR_FETCH_TABLE_NAMES - повертає імена колонок у форматі "ім'ятаблиці.ім'яколонки". Підтримується не всіма драйверами.
- PDO::FETCH_ORI_* - 6 функцій з управління курсором при отриманні даних із БД. Приклад є в документації