Николай Ланец
22 мар. 2015 г., 16:53

Классы для работы с базой данных на лету

Пишу топик с описанием новейших технологий, корни которых берут свое начало еще вот в этом топике, написанном более двух лет назад. Решил его перенести сюда. Почитайте пока, а я статью свою допишу. Она довольно интересная :)
Один из самых главных барьеров в переходе с MODX Evo на MODX Revo — это xPDO. Многим выносит мозг тот факт, что надо создавать физические файлы с какими-то классами, генерировать схему и много еще каких-то танцев с бубнами. «Невозможность» работать в полной мере с базой данных отпугивает очень многих, и многие продолжают разрабатывать на Эво, просто потому что там «проще», хотя и с соблазном смотрят на всякие плюшки Ревы, типа пакетов, источников файлов и т.п.
Но ответьте мне на такой вопрос: «Вы родились со знаниями того, как работать с mysql? Все сразу освоили mysql_connect(), mysql_select_db(), mysql_query() и т.д.и т.п.?» Согласитесь, что все это так же приходилось осваивать, и совсем не за один день.
Я сейчас приведу совсем небольшой, но очень и очень хитрый код (результат моих последних исследований xPDO и продолжение позавчерашней темы), а под катом вы узнаете очень много нового, и возможно кому-то работа с xPDO покажется еще проще, чем с mysql-функциями и библиотеками.
$modx->map['page'] = array ( 'table' => 'site_content', 'fields' => array ( 'id' => '', 'pagetitle' => '', 'content' => '', ), ); class page extends xPDOObject{} class page_mysql extends page{} $o=$modx->getObject('page', array( 'id' => 1 ));
Что здесь происходит?

А происходит следующее: мы быстренько и на лету создали свой объект для взаимодействия с базой данных ( в частности с таблицей site_content (вместе с префиксом modx_site_content)), и извлекли из нее запись с id=1 (выполнили запрос).
Далее можно, к примеру, получить значения объекта. К примеру так:
$content = $o->get('content');
Или так (сразу все данные):
$data_array = $o->toArray();
Заметка: имя таблицы указано в описании объекта
'table' => 'site_content',
Как это работает? (вкратце)
  1. Мы создали классы page и page_mysql, расширяющие объект xPDOObject (так как именно объекты xPDOObject имеют необходимый функционал для взаимодействия с базой данных, типа load(),save(), remove() и т.п.)
  2. Мы добавили описание своего объекта в окружение xPDO ($modx->map['page']) (не забывает, что MODx — это расширение класса xPDO).
  3. Получили объект (выполнили запрос)$o=$modx->getObject('page', array( 'id' => 1 ));
Что это дает?
Такой подход позволяет выполнять взаимодействие с базой данных вообще из любого положения, будь то сниппет, плагин или даже во внешнем файле. Вот такой код работает:
// Bla-Bla-Bla include MODx class $modx= new modX(); $modx->map['page'] = array ( 'table' => 'site_content', 'fields' => array ( 'id' => '', 'pagetitle' => '', 'content' => '', ), );
Читай: самый быстрый коннектор с MODX-окружением и без лишних инициализаций классов-реквестеров-респонсеров, сессии и т.п. Можно еще облегчить, если напрямую подсосать конфиги и инициализировать не MODx класс, а xPDO.
Плюс к этому можно четко управлять какие колонки извлекать, а какие нет, и не бояться за повторные запросы (читаем про это здесь).
И вообще еще много чего дает, о чем я прям сейчас не буду писать, но в дальнейшем буду время от времени выкладывать примеры.
А почему не писать чистые SQL-запросы и не выполнять их через $modx->prepare($sql)?
Здесь несколько причин.
1. Префиксы таблиц в базе данных. По умолчанию они modx_, но не редкость, когда и отличаются. Даже если вы перед каждым запросом будет получать имя таблицы через API MODX-а, то это как минимум не удобно. 2. Написание чистых SQL-запросов так же требуют знаний, и не малых. В приведенном же случае достаточно просто знать структуру таблицы (какие есть колонки). 3. Просто выборка — это еще ладно, не сложно (select * from table;). А что вы будете делать, если вам надо обновить 28 колонок за раз? Могу сказать точно, что написание такого SQL-запроса так же займет не мало времени. Есть еще причины, но этого, думаю, достаточно.
А почему сразу 2 класса надо, а не один?
Один класс — чисто базовый, содержащий дополнительный функционал. Второй класс — для своего типа базы данных, чтобы логику взаимодействия с БД можно было разделить. То есть если это mysql, то класс будет classname_mysql, если MSSQL SERVER, то classname_sqlsrv.
Более подробно с примерами.
Приведенный выше пример — это простейший вариант, позволяющий только делать выборки из БД. Для более полного взаимодействия с базой требуется описание колонок таблицы (я не нашел документации с описанием мета-данных xPDO-объектов, потому напишу при аказии мануал, а пока по примерам и копаем класс xPDOObject).
В итоге, если мы хотим, чтобы можно было сохранять данные объектов в БД, то нам понадобится описание колонок. К слову, при попытке сохранения объекта xPDO будет писать запись в таблицу, только она не будет содержать значений.
Итак, дополним наш класс
$modx->map['page'] = array ( 'table' => 'site_content', 'fields' => array ( 'id' => '', 'pagetitle' => '', 'content' => '', ), 'fieldMeta' => array ( 'id' => array ( 'dbtype' => 'int', 'precision' => '10', 'attributes' => 'unsigned', 'phptype' => 'integer', 'null' => false, 'index' => 'pk', 'generated' => 'native', ), 'pagetitle' => array ( 'dbtype' => 'varchar', 'precision' => '255', 'phptype' => 'string', 'null' => false, 'default' => '', 'index' => 'fulltext', 'indexgrp' => 'content_ft_idx', ), 'content' => array ( 'dbtype' => 'mediumtext', 'phptype' => 'string', 'index' => 'fulltext', 'indexgrp' => 'content_ft_idx', ), ), ); class page extends xPDOObject{} class page_mysql extends page{} $o=$modx->newObject('page', array( 'pagetitle' => 'new pagetitle', 'content' => 'some content', )); $o->save();
Здесь мы добавили массив-описание с мета-данными колонок. Зачем они нужны? Они позволяют определить xPDO-объекту какого типа данные хранятся на стороне базы данных, а какие типы на стороне php. Рассмотрим на примере описания колонки id:
'id' => array ( 'dbtype' => 'int', // Тип данных настороне базы (число) 'precision' => '10', // Длина (10 цифр) 'attributes' => 'unsigned', // только положительное 'phptype' => 'integer', // Тип на стороне PHP - число 'null' => false, // Может ли быть нулевым* 'index' => 'pk', // индекс Primary Key 'generated' => 'native', // Флаг, что генерируемое. То есть если не указано и не нулевое, то будет надеяться на базу данных, иначе ругаться будет ),
Вот когда колонка четко описана, тогда xPDO знает как работать с данной таблицей, и можно сохранять данные в базу методом ->save();
Кстати, тут есть хитрость: дело в том, что ООП никто не отменял, и можно переопределять не только xPDOObject, но и его дочерние классы, к примеру xPDOSimpleObject. Что это нам дает? В xPDOSimpleObject уже описана колонка id (и ничего более кроме нее), и поэтому мы можем выкинуть ее из нашего описания. Вот описание объекта xPDOSimpleObject:
<?php /* * Copyright 2010-2012 by MODX, LLC. * * This file is part of xPDO. * * xPDO is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * xPDO is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place, * Suite 330, Boston, MA 02111-1307 USA */ /** * Metadata map for the xPDOSimpleObject class. * * Provides an integer primary key column which uses MySQL's native * auto_increment primary key generation facilities. * * @see xPDOSimpleObject * @package xpdo * @subpackage om.mysql */ $xpdo_meta_map = array( 'xPDOSimpleObject' => array( 'table' => null, 'fields' => array( 'id' => null, ), 'fieldMeta' => array( 'id' => array( 'dbtype' => 'INTEGER', 'phptype' => 'integer', 'null' => false, 'index' => 'pk', 'generated' => 'native', 'attributes' => 'unsigned', ) ), 'indexes' => array( 'PRIMARY' => array( 'alias' => 'PRIMARY', 'primary' => true, 'unique' => true, 'type' => 'BTREE', 'columns' => array( 'id' => array( 'length' => '', 'collation' => 'A', 'null' => false, ), ), ) ) ) );
К слову, находится он в файле core/xpdo/om/mysql/xpdosimpleobject.map.inc.php, а не в model/modx/… Это вам гарантирует, что ваши объекты, расширяющие этот класс, будут работать даже за пределами MODX на чистом xPDO. Из MODX-объектов минимум 28 штук расширяют этот класс, дабы не плодить описание колонки id.
Обновим наш код, удалив описание колонки id.
<?php print '<pre>'; $modx->map['page'] = array ( 'table' => 'site_content', 'fields' => array ( 'pagetitle' => '', 'content' => '', ), 'fieldMeta' => array ( 'pagetitle' => array ( 'dbtype' => 'varchar', 'precision' => '255', 'phptype' => 'string', 'null' => false, 'default' => '', 'index' => 'fulltext', 'indexgrp' => 'content_ft_idx', ), 'content' => array ( 'dbtype' => 'mediumtext', 'phptype' => 'string', 'index' => 'fulltext', 'indexgrp' => 'content_ft_idx', ), ), ); class page extends xPDOSimpleObject{} class page_mysql extends page{} $o=$modx->getObject('page', 1); print_r($o->toArray());
Вот, уже компактней. Многие конечно могут посетовать на то, что описывая так объекты в своих сниппетах, они получатся громозскими. Но ведь и здесь можно играться. Создать несколько базовых элементарных классов, воткнуть их в один плагин, и расширять нужные. Ведь расширять и 10 классов можно. Кстати, несколько классов, воткнутых в плагин, будет быстрее работать, чем рассовывать их по файлам по классической модели, так как по классике получается на каждый объект по 3 файла чтение, а плагин — один. Читай: еще момент для оптимизации, хоть и не значительной.
В оставшихся колонках особо описание не отличается, только что типы данных другие, но на последок есть у меня еще фишка про запас:-) Давайте посмотрим на код метода xPDOObject::save() (правда он не маленький)
public function save($cacheFlag= null) { if ($this->isLazy()) { $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Attempt to save lazy object: ' . print_r($this->toArray('', true), 1)); return false; } $result= true; $sql= ''; $pk= $this->getPrimaryKey(); $pkn= $this->getPK(); $pkGenerated= false; if ($this->isNew()) { $this->setDirty(); } if ($this->getOption(xPDO::OPT_VALIDATE_ON_SAVE)) { if (!$this->validate()) { return false; } } if (!$this->xpdo->getConnection(array(xPDO::OPT_CONN_MUTABLE => true))) { $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not get connection for writing data", '', __METHOD__, __FILE__, __LINE__); return false; } $this->_saveRelatedObjects(); if (!empty ($this->_dirty)) { $cols= array (); $bindings= array (); $updateSql= array (); foreach (array_keys($this->_dirty) as $_k) { if (!array_key_exists($_k, $this->_fieldMeta)) { continue; } if (isset($this->_fieldMeta[$_k]['generated'])) { if (!$this->_new || !isset($this->_fields[$_k]) || empty($this->_fields[$_k])) { $pkGenerated= true; continue; } } if ($this->_fieldMeta[$_k]['phptype'] === 'password') { $this->_fields[$_k]= $this->encode($this->_fields[$_k], 'password'); } $fieldType= PDO::PARAM_STR; $fieldValue= $this->_fields[$_k]; if (in_array($this->_fieldMeta[$_k]['phptype'], array ('datetime', 'timestamp')) && !empty($this->_fieldMeta[$_k]['attributes']) && $this->_fieldMeta[$_k]['attributes'] == 'ON UPDATE CURRENT_TIMESTAMP') { $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S'); continue; } elseif ($fieldValue === null || $fieldValue === 'NULL') { if ($this->_new) continue; $fieldType= PDO::PARAM_NULL; $fieldValue= null; } elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('timestamp', 'datetime')) && in_array($fieldValue, $this->xpdo->driver->_currentTimestamps, true)) { $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S'); continue; } elseif (in_array($this->_fieldMeta[$_k]['phptype'], array ('date')) && in_array($fieldValue, $this->xpdo->driver->_currentDates, true)) { $this->_fields[$_k]= strftime('%Y-%m-%d'); continue; } elseif ($this->_fieldMeta[$_k]['phptype'] == 'timestamp' && preg_match('/int/i', $this->_fieldMeta[$_k]['dbtype'])) { $fieldType= PDO::PARAM_INT; } elseif (!in_array($this->_fieldMeta[$_k]['phptype'], array ('string','password','datetime','timestamp','date','time','array','json'))) { $fieldType= PDO::PARAM_INT; } if ($this->_new) { $cols[$_k]= $this->xpdo->escape($_k); $bindings[":{$_k}"]['value']= $fieldValue; $bindings[":{$_k}"]['type']= $fieldType; } else { $bindings[":{$_k}"]['value']= $fieldValue; $bindings[":{$_k}"]['type']= $fieldType; $updateSql[]= $this->xpdo->escape($_k) . " = :{$_k}"; } } if ($this->_new) { $sql= "INSERT INTO {$this->_table} (" . implode(', ', array_values($cols)) . ") VALUES (" . implode(', ', array_keys($bindings)) . ")"; } else { if ($pk && $pkn) { if (is_array($pkn)) { $iteration= 0; $where= ''; foreach ($pkn as $k => $v) { $vt= PDO::PARAM_INT; if ($this->_fieldMeta[$k]['phptype'] == 'string') { $vt= PDO::PARAM_STR; } if ($iteration) { $where .= " AND "; } $where .= $this->xpdo->escape($k) . " = :{$k}"; $bindings[":{$k}"]['value']= $this->_fields[$k]; $bindings[":{$k}"]['type']= $vt; $iteration++; } } else { $pkn= $this->getPK(); $pkt= PDO::PARAM_INT; if ($this->_fieldMeta[$pkn]['phptype'] == 'string') { $pkt= PDO::PARAM_STR; } $bindings[":{$pkn}"]['value']= $pk; $bindings[":{$pkn}"]['type']= $pkt; $where= $this->xpdo->escape($pkn) . ' = :' . $pkn; } if (!empty ($updateSql)) { $sql= "UPDATE {$this->_table} SET " . implode(',', $updateSql) . " WHERE {$where}"; } } } if (!empty ($sql) && $criteria= new xPDOCriteria($this->xpdo, $sql)) { if ($criteria->prepare()) { if (!empty ($bindings)) { $criteria->bind($bindings, true, false); } if ($this->xpdo->getDebug() === true) $this->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Executing SQL:\n{$sql}\nwith bindings:\n" . print_r($bindings, true)); if (!$result= $criteria->stmt->execute()) { $errorInfo= $criteria->stmt->errorInfo(); $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n" . $criteria->toSQL() . "\n" . print_r($errorInfo, true)); if (($errorInfo[1] == '1146' || $errorInfo[1] == '1') && $this->getOption(xPDO::OPT_AUTO_CREATE_TABLES)) { if ($this->xpdo->getManager() && $this->xpdo->manager->createObjectContainer($this->_class) === true) { if (!$result= $criteria->stmt->execute()) { $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $criteria->stmt->errorCode() . " executing statement:\n{$sql}\n"); } } else { $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error " . $this->xpdo->errorCode() . " attempting to create object container for class {$this->_class}:\n" . print_r($this->xpdo->errorInfo(), true)); } } } } else { $result= false; } if ($result) { if ($pkn && !$pk) { if ($pkGenerated) { $this->_fields[$this->getPK()]= $this->xpdo->lastInsertId(); } $pk= $this->getPrimaryKey(); } if ($pk || !$this->getPK()) { $this->_dirty= array(); $this->_validated= array(); $this->_new= false; } $callback = $this->getOption(xPDO::OPT_CALLBACK_ON_SAVE); if ($callback && is_callable($callback)) { call_user_func($callback, array('className' => $this->_class, 'criteria' => $criteria, 'object' => $this)); } if ($this->xpdo->_cacheEnabled && $pk && ($cacheFlag || ($cacheFlag === null && $this->_cacheFlag))) { $cacheKey= $this->xpdo->newQuery($this->_class, $pk, $cacheFlag); if (is_bool($cacheFlag)) { $expires= 0; } else { $expires= intval($cacheFlag); } $this->xpdo->toCache($cacheKey, $this, $expires, array('modified' => true)); } } } } $this->_saveRelatedObjects(); if ($result) { $this->_dirty= array (); $this->_validated= array (); } return $result; } Что здесь самое интересное? А интересное начинается с этой строчки: foreach (array_keys($this->_dirty) as $_k) { Смотрим, к примеру, на это: if ($this->_fieldMeta[$_k]['phptype'] === 'password') { $this->_fields[$_k]= $this->encode($this->_fields[$_k], 'password'); }
То есть, если в описании колонки есть 'phptype'=>'password', то значение этой переменной автоматически будет закодировано методом $this->encode() Посмотрим на этот метод.
public function encode($source, $type= 'md5') { if (!is_string($source) || empty ($source)) { $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'xPDOObject::encode() -- Attempt to encode source data that is not a string (or is empty); encoding skipped.'); return $source; } switch ($type) { case 'password': case 'md5': $encoded= md5($source); break; default : $encoded= $source; $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOObject::encode() -- Attempt to encode source data using an unsupported encoding algorithm ({$type})."); break; } return $encoded; }

То есть если тип — password или md5, то значение будет закодировано в md5. И не забывайте, что это ООП, то есть этот метод можно переопределить.
Или вот это:
if (in_array($this->_fieldMeta[$_k]['phptype'], array ('datetime', 'timestamp')) && !empty($this->_fieldMeta[$_k]['attributes']) && $this->_fieldMeta[$_k]['attributes'] == 'ON UPDATE CURRENT_TIMESTAMP') { $this->_fields[$_k]= strftime('%Y-%m-%d %H:%M:%S'); continue; }
То есть если тип — datetime или timestamp и указан атрибут 'ON UPDATE CURRENT_TIMESTAMP', то при сохранении объекта xPDO автоматически будет обновлять значение на текущее время. К слову, это описание есть в класс modSystemSetting, и мы всегда видим время последнего обновления. При этом исключается человеческий фактор, что кто-то забудет обновить это поле. То есть даже если вы в своем сниппете сделаете так:
$o = $modx->getObject('modSystemSetting','site_name'); $o->set('value', 'New site name'); $o->save();
, время изменения записи зафиксируется.
В общем там еще многое всего очень и очень интересного, и я буду постепенно выкладывать новые материалы.
P.S. Если кто-то все еще считает, то xPDO сложно и вообще не заслуживает внимания, тот — не я.
БлагоДарю тебя друг за такой интереснейший пост!!!
Ближе к середине был уверен, что выражу благодарность в комментариях обязательно. :)
Всегда пожалуйста! :)
Хорошая статья, спасибо за предоставленный материал
Спасибо большое, очень полезная статья)
А как выбрать из таблицы в БД массив строк? Например, я пишу в сниппете:
<?php $modx->map['page'] = array ( 'table' => 'shopkeeper3_orders', 'fields' => array ( 'id' => '', 'price' => '', 'date' => '', 'sentdate' => '', 'email' => '', 'delivery' => '', 'payment' => '', 'status' => '', ), ); class page extends xPDOObject{} class page_mysql extends page{} $o=$modx->getObject('page', array( 'id' => $modx->getUser()->get("id") )); return $o->get('status');
Возвращается результат только первой записи в таблице, ассоциированной с данным пользователем
Всё, решил вопрос через getCollection)

Добавить комментарий