Jul 20

На работе пришлось столкнуться с очень не нравившейся мне 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, посвященный его улучшению (правда, я не знаю, насколько он сейчас актуален, проверять нет времени :( ).

written by fxposter \\ tags: , ,


31 Responses to “Использование Zend_Db_Table”

  1. 1. Сергей Says:

    Насчёт валидаторов не могу согласиться – они очень удобно проинтегрированы с Zend_Form, за счёт чего валидация данных и вывод ошибок идёт не на уровне вставки в БД, а раньше, при отправке формы. Очень удобно.
    Всё же ZF – это фреймворк, а не набор библиотек.

  2. 2. Vadim Voituk Says:

    Уже лет так 7 наблюдаю попытки создать для PHP нормальный ORM – ещё ни разу не видел решения построенного на интроспекции имен методотов (которая кстати в PHP есть ещё с 4й версии), как в Grails ORM.

  3. 3. FX Poster Says:

    Сергей
    Не нужен мне Zend_Form, понимаешь. И, как ни странно, данные могут приходить не только из форм. ;) Поэтому валидаторы/контроль типов вполне уместны именно в ORM.

    Vadim Voituk
    В смысле? Я знаю, что такое интроспекция методов. Но фраза “решения построенного на интроспекции имен методотов” мне как-то не сильно понятна.

  4. 4. Vadim Voituk Says:

    Посмотри примеры использования Grails ORM – логика того, что нужно выбрать формируется на основе имени вызываемого метода.
    Например:
    Obj.findUserByLoginAndPassword(‘mylogin’, ‘mypassword’);

  5. 5. Сергей Says:

    Всё же ZF предназначен для комплексного использования. Связка Zend_Form+Zend_Db_Table+Zend_Validate прекрасно и чётко работает. Если нужна нестандартная валидация данных при вставке в таблицу (в чём я, честно говоря, смысла не вижу), можно расширить Zend_Db_Table для интеграции с Zend_Validate, или же использовать стандартные средства Zend_Validate вручную, перед вставкой.

  6. 6. Vadim Voituk Says:

    IMHO такой вариант является наиболее удобным из тех, что я когда-либо видел.
    Также очень понравился в использовании Java Persistent API – но ввиду отсутсвия аннотаций в PHP его реализация мне видится смутно.

    P.S. ну “закидон” для holy war-а:
    Последние 3 года работы над высоконагруженными системам, очень неласковыми методами научили меня обходиться без ORM и пользоваться только примитивными классами-обертками над plain old sql.

  7. 7. Сергей Says:

    2 Vadim Voituk:
    В Zend_Db_Table такое есть, посмотрите внимательнее:
    http://framework.zend.com/manual/ru/zend.db.table.relationships.html#zend.db.table.relationships.fetching.dependent

  8. 8. FX Poster Says:

    Сергей
    можно расширить Zend_Db_Table для интеграции с Zend_Validate
    А я вижу в этом смысл и именно об этом и говорю.

    По поводу ZF как фреймворка – я останусь при своём мнении.

    Vadim Voituk
    Т.е. типа того, что я сделал с fetchRowBy/fetchAllBy, только помощнее? Не знаю, насколько это нужно… Всё, что действительно требует крутых запросов – удобнее делать через всякие Finder’ы. В данном случае – для построение sql удобнее использовать Zend_Db_Select.

  9. 9. FX Poster Says:

    Последние 3 года работы над высоконагруженными системам, очень неласковыми методами научили меня обходиться без ORM и пользоваться только примитивными классами-обертками над plain old sql.
    Да, есть такое дело. Мне, правда, такое не грозит – высоконагруженных сайтов мы не делаем. :( От себя еще добавлю, что на самом деле удобно пользоваться лишь двумя типами систем работы с бд:

    1. Хорошие ORM (Hibernate, Doctrine)
    2. Чистый SQL, либо обертки для построения SQL, но данные потом в обьекты не рассовывать, а использовать “как бд выдала”

    В “недо-ORM” вот этот недо-handling связей, а именно – то, что приходится из result’ов баз данных данные в обьекты засовывать вручную, а потом эти обьекты еще и связывать, ОЧЕНЬ напрягает.

  10. 10. FX Poster Says:

    Сергей
    Да ладно? Это используется исключительно для связей и, если честно, не сильно понятно, почему вообще было сделано – так как ни в каких других местах Zend_Db этот подход не используется.

  11. 11. Vadim Voituk Says:

    Сергей

    В Zend_Db_Table такое есть, посмотрите внимательнее:

    Посмотрел внимательнее – не увидел ничего похожего на то, о чем я писал.

    FX Poster
    Правильно мыслишь :)

  12. 12. Vadim Voituk Says:

    … и к последней ссылке
    Можете посчитать меня немодным и отсталым, но такого вот г%%на я уже наглотался и больше не буду:

    $select = $db->select()
    ->from( ...specify table and columns... )
    ->where( ...specify search criteria... )
    ->order( ...specify sorting criteria... );

  13. 13. FX Poster Says:

    Vadim Voituk
    Эмм… А почему? Doctrine такой же способ предлагает, кроме написания DQL вручную (DQL – Doctrine Query Language, что-то типа SQL, только ориентированное на обьекты).

  14. 14. Сергей Says:

    Прошу прощения, не совсем верно понял, в чём суть предлагаемого метода. Действительно, такого в ZF нет и он был бы полезен.

  15. 15. quard Says:

    А мне нравится ORM в cakephp :) Довольно удобно описываются связи и запросы. Решил для проекта сделать тоже похожую штуку. День потратил на создание. Работало хорошо, но потом пришлось часть переделать и дополнить, так как в одном месте не учел кол-во записей в таблице и скрипт просто вылетал на max execution time :) тьфу-тьфу-тьфу, щас все починил и жду проверки на основном сервере :)

  16. 16. FX Poster Says:

    А мне нравится ORM в cakephp
    Фигня. Почему у нас нет понятия “запись”? Почему я не могу нормально работать с записью и добавлять ей предметную логику? Почему запись вообще представлена обычным ассоциативным массивом?

    Почему у меня findAll возвращает вот такую фигню:

    Array
    (
        [0] => Array
            (
                [ModelName] => Array
                    (
                        [id] => 83
                        [field1] => value1
                        [field2] => value2
                        [field3] => value3
                    )
    
                [AssociatedModelName] => Array
                    (
                        [id] => 1
                        [field1] => value1
                        [field2] => value2
                        [field3] => value3
                    )
            )
        [1] => Array
            (
                [ModelName] => Array
                    (
                        [id] => 85
                        [field1] => value1
                        [field2] => value2
                        [field3] => value3
                    )
    
                [AssociatedModelName] => Array
                    (
                        [id] => 2
                        [field1] => value1
                        [field2] => value2
                        [field3] => value3
                    )
            )
    )

    Это не ORM, ни капли.

  17. 17. quard Says:

    Хм. Запись должна быть именно объектом? Почему-то меня устраивает ассоциативные массивы. А в примере что не устраивает? :) получили запись ModelName с данными, получили hasOne модель (ну или hasMany, но не понятно там что именно).

    Можешь тогда написать что для тебя ORM? Я все хочу статью написать об ORM, как едят, с чем едят. Плюсы и минусы использования, примеры. Вобщем обзорную статью :)

  18. 18. FX Poster Says:

    У меня есть статья $article. Как мне по ней получить группу $group, с которой эта статья связана?

    Насчет ORM – читать Patterns of EAA, а именно главы про источники данных и способы огранизации бизнес-логики. И я много писал об этом, преимущественно в комментариях.

  19. 19. FX Poster Says:

    “Ведь что собой ActiveRecord представляет – класс, обьекты которого полностью представляют из себя записи таблицы – их можно сохранять, на них навешиваются валидаторы и прочее. И набор методов, которые бы возвращали обьекты этого класса.”

  20. 20. quard Says:

    навскидку скажу что надо сделать связь belongsTo c моделью Group. когда получаем $article, мы получим заодно и $group.

    про книгу спасибо, почитаю если найду :) в cakephp валидаторы навешиваются, можно для каждой записи филтры ставить, тоесть писать код для события “вставили”, “получили запись”, “сохранили”. Тоже удобно. Я соглашусь что symfony с Propel и Doctrine штука серьезная и расчитана для крупных проектов. Но когда надо сделать что-то быстро, “развернуться” – cakephp подходит. Ещё cakephp можно использовать в пототипировании. Начать что-то делать можно через 10 минут после установки.

  21. 21. FX Poster Says:

    навскидку скажу что надо сделать связь belongsTo c моделью Group. когда получаем $article, мы получим заодно и $group.
    Мне не нужно “заодно”. Мне нужно получить группу именно тогда, когда она мне понадобится – может и сразу, а может и потом.

    Я соглашусь что symfony с Propel и Doctrine штука серьезная и расчитана для крупных проектов. Но когда надо сделать что-то быстро, “развернуться” – cakephp подходит. Ещё cakephp можно использовать в пототипировании. Начать что-то делать можно через 10 минут после установки.
    В symfony возможности начала работы сразу после установки не меньшие. И для мелких проектов она тоже вполне себе подходит. В CakePHP, как и в CI мне модель не нравиться абсолютно, потому как возможностей писать бизнес-логику нулевые – её в массив не вставишь, т.е. нужен еще один обьект для понятия “запись”.

  22. 22. quard Says:

    если “заодно” не нравится — можно сделать так $this->Article->Group->findById(); это в контроллере конечно

  23. 23. FX Poster Says:

    Ок. А не логично, чтобы я мог сделать

    $article->getGroup()

    не используя уже таблицы, и не указывая специально group_id?

  24. 24. quard Says:

    Никто не мешает добавить функцию в модель.

  25. 25. quard Says:

    Хотя там наверно все равно придется указать id группы. В Propel и Doctrine это автоматически?

  26. 26. FX Poster Says:

    В CakePHP нет модели записи, есть модель таблицы. Так что добавлять функцию мне некуда. :)

    Кстати, дело не в “автоматически” (хотя в двух указанных ORM это делается всё же автоматом), а в том, что добиться такого в CakePHP я не могу в принципе.

  27. 27. quard Says:

    Хм, надо будет поизучать как это работает в доктрине и пропеле :) Может в свою реализацию перетащу идеи :)

  28. 28. FX Poster Says:

    Удачи. :)

  29. 29. quard Says:

    Спасибо! :)

  30. 30. Vadim Says:

    Насчет ORM – читать Patterns of EAA

    Именно читать, а не слепо следовать.
    Впрочем это относится к большинству творений Фаулера. Особенно поздних.

  31. 31. FX Poster Says:

    Я не могу сказать, что узнал много нового из POEAA, но для того, кто “не в теме” – книга будет полезной. :)

Leave a Reply