Как стать автором
Обновить

Zend_Db – объекты модели, связи и сложные условия

Время на прочтение9 мин
Количество просмотров750

1. Модель данных, таблицы и связи


Многие задавались вопросом, как правильно задать метод

$articles->findAllByCategoryId($categoryId);
или
$category->findAllArticles();

Причем оба варианта подразумевают связывание объектов — $category «знает» об $articles или наоборот.

А можно ли обойтись без таких методов, которые слишком много знают? Кто-то предпочитает doctrine, я же пошел своим путем – написал несколько классов, наследовав их от Zend_Db_Table_Abstract и Zend_Db_Table_Row_Abstract

Цели были
1) получить тонкий и легко читаемый контроллер,
(Удивительно, но модель при таком подходе также осталась не очень толстой)
2) сделать очеловеченные объекты модели,
3) избавиться от реализации кучи методов getAllSomethingByAnythingAlsoWhere(...)
4) а также добиться того, чтобы модели не знали друг о друге ничего(*).

Постарался обойтись без употребления в контроллере и модели таких слов как select, adapter, fetch, Zend_Db_Expr, quote,… ну это личное.

Проще все рассмотреть на примере, который хоть и является «сферическим конем в вакууме», но довольно нагляден.

Контроллер:

<?php
class User_IndexController extends EXT_Controller_Action
{
    /* Action 1 */ 
    public function indexAction()
    {
        $table = new User_Model_DbTable_Users();
        $users = $table->with('Address', 'User_Model_DbTable_Address', 'address_id')
                      ->with('Location', 'User_Model_DbTable_Locations', 'Location.id = Address.location_id')
                      ->addRelationMany('Places', 'User_Model_DbTable_Address', 'user_id')
                      ->addRelationMany('Friends', 'User_Model_DbTable_Friends', 'user_id', true)
                      ->findAll();
        $this->view->users = $users;
    }
        
    /* Action 2 */ 
    public function listAction()
    {
        $table = new User_Model_DbTable_Users();
        $users = $table->withRelated()
                      ->addRelationMany('Places', 'User_Model_DbTable_Address', 'user_id')
                      ->addRelationMany('Friends', 'User_Model_DbTable_Friends', 'user_id', true)
                      ->addFilter(array('OR',
                                         array('name', 'LIKE', 'Alex%'),
                                         array('AND',
                                              array('Location.title', '=', 'Swietokrzyskie'),
                                              array('Address.street', 'LIKE', 'Sovi%') )))
                      ->setOrder('created_at DESC')
                      ->setLimit(10)
                      ->findAll();
        $this->view->users = $users;
    }
}

* This source code was highlighted with Source Code Highlighter.


А вот объекты БД (модель):

/* Таблица пользователей */
class User_Model_DbTable_Users extends EXT_Db_Table
{
  /* имя таблицы */
  protected $_name = 'user__users';
  /* класс объекта - строки таблицы */
  protected $_rowClass = 'User_Model_User';
  /* связи. если они и присутствуют, то только в этом $_relations */
  protected $_relations = array('Address' => array('User_Model_DbTable_Address', 'address_id'),
                 'Location' => array('User_Model_DbTable_Locations', 'Location.id = Address.location_id'));
  /* будет ли id строки являться индексом резельтата выборки (массива) */
  protected $_isIndexedRowset = true;

  const AUTH_NAMESPACE = 'auth';
  
  /* фильтры по умолчанию. Хорошо сочетаются с добавленными */
  public function loadDefaultFilters()
  {
    if (!$this->isLoadDefaultFilters()) {
      return $this;
    }    
    $this->addFilter('is_deleted', '<>', 1);
    return $this;     
  }
}

/* Один пользователь */
class User_Model_User extends EXT_Db_Table_Row
{
}

/* Адреса */
class User_Model_DbTable_Address extends EXT_Db_Table
{
  protected $_name = 'user__address';
  protected $_rowClass = 'User_Model_Address';
  protected $_relations = array('Location' => array('User_Model_DbTable_Locations', 'location_id'),
                 'User'   => array('User_Model_DbTable_Users', 'user_id'),);
}

/* Адрес */
class User_Model_Address extends EXT_Db_Table_Row
{
}

/* Друзья, таблица, связывающая user'ов' */
class User_Model_DbTable_Friends extends EXT_Db_Table
{
  protected $_name = 'user__friends';
  protected $_rowClass = 'User_Model_Friend';
  
  protected $_relations = array('User'   => array('User_Model_DbTable_Users', 'user_id'),
                 'Friend'  => array('User_Model_DbTable_Users', 'friend_id'));
}

* This source code was highlighted with Source Code Highlighter.


В примере два практически одинаковых action-a. Выборка пользователей.

В основной SQL-запрос попадают данные, которые привязаны к таблице пользователей «один-к-одному» с помощью ->with() и будут доступны через объекты пользователей без дополнительных запросов к БД примерно так:
$user->getLocation()->title // синоним $user->Location__title
$user->getAddress()->street // синоним $user->Address__street
Естественно нет никаких функций getAddress() и getLocation() — они реализованы динамически через __call().

Здесь я вернусь к началу топика.
У меня в шаблоне view используются
$user->getAllFriends() // получение всех друзей пользователя
$user->getAllPlaces() // получение всех адресов пользователя
Эти несуществующие (также __call()-вызов) методы обращаются к связям, заданным в контроллере:
1) $table->with(...) // задает связь «к-одному» — эти данные будут включены в основной запрос findAll(), findRow(), findAllBy(), findRowBy().
Можно задать только необходимые присоединенные поля в запрос, уменьшив его.
Еще в with можно передавать не имя класса, а например $friendsTable — таблицу с установленными фильтрами и своими связями.
Вообще описать все нюансы может выйти дольше, чем написать сами классы. :)
2) $table->addRelationMany(...) // задает связь «ко многим» — данные в запрос не включаются, но будут подгружены из БД при первом непосредственном обращении ( $user->getAllFriends() )
Связующее поле можно задать именем, имя+префикс, полное обычное условие равенства полей.

Т.е. сами модели не связаны, но при этом связи можно прозрачно использовать.

* Вы наверняка заметили protected $_relations =…
Всё-таки бывают очевидные, постоянно использующиеся связи, которые для удобства можно (если очень хочется) прописать в единственном(!) месте в заголовке класса, не запутывая объекты избыточными связями.
Во втором моем action-е метод $table->withRelated() говорит, что эти связи надо включить в основной запрос.
Если этого не сделать, то при вызове $user->getAddress() для каждого пользователя будет дополнительно вызываться SQL.
(прим. 'Location' — таблица дерева «Страна-Область-Город»)

2. Фильтры — условия выборки


Я считаю, что у Zend_Db нет хорошего построителя where() – условий для SQL запросов.
Т.е. условие (field_a=1 OR field_b=2) AND (field_c=1 OR field_d=2) уже вызывает некоторые сложности в построении.
Во втором action-e моего контроллера я привел пример построения сложных условий на базе своего самодельного класса фильтров.
Этот фильтр нормально «переваривает» знаки равенства / неравенства в IS NULL / IS NOT NULL, IN (1,2,3) / NOT IN (1,2,3) и т.д.
Вот какая монстр-конструкция была построена этим фильтром в связке с findAll() в итоге:

SELECT `user__users`.*,
    `Address`.`id` AS `Address__id`,
    `Address`.`street` AS `Address__street`,
    `Address`.`house` AS `Address__house`,
    `Address`.`floor` AS `Address__floor`,
    `Address`.`flat` AS `Address__flat`,
    `Address`.`location_id` AS `Address__location_id`,
    `Address`.`user_id` AS `Address__user_id`,
    `Location`.`id` AS `Location__id`,
    `Location`.`parent_id` AS `Location__parent_id`,
    `Location`.`root_path` AS `Location__root_path`,
    `Location`.`title` AS `Location__title`,
    `Location`.`level` AS `Location__level`,
    `Location`.`code` AS `Location__code`
FROM `user__users`
LEFT JOIN `user__address` AS `Address`
ON user__users.address_id=Address.id
LEFT JOIN `user__locations` AS `Location`
ON Location.id = Address.location_id
WHERE (((`user__users`.`is_deleted` != 1)
        AND ((`user__users`.`name` LIKE 'Alex%'
             OR ((`Location`.`title` = 'Swietokrzyskie') AND (`Address`.`street` LIKE 'Sovi%')))))    
ORDER BY `created_at` DESC
LIMIT 10

* This source code was highlighted with Source Code Highlighter.


Есть нюанс с лишней парой скобок, но на правильность запроса это не влияет.
Вообще чаще используются более простые условия выборки, чем в примере:
->addFilter('created_at', '>=', $dateStart)
->addFilter('is_new', '=', 1)
Фильтры, заданные в loadDefaultFilters() применяются автоматически, если не сброшены принудительно этим clearFilters(false) или этим setIsLoadDefaultFilters(false). Так-же задаются сортировка и лимит по умолчанию.

Свои надстройки над Zend я поместил в отдельную библиотечку. Требует рефакторинга и прочего шлифования, но тем не менее уже работоспособно и довольно удобно (imho).
скачать — там только библиотека, модуль и таблицы из примера.
Можно «поковырять» или прикрутить к своему Zend-приложению, запустить и снова поковырять.

P.S. Если будет интересно, могу выложить еще описание своей ACL на базе Zend_Acl — там есть интересные, возможно СПОРНЫЕ (как и вышеописанные) решения. А также класс простого дерева Tree без nested sets — были причины сделать именно без. К тому же nested sets для Zend уже успешно реализован и без меня.

P.P.S.
Забыл показать шаблон вида. И удивляюсь, почему так много уделяют внимания контроллеру.
Вот собственно:

<div class="container2">
<div class="container">
  <?php foreach ($this->users as $user):?>
  <div>
    <?= $user->name ?> (<?= $user->email ?>),
    <?= $user->Location__title ?> <?= $user->getAddress()->street ?>, <?= $user->Address__house ?>
    <ul>
      <?php foreach ($user->getAllPlaces() as $place):?>
       <li><?= $place->street ?> <?= $place->house ?> <?= $place->getLocation()->title ?></li>
      <?php endforeach;?>
    </ul>
    <ul>
      <?php foreach ($user->getAllFriends() as $friend):?>
       <li>friend: <?= $friend->Friend__name ?> </li>
      <?php endforeach;?>
    </ul>
  </div>
  <?php endforeach;?>
</div>
</div>


* This source code was highlighted with Source Code Highlighter.
Теги:
Хабы:
Всего голосов 11: ↑8 и ↓3+5
Комментарии23

Публикации

Истории

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн