небольшой хак для Zend_Db_Table_Row класса

    Основной любого современного web-приложения является взаимодействие с базой данных. В случае если для реализации приложения выбран Zend Framework мы неизбежно столкнемся с использованием класса Zend_Db_Table_Abstract. Этот класс успешно реализует CRUD функции, необходимые для работы с БД, но, тем не менее, эта конструкция содержит один недостаток.


    Рассмотрим следующую структуру БД:

    image

    В данной БД представлено две сущности – новости и комментарии к новостям. Очевидно, что к одной новости может быть оставлено множество комментариев.
    Ставим перед собой задачу – выбрать все новости и комментарии к ним и передать информацию в представление. Как подойти к ней с точки зрения Zend_Db_Table_Abstract? Предположим, что у нас есть соответствующие модели – News и Comments.
    Выбрать все новости достаточно просто:

    $News=new News();
    $news=$News->fetchAll();


    * This source code was highlighted with Source Code Highlighter.

    Затем, следуя логике необходимо в цикле обойти все новости и выбрать для каждой новости все комментарии принадлежащие ей. Тут и возникает главный вопрос – куда записать массив объектов с комментариями?
    Очень хочеться сделать так:

    foreach($news as $record)
    {
        $comments=$Comments->fetchAll(“news_id=”.$record->news_id);
        $record->comments=$comments;
    }

    $this->view->news=$news;


    * This source code was highlighted with Source Code Highlighter.

    Но мы получим исключение, гласящее Specified column «comments» is not in the row. Дело в том, что каждая переменная $record является объектом типа Zend_Db_Table_Row_Abstract. Пытаясь присвоить его свойству comments массив (на самом деле объект типа Zend_Db_Table_Rowset_Abstract) мы вызываем метод __set(). Осмотрим его:

      public function __set($columnName, $value)
      {
        $columnName = $this->_transformColumn($columnName);
        if (!array_key_exists($columnName, $this->_data)) {
          require_once 'Zend/Db/Table/Row/Exception.php';
          throw new Zend_Db_Table_Row_Exception("Specified column \"$columnName\" is not in the row");
        }
        $this->_data[$columnName] = $value;
        $this->_modifiedFields[$columnName] = true;
      }


    * This source code was highlighted with Source Code Highlighter.

    Становиться ясно, что присвоение возможно, только если данное свойство зарегистрировано в массиве $_data. Ну а в массив дата оно попадает с помощью изучения мета информации таблиц участвующих в запросе. Очевидно, что в нашем случае поля comments там никогда не будет.

    Наконец-то мы добрались до решения проблемы. Самым ужасным, что можно сделать, является исправление проблемы непосредственно в коде класса Zend_Db_Table_Row_Abstract и выкинуть оттуда проверку на регистрацию в массиве $_data. Мы, естественно, так делать не будем, мы предпочтем ООП. Все что необходимо сделать – создать собственные классы Row и Rowset.
    Вот код нового класса Row (положим его в файл /library/Main/Db/Table/Row.php):

    <?php

    class Main_Db_Table_Row extends Zend_Db_Table_Row
    {
      public function newDataProperty($value)
      {
        if(!array_key_exists($value,$this->_data))
          $this->_data[$value]="";
      }
    }


    * This source code was highlighted with Source Code Highlighter.

    Вот код нового класса Rowset (файл /library/Main/Db/Table/Rowset.php):

    <?php

    class Main_Db_Table_Rowset extends Zend_Db_Table_Rowset
    {
      function init()
      {
        $this->_rowClass='Main_Db_Table_Row';
      }
    }


    * This source code was highlighted with Source Code Highlighter.

    Это не все, теперь заставим наши модели использовать эти классы. На примере модели News:

    <?php

    class News extends Zend_Db_Table_Abstract
    {
      protected $_name='news';
      protected $_primary='news_id';
      
      function init()
      {
        $this->setRowClass('Main_Db_Table_Row');
        $this->setRowsetClass('Main_Db_Table_Rowset');
      }
      
    }


    * This source code was highlighted with Source Code Highlighter.

    Все практически готово. Последний штрих, изменим код добавления комментариев к объекту новости:

    foreach($news as $record)
    {
        $comments=$Comments->fetchAll(“news_id=”.$record->news_id);
        $record->newDataProperty(‘comments’);
        $record->comments=$comments;
    }

    $this->view->news=$news;


    * This source code was highlighted with Source Code Highlighter.

    Теперь работает!
    Поделиться публикацией

    Похожие публикации

    Комментарии 51

      +3
      Я думаю вам следует почитать про Zend_Db_Table Relationships и тогда хаки делать будет ненужно.

      framework.zend.com/manual/en/zend.db.table.relationships.html
        –2
        Спасибо, я читал.
        Relationship облегчает только получение данных, записать полученные данные в свойство объекта по-прежнему будет не возможно.
          +1
          хорошо что вам все понятно. Если бы был встречный вопрос, я бы написал как ПРАВИЛЬНО это делать, а так не хочется тратить время. Удачи.
            0
            Напишите плиз! Мне прям интересно стало сравнить, как это решается в зенде. Ведь в других yii, kohana, doctrine все елементарно)
              0
              в зенде тоже все элементарно. Сводится все к вызову getBlaBlaBla и готово. Главное прочесть доку :)

                –1
                Сорри, промазал с комментарием. Большой комментарий снизу предназначается вам.

                Магический метод getBlaBlaBla который действительно можно вызвать в представлении, если предопределены связи, является вызовом метода модели в представлении. По моему мнению вызывать методы модели в представлении плохо, все данные должны извлекаться в контроллере и передаваться в представление.
                  +1
                  Row это обертка для конкретного объекта/записи в базе. Это уже не модель.

                  Ничего нет плохого если во view будет что-то типа такого

                  Автор — <?=$this->post->getAuthor()->userName?>

                  Понятно что будет вызов к БД, но например ОРМ фреймворки так и работают. Не думаю что active records в этом отличается.

                    0
                    согласно MVC, представление вполне может брать данные из модели напрямую.
                      –1
                      Плохо? С чего вы взяли что это плохо. Это иногда гораздо удобнее нежели дублирование функциональности в контроллере, а после и во view. Да даже посмотрите на схемку MVC — видите там стрелочку от прямоугольничка View к прямоугольничку Model — так вот это как раз использование модели во view, что бы дать ей представление. А не напихать туда новый порции информации — пусть этим контроллер занимается.

                      Приведу пример — мне нужно для какой-то странички сделать несколько разных представлений. Допустим три. Допустим возьмем пример из статьи. Если тащат обычными способами — даем статью с комментами, автором, тегами, блекджеком и еще чем-нибудь, по RSS — только статью и именем автора. Я могу спокойно сделать так, что бы от смены лайаута у меня грузились совершенно различные данные, никак не затрагивая при этом контроллер, который будет заниматься совершенно другой работой — к примеру проверит права доступа или еще чего — но это если у нас используется ORM который ограждает нас от глупостей вроде написания SQL запросов в этом месте…
                0
                Я вот плотно с Zend не работал. Но в других фреймворках вручную доставать комменты и не нужно. Объявлением relations вы показываете фреймворку как доставать данные из зависимых таблиц.
                Да и вы думаете что до сих пор существовал бы такой минус? Zend серьезный фреймворк, и таких проколов там не может быть. Ваш баг — это стандартная задача при разработке (достать зависимые данные), по этому там это решается иначе, 100%. Удачи!
                  0
                  В Yii действительно все выглядит намного проще. Поэтому меня и интересует этот вопрос по отношению к ZF.
                    –1
                    Ну я мельком посмотрел — там предлагается получать данные через функцию, для каждого объекта.
                    А вообще я вам рекомендую попробовать Doctrine — все очень функционально и удобно. А можно сразу в связке с Symfony, тогда вообще бомба.
                      –1
                      Конечно, уходить на другой фреймворк, если есть проблема — не выход. Но для себя давно решил, что Zend — это набор хороших, даже отличных классов для чего угодно, но это не фреймворк, слишком много нужно доводить вручную.
                        0
                        > но это не фреймворк, слишком много нужно доводить вручную.
                        0_o
                        Фреймворк != полная автоматизация.
                          –2
                          А я где то написал что нужна полная автоматизация? Вы использовали Yii или Symfony?
                          Сравните их с Зендом. Там все более родственно можно так сказать. В Зенде классы почти независимые, что есть и плюсом, и минусом одновременно.
                          Несогласны?
                            0
                            Если минусуете, аргументируйте, пожалуйста, свою позицию.
                              +1
                              Не минусую. Вы сказали что зенд это не фреймворк, я с этим не согласился, все остальное это совершенно о другом.
                              0
                              Я не согласен. Фреймворки это достаточно широкое словечко.

                              Да и архитектура фреймворка вместе с идиологией можут быть какоми угодно, в ZF мне ничего не навязывает, но если я буду использовать встроенные компоненты — он будет собираться по кубикам и подходить друг к другу не хуже нежели symfony — да мне придется потратить чуть больше сил, но зато мне не придется рвать волосы на жопе, что бы изменить поведение фреймворка потому что захотелось такого вот поведения а оно… как бы это сказать… слегка не в стиле получаеться.

                              А сравнивать ZF и Symfony в качестве двух равновесных фреймворков я бы не стал. По крайне мере не встречал, что бы компоненты из других фреймворков использовались так же часто в своих велосипедах, прикрученные к другим фреймворкам и многие разработчики считали что это ничем не хуже нежели использование стандартных решений встроенных в язык.

                              Но плата за это — кривожопые реализации, неоптимальное использование ресурсовов, отсутствие нормального ядра, которое бы действительно объеднило это все в полноценную фреймворк в вашем понимании (все связанно, работает вместе, отлаженно и ничего лишнего делать не надо). То что есть сейчас в Zend_Application — это нецензурный зверек на костылях.

                              зы
                              в этом комменте слишком много раз повторяется слово фреймворк…
                                0
                                Вот с вами я согласен. И я думаю мы прийдем к выводу, что из коробки Zend удобен точно не будет. Ясно, что каждый нормальный разработчик все настроит под себя, у него будет уже свой каркас на основе Zenda. Но такой связанности как Symfony+Doctrine он врятли получит.

                                И Zend, понимая такую ситуацию начал делать Zend_Application, Zend_CodeGenerator, Zend_Tool_*

                                P.S. Я ни в коем случае не обижаю Зенд. Очень класнючий фреймворк, чего только стоит Zend_Search_Lucene! Просто у меня не срослось с ним)
                                  0
                                  Мне к примеру раньше был удобен. Сейчас я стал более ленив и скорее всего, если потребуется поднять стандартный проект что бы там все было стандартно — возьму как раз связку симфонию + доктрину.

                                  Работая с проектом где мне будет необходима дополнительная гибкость + еще кучу разных неизвестных плюшек — то увы, но в руки возьму зенд.

                                  Ясно, что каждый нормальный разработчик все настроит под себя, у него будет уже свой каркас на основе Zenda.
                                  Он получит приложение. Посмотрите в сторону Zend_Tool и Zend_Application — используя их уже можно получить большую связанность и «стандартность» архитектуры приложения.

                                  А Zend_CodeGenerator — немного для другого. Я вот его сейчас активно использую для генерации модели.
                0
                Дело в том что я задавал этот вопрос ранее на askdev.ru ( www.askdev.ru/question/627/Zend_Db_Table-Relationships-%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8-%D0%BF%D0%BE%D0%B4%D1%86%D0%B5%D0%BF%D0%BB%D1%8F%D1%82%D1%8C-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BA-%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D1%83/ ).
                Где вы (я полагаю это вы) в итоге согласились с представленным выше решением.

                Безусловно мне будет все еще интересно услышать правильное решение с использованием relationships, если оно у вас есть.

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

                  Раз уже на то пошло, постараюсь детально изложить свои мысли вечером и напишу ответ на аскдеве.

                  ps — на минусы не обращаю внимания, они на то и минусы :)
                    0
                    protected $_rowClass = 'Default_Model_DbRow_News'; — определили Row класс в классе таблицы.

                    В классе Default_Model_DbRow_News объявляем переменную комментариев.

                    public $comments = null;

                    Теперь когда в цикле вы будете делать

                    $record->comments=$comments;

                    Все будет работать.

                    Т.е всего что нужно сделать это объявить переменную, тогда ругаться не будет.

                    Это немного упростит ваш хак.
                      0
                      Спасибо.

                      Но, думаю вы согласитесь, ваш пример хорош только для одноразового использования.
                      Таких случаев как News и Comments может быть множество. Прийдется для каждого создавать свой Row класс. Заранее продумывать какие свойства в нем объявлять.
                        0
                        Я думаю что не стоит менять поведение самого зенда и добавлять что-то в внутренние массивы типа _data.

                        Если вам нужно что-то многоразовое вспомните о ООП и наследовании. Сделайте базовый Row Класс и наследуйте те у которых будут комментарии от него.

                        Код не должен быть через-чур гибкий и делать все динамически. Нельзя этим злоупотреблять.
                    +4
                    foreach($news as $record)
                    {
                      $comments=$Comments->fetchAll(“news_id=”.$record->news_id);
                      $record->newDataProperty(‘comments’);
                      $record->comments=$comments;
                    }


                    * This source code was highlighted with Source Code Highlighter.

                    Это просто жесть. Зачем делать запросы к базе в цикле? Если уж на то пошло, то соберите все идентификаторы в массив. Потом выборку по in (1, 5, 6, 7) + order, и в цикле разберёте по новостям. Будет 2 запроса вместо фиг знает скольки.

                    И лучше не часто использовать Zend_Db_Table_Row(set) для работы с базой. Если задача просто вывести новости, то никакой проблемы заметно не будет. А вот при повсевместном использовании это сильно затормозит работу приложения. Проверено на личном опыте =).
                      0
                      ну тут проблема неоднозначная. Используя ORM мы получаем удобство кодирования, скорость разработки но взамен получаем меньше контроля над запросами и в итоге нагрузка на БД.
                      Active Records стоит где-то посредине между plain sql и ORM.

                      Если проект будет большой то лучше использовать ORM/Active Records и позаботиться о масштабируемости другими средствами.
                        0
                        Согласен. Но и ORM надо использовать с умом, а не как предлагает автор, запрашивая базу при каждой итерации цикла.

                        Что же касается Zend Framework, то наиболее удобным и гибким методом для извлечени данных считаю Zend_Db_Select. Создав более или менее абстрактный запрос к основным таблицам модели, в будущем его можно без проблем модифицировать в специализированных методах.
                          0
                          Всетаки опровергну, цель заметки не в агитации за множественные запросы к базе данных.
                          Данная структура базы данных служит лишь примером, то же можно сказать и по отношению к коду.
                            0
                            Хорошо, что вы это понимаете. Работая с чужим кодом насмотрелся подобного рода выборок данных предостаточно, поэтому отношусь к ним с подозрением.
                            0
                            Долго смотрел в сторону ZF-ной модели… и увидел многое. Во первых — селект ихний хоть и хорош, но давайте спустимся с высоты ооп и посмотрим все глазами адекватных людей — это большой такой костыль для SQL, где половину всего я пишу плейн текстом и основное предназначение которое — облегчить динамическую сборку запросов.

                            И если вам нравится ZF Select, то вообще советую посмотреть в сторону Doctrine с ее DQL, который хоть и чуточку более плейн текстовый, но способен на достаточно больших запросах не превращаться в треш который невозможно понять с превого, второго и третьего раза и умеет возвращать данные кучкой объектов с которыми можно работать
                              0
                              >Во первых — селект ихний хоть и хорош, но давайте спустимся с высоты ооп и посмотрим все глазами адекватных людей — это большой такой костыль для SQL
                              Это так, но такой «костыль» помогает вести командную разработку, т.к. динамическая сборка запроса облегчается в разы без боязни что-то испортить в исходном варианте.

                              >И если вам нравится ZF Select, то вообще советую посмотреть в сторону Doctrine с ее DQL
                              Честно говоря, давно собираюсь присмотрется поближе, т.к. паттерн Row, Rowset совсем не вызывает симпатии.
                            +1
                            Первое — почему это ActiveRecord стоит где-то посредине plain sql и ORM. AR — это вполне себе такая хорошая реализация ORM, которая будет получше нежели Table/Row Gateway с намеками на AR которая используется в ZF.

                            Используя ORM мы получаем удобство кодирования, скорость разработки но взамен получаем меньше контроля над запросами и в итоге нагрузка на БД.

                            По поводу контроля запроса категорически не согласен. Давайте к примеру взглянем на такую замечательную чтуку как DataMapper, которая позволяет нам сделать такую замечательную вещь как пограничный слой между объектами используемыми в приложении и хранилищем. И в этом слое мы, при должном желании может делать все что нам угодно (хотя кстати встречал людей, которые советовали и в этом слое «избавляться» от SQL...).

                            К примеру у нас есть ситуация (из коммента выше но чуть усложненная) — когда нам следует вытаскивать несколько статей, несколько полей из юзера (к примеру имя, репутация), и список тегов к этой статье. Начинаем вытаскивать в запросе и понимаем, что наступает небольшая такая жопа. И вот тут нам следует на начинать вопить какие плохие ORM-ы, а подумать головой и к примеру вспомнить самый простой и очень действенный способ в данном случае — создаем еще один метод, Назовем его getArticlesWithComments(). Где сделать три-четыре толстых запроса к бд, которая прожует их в несколько раз лучше и собрать все в замечательные объекты.

                            Сейчас в принципе занимаюсь копанием этой темы (особенно занимательно становиться когда приходиться делать всякие статистические запросы к бд) — пишется свой велосипед, ибо использовать ORM, которые необходимо будет допиливать напильником до нужного состояния практически все время (в проектах есть куча специфических вещей в плоть до того, что иногда вместо бд используется совершенное другое приложение являющееся сервисом) — не вижу смысла.

                            Довести бы его до состояния на котором он тут лежит расписанный на четырех листах A4… замечательная чтука должна получиться
                              –1
                              Думаю, процесс работы с БД эволюционирует по такой схеме: используем ORM везде, т.к. очень удобно => понимаем, что она тормознутая, и используеп ООП обёртки над SQL => используем ORM только там, где это можно сделать без лишней нагрузки на сервер и на себя.
                                0
                                Скорее эволюционирует по другой схеме — понимаем как работает наша ORM и учимся правильно ей пользоваться.
                                +1
                                Вы знаете чем отличается AR от Row Gateway?
                                AR = Row Gateway + бизнес логика.

                                Фаулер:
                                «Иногда шлюз записи данных трудно отличить от активной записи(Active Record). В этом случае следует обратить внимание на наличие какой-либо логики домена; если она есть, значит, это активная запись. Реализация шлюза записи данных должна включать в себя только логику доступа к базе данных и никакой логики домена.»

                                Кастомизацией роу в зенде можно добиться любого результата.

                                По поводу выборок зависимых объектов, мы поступили следующим образом (кстати, в трекере зенда есть тикет на эту фичу).
                                Делается выборка, например, статей: $articles = $table->findLastArticles();
                                  +1
                                  Случайно отправил (

                                  Так вот, потом во вьюхе делается следующее.
                                  foreach…
                                  <?= $article->findParentRow('User')->name ?> //Тут планируется шоткат.

                                  Причем юзеры будут загружены только один раз и только те которые нужны для этих статей. Для этого переопределено много логики, в том числе каждая row хранит в себе rowset.

                                  Это очень сильно упрощает код и его становится гораздо меньше. Плюс не нужно писать кучу лишних методов и тестировать их, т.к. этот функционал уже протестирован.
                                    0
                                    Для этого переопределено много логики, в том числе каждая row хранит в себе rowset.

                                    Когда я понял сколько придется мне переопределять логики, я понял, что лучше написать свой простенький велосипед, ибо в одном и том же проекте использовать в разных местах разные методы доступа к данным ну никак не хотелось…
                                      0
                                      Имелось ввиду что это расширение для ZF, и оно используется во всех проектах. В своем коде Вы не делаете ничего нового и не пишите ничего лишнего, все это происходит за кулисами.

                                      А переписать пришлось findParentRow и еще некоторые связанные с ним методы. Все сводится к простому наследованию от нужных классов My_Table extends Ext_Db_Table_Abstract, My_Row extends Ext_Db_Table_Row_Abstract
                                  0
                                  Спасибо, но я в курсе чем отличается AR от Row Gateway, книжка Фаулера так же лежит рядом, и почитываю ее иногда ;)
                                0
                                Запросы в цикле — это зло, т.к. использование ORM или ActiveRecord никак не противоречит применению join'ов.
                                  0
                                  Я скажу больше — они иногда и только за применение всяких джойнов и прочих SQL прелестей в нужных ситуациях.

                                  Хотя я вот никак не могу сейчас соединить воедино запросы в цикле и join. Хотя может быть я не настолько знаю SQL, что бы с помощью джойна вытащить одним запросом связанные сущности для нескольких других связанных сущностей…
                              +1
                              Тут несколько раз проскакивала тема про выборку зависимых объектов одним запросом, с использованием джойнов. Отвечу сразу всем).

                              Предположим что мы используем ZF table, кастомизируем его row и определяем там бизнес логику. Когда мы в какой то row делаем выборку этой сущности и джойном тянем другую сущность, то мы фактически в один класс row загоняем данные которые ей не принадлежат. В zf не реализовано расфасовывание объектов по своим row, отсюда возникает куча проблем.
                              Например мы достали статьи и к ним приосединили юзверей. При выводе статей нам нужно выводить создателей этих статей и рядом показывать значек статуса пользователя (онлайн/офлайн).
                              В row_user у нас был метод isOnline() который содержал какую то простую логику. А в row_article этого метода нет. Что нам остается делать? Либо писать эту проверку прямо в шаблоне (зло), либо дублиовать этот метод в row_article (зло), либо реализовывать разделение объектов (не очень просто) и самый простой и доступный способ это тянуть данные без использования джойнов через findParentRow().

                              Можно конечно возразить что findParentRow(), тянет данные для каждого объекта и это запросы в цикле, но ничто не мешает его переопределить так чтобы подтягивались данные сразу для всего rowset, только при первом обращении к findParentRow.
                                +1
                                Еще забыл про использование хелперов, но это тоже не тру вей. Разделение логики и данных уводит нас от ООП и в итоге мы получим кучу функций в виде хелперов и кучу объектов представляющих собой просто контейнеры разнородных данных.
                                  0
                                  Предположим что мы используем ZF table, кастомизируем его row и определяем там бизнес логику.

                                  Паттерн Row Data Gateway выполняет роль удобного взаимодействия с БД, бизнес-логика, как правило, реализуется в другом слое, который расположен над этим.
                                    +1
                                    В данном случае нет правила. Делают как удобно. Я выше писал про то что разбавляя Row Data Gateway бизнес логикой мы получаем AR. И это проще чем писать еще один класс на каждую таблицу к уже существующим двум классам Table и Row. При этом если Вам хочется вынести логику выше, потому что Вы считаете так правильнее, то почему бы и нет. Ведь тогда мы придем к преобразователю данных, разделив взаимодействие c базой и логику на две паралелльные иерархии классов, но за это придется платить сложностью.
                                      0
                                      >Ведь тогда мы придем к преобразователю данных, разделив взаимодействие c базой и логику на две паралелльные иерархии классов, но за это придется платить сложностью.
                                      Как-то мы с вами уже спорили на подобную тему… и если уж выносить логику выше в отдельную иерархию классов, то надо отказываться от Row совсем. Иначе получится путаница — 2 места для добавления новой функциональности. Применять сразу оба подхода в рамках одного компонента нецелесообразно.
                                        0
                                        На самом деле все еще сложнее), просто мы обсуждали не это и я специально не заострял внимание на деталях.
                                          0
                                          Не вижу сложностей =). Либо вы кладёте логику в Row, либо в метод модели. На вкус и цвет, как известно, никого нет.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.