На работе пришлось столкнуться с очень не нравившейся мне ORM’кой собственного производства. Стал делать свою (ну не дурак ли, а? :)), наваял за 3 дня простенькую ORM, отображающую структуру таблиц на обьекты, не контроллируя типов. Итог получился примерно такой:
- класс базы данных (относледовался от mysqli, pdo использовать было нельзя)
- класс таблицы, хранящий в себе бд, и отвечающий за CRUD записей
- класс записи, перенаправляющий методы CUD классу таблицы
Примерное использование:
$table = new ArticleTable();
$record = $table->fetchOneWhere("slug = 'hello'"); // получаем существующую запись
$record->name = 'Fucking Article!';
$record->save(); // вызывает insert/update в зависимости от того, новая ли это запись
$record = $table->create(); // создаем новую запись
$record->name = 'Fucking Article2!';
$record->slug = 'fucking_article';
// ...
$record->save();
И что-то мне это очень сильно напомнило, а именно - Zend_Db: Zend_Db_Table / Zend_Db_Table_Row. Недолго думая - выкинул нафиг свою систему и залил в проект кусок Zend Framework’а (если нужно - потом скажу, какие именно файлы нужны для полноценной работы всего компонента Zend_Db), а также решил почитать, что сейчас вообще есть в этой Zend_Db, а есть, как оказалось - довольно много:
- Хорошая абстракция работы с бд
- Классы записи/таблицы
- Поддержка fetch’инга связанных обьектов
- Поддержка many-to-many связей (этого даже в Propel нет)
На самом деле - еще есть вещи, коотрые бы можно было добавить, чтобы они работали автоматически:
- Валидаторы в зависимости от типов полей таблицы
- Возможность сразу fetch’ить данные из нескольких таблиц (точнее - получить такие данные довольно легко, но вот разбрасывать их по разным обьектам и связывать эти обьекты сейчас нужно ручками, если я не ошибаюсь, но опять же - это проблем не составляет)
Вроде всё. Общее впечатление - просто замечательная система. Использовать легко и приятно. :)
Пример:
Сначала пойдут мои супертипы слоя (кто читал Patters of EAA - поймет):
class Db_Table extends Zend_Db_Table_Abstract {
/**
* @return Zend_Db_Table_Rowset_Abstract
*/
public function fetchAllBy($key, $value) {
$where = $this->getAdapter()->quoteInto("$key = ?", $value);
return $this->fetchAll($where);
}
/**
* @return Zend_Db_Table_Row_Abstract
*/
public function fetchRowBy($key, $value) {
$where = $this->getAdapter()->quoteInto("$key = ?", $value);
return $this->fetchRow($where);
}
public function __call($name, $arguments) {
if(strpos($name, 'fetchRowBy') === 0) {
array_unshift($arguments, substr($name, 10));
return call_user_func_array(array($this, 'fetchRowBy'), $arguments);
}
if(strpos($name, 'fetchAllBy') === 0) {
array_unshift($arguments, substr($name, 10));
return call_user_func_array(array($this, 'fetchAllBy'), $arguments);
}
throw new Exception("Undefined method $name");
}
}
class Db_Record extends Zend_Db_Table_Row_Abstract {
}
А теперь - пример использования:
class Item extends Db_Table {
protected $_name = 'items';
protected $_rowClass = 'ItemRecord';
protected $_referenceMap = array(
'Group' => array(
'columns' => 'groupid',
'refTableClass' => 'Group',
'refColumns' => 'groupid',
)
);
}
class ItemRecord extends Db_Record {
}
class Group extends Db_Table {
protected $_name = 'groups';
protected $_rowClass = 'GroupRecord';
protected $_dependentTables = array('Item');
}
class GroupRecord extends Db_Record {
}
$itemTable = new Item();
$item = $itemTable->fetchRowBySlug('hello');
$group = $item->findParentGroup();
Согласитель - всё просто и удобно, не так ли?
Для заинтересовавшихся - очень советую проштудировать полностью главу о Zend_Db из документации Zend Framework’а. А также - мой пост про Zend_Db_Table, посвященный его улучшению (правда, я не знаю, насколько он сейчас актуален, проверять нет времени :( ).






July 20th, 2008 at 08:46
Насчёт валидаторов не могу согласиться - они очень удобно проинтегрированы с Zend_Form, за счёт чего валидация данных и вывод ошибок идёт не на уровне вставки в БД, а раньше, при отправке формы. Очень удобно.
Всё же ZF - это фреймворк, а не набор библиотек.
July 20th, 2008 at 13:16
Уже лет так 7 наблюдаю попытки создать для PHP нормальный ORM - ещё ни разу не видел решения построенного на интроспекции имен методотов (которая кстати в PHP есть ещё с 4й версии), как в Grails ORM.
July 20th, 2008 at 13:48
Сергей
Не нужен мне Zend_Form, понимаешь. И, как ни странно, данные могут приходить не только из форм. ;) Поэтому валидаторы/контроль типов вполне уместны именно в ORM.
Vadim Voituk
В смысле? Я знаю, что такое интроспекция методов. Но фраза “решения построенного на интроспекции имен методотов” мне как-то не сильно понятна.
July 20th, 2008 at 13:53
Посмотри примеры использования Grails ORM - логика того, что нужно выбрать формируется на основе имени вызываемого метода.
Например:
Obj.findUserByLoginAndPassword(’mylogin’, ‘mypassword’);
July 20th, 2008 at 13:56
Всё же ZF предназначен для комплексного использования. Связка Zend_Form+Zend_Db_Table+Zend_Validate прекрасно и чётко работает. Если нужна нестандартная валидация данных при вставке в таблицу (в чём я, честно говоря, смысла не вижу), можно расширить Zend_Db_Table для интеграции с Zend_Validate, или же использовать стандартные средства Zend_Validate вручную, перед вставкой.
July 20th, 2008 at 13:59
IMHO такой вариант является наиболее удобным из тех, что я когда-либо видел.
Также очень понравился в использовании Java Persistent API - но ввиду отсутсвия аннотаций в PHP его реализация мне видится смутно.
P.S. ну “закидон” для holy war-а:
Последние 3 года работы над высоконагруженными системам, очень неласковыми методами научили меня обходиться без ORM и пользоваться только примитивными классами-обертками над plain old sql.
July 20th, 2008 at 14:01
2 Vadim Voituk:
В Zend_Db_Table такое есть, посмотрите внимательнее:
http://framework.zend.com/manual/ru/zend.db.table.relationships.html#zend.db.table.relationships.fetching.dependent
July 20th, 2008 at 14:03
Сергей
можно расширить Zend_Db_Table для интеграции с Zend_Validate
А я вижу в этом смысл и именно об этом и говорю.
По поводу ZF как фреймворка - я останусь при своём мнении.
Vadim Voituk
Т.е. типа того, что я сделал с fetchRowBy/fetchAllBy, только помощнее? Не знаю, насколько это нужно… Всё, что действительно требует крутых запросов - удобнее делать через всякие Finder’ы. В данном случае - для построение sql удобнее использовать Zend_Db_Select.
July 20th, 2008 at 14:10
Последние 3 года работы над высоконагруженными системам, очень неласковыми методами научили меня обходиться без ORM и пользоваться только примитивными классами-обертками над plain old sql.
Да, есть такое дело. Мне, правда, такое не грозит - высоконагруженных сайтов мы не делаем. :( От себя еще добавлю, что на самом деле удобно пользоваться лишь двумя типами систем работы с бд:
В “недо-ORM” вот этот недо-handling связей, а именно - то, что приходится из result’ов баз данных данные в обьекты засовывать вручную, а потом эти обьекты еще и связывать, ОЧЕНЬ напрягает.
July 20th, 2008 at 14:11
Сергей
Да ладно? Это используется исключительно для связей и, если честно, не сильно понятно, почему вообще было сделано - так как ни в каких других местах Zend_Db этот подход не используется.
July 20th, 2008 at 14:14
Сергей
Посмотрел внимательнее - не увидел ничего похожего на то, о чем я писал.
FX Poster
Правильно мыслишь :)
July 20th, 2008 at 14:16
… и к последней ссылке
Можете посчитать меня немодным и отсталым, но такого вот г%%на я уже наглотался и больше не буду:
$select = $db->select()
->from( ...specify table and columns... )
->where( ...specify search criteria... )
->order( ...specify sorting criteria... );
July 20th, 2008 at 14:21
Vadim Voituk
Эмм… А почему? Doctrine такой же способ предлагает, кроме написания DQL вручную (DQL - Doctrine Query Language, что-то типа SQL, только ориентированное на обьекты).
July 20th, 2008 at 14:30
Прошу прощения, не совсем верно понял, в чём суть предлагаемого метода. Действительно, такого в ZF нет и он был бы полезен.
July 20th, 2008 at 17:24
А мне нравится ORM в cakephp :) Довольно удобно описываются связи и запросы. Решил для проекта сделать тоже похожую штуку. День потратил на создание. Работало хорошо, но потом пришлось часть переделать и дополнить, так как в одном месте не учел кол-во записей в таблице и скрипт просто вылетал на max execution time :) тьфу-тьфу-тьфу, щас все починил и жду проверки на основном сервере :)
July 20th, 2008 at 17:47
А мне нравится ORM в cakephp
Фигня. Почему у нас нет понятия “запись”? Почему я не могу нормально работать с записью и добавлять ей предметную логику? Почему запись вообще представлена обычным ассоциативным массивом?
Почему у меня findAll возвращает вот такую фигню:
Это не ORM, ни капли.
July 20th, 2008 at 17:56
Хм. Запись должна быть именно объектом? Почему-то меня устраивает ассоциативные массивы. А в примере что не устраивает? :) получили запись ModelName с данными, получили hasOne модель (ну или hasMany, но не понятно там что именно).
Можешь тогда написать что для тебя ORM? Я все хочу статью написать об ORM, как едят, с чем едят. Плюсы и минусы использования, примеры. Вобщем обзорную статью :)
July 20th, 2008 at 18:00
У меня есть статья $article. Как мне по ней получить группу $group, с которой эта статья связана?
Насчет ORM - читать Patterns of EAA, а именно главы про источники данных и способы огранизации бизнес-логики. И я много писал об этом, преимущественно в комментариях.
July 20th, 2008 at 18:03
“Ведь что собой ActiveRecord представляет - класс, обьекты которого полностью представляют из себя записи таблицы - их можно сохранять, на них навешиваются валидаторы и прочее. И набор методов, которые бы возвращали обьекты этого класса.”
July 20th, 2008 at 18:25
навскидку скажу что надо сделать связь belongsTo c моделью Group. когда получаем $article, мы получим заодно и $group.
про книгу спасибо, почитаю если найду :) в cakephp валидаторы навешиваются, можно для каждой записи филтры ставить, тоесть писать код для события “вставили”, “получили запись”, “сохранили”. Тоже удобно. Я соглашусь что symfony с Propel и Doctrine штука серьезная и расчитана для крупных проектов. Но когда надо сделать что-то быстро, “развернуться” - cakephp подходит. Ещё cakephp можно использовать в пототипировании. Начать что-то делать можно через 10 минут после установки.
July 20th, 2008 at 18:35
навскидку скажу что надо сделать связь belongsTo c моделью Group. когда получаем $article, мы получим заодно и $group.
Мне не нужно “заодно”. Мне нужно получить группу именно тогда, когда она мне понадобится - может и сразу, а может и потом.
Я соглашусь что symfony с Propel и Doctrine штука серьезная и расчитана для крупных проектов. Но когда надо сделать что-то быстро, “развернуться” - cakephp подходит. Ещё cakephp можно использовать в пототипировании. Начать что-то делать можно через 10 минут после установки.
В symfony возможности начала работы сразу после установки не меньшие. И для мелких проектов она тоже вполне себе подходит. В CakePHP, как и в CI мне модель не нравиться абсолютно, потому как возможностей писать бизнес-логику нулевые - её в массив не вставишь, т.е. нужен еще один обьект для понятия “запись”.
July 20th, 2008 at 18:44
если “заодно” не нравится — можно сделать так $this->Article->Group->findById(); это в контроллере конечно
July 20th, 2008 at 18:47
Ок. А не логично, чтобы я мог сделать
не используя уже таблицы, и не указывая специально group_id?
July 20th, 2008 at 18:54
Никто не мешает добавить функцию в модель.
July 20th, 2008 at 18:56
Хотя там наверно все равно придется указать id группы. В Propel и Doctrine это автоматически?
July 20th, 2008 at 19:11
В CakePHP нет модели записи, есть модель таблицы. Так что добавлять функцию мне некуда. :)
Кстати, дело не в “автоматически” (хотя в двух указанных ORM это делается всё же автоматом), а в том, что добиться такого в CakePHP я не могу в принципе.
July 20th, 2008 at 19:14
Хм, надо будет поизучать как это работает в доктрине и пропеле :) Может в свою реализацию перетащу идеи :)
July 20th, 2008 at 19:16
Удачи. :)
July 20th, 2008 at 19:16
Спасибо! :)
July 20th, 2008 at 21:00
Именно читать, а не слепо следовать.
Впрочем это относится к большинству творений Фаулера. Особенно поздних.
July 20th, 2008 at 21:05
Я не могу сказать, что узнал много нового из POEAA, но для того, кто “не в теме” - книга будет полезной. :)