ZFConf 2010: What News Zend Framework 2.0 Brings to Us
ZFConf 2011: Толстая модель: История разработки собственного ORM (Михаил Шамин)
1. Толстая модель
История разработки ORM
Шамин Михаил
Geometria.ru
Ведущий разработчик
2.
3. Geometria.ru
• Главный фотохроникер страны
• 8 лет на рынке
• Представительство в 150 городах России, СНГ и
Прибалтики
• Ежедневно 80 000 пользователей / 600 000 просмотров
• В понедельник 110 000 / 1 000 000
• Более 500 000 репортажей
• 15 000 000 фотографий
• 800 000 зарегистрированных пользователей
4.
5. Почему понадобился свой ORM
Было
• Наследство в виде залежей кода-лапши
• Практически вся бизнес-логика в контроллерах
• Вплоть до формирования select ов!
• Некоторые экшены размером в 200 строк!
• В роли модели - Zend_Db_Table
6. Почему понадобился свой ORM
Стало
• Стали использовать NoSQL решения, такие как Redis и
Mongo
• Понадобилось решение, готовое работать с любым
хранилищем, а не только SQL
• Есть ли что-то на рынке?
• Doctrine2 в alphа, еще сырая - страшно.
• Что делать?
• Пишем свой велосипед!
7. Выбор дизайна
Открываем книгу
Мартина Фаулера
(Martin Fowler)
"Шаблоны корпоративных приложений"
("Patterns of Enterprise Application Architecture")
8.
9. Выбор дизайна
И находим то что нужно.
Domain Model
или
Модель предметной области
10. Поля модели. Как задавать ?
• В Zend_Db_Table_Row поля не прописаны явно, а
берутся из схемы таблицы БД
• В Doctrine2 через задание private/protected свойств и
генерацию getter/setter методов.
11. Поля модели. Решение:
Использовать DocBlock
Профиты:
• Готовый шаблон для типов данных
• Сразу в аннотации класса видны все поля модели
• Автокомплит в IDE (Zend Studio, PhpStorm, NetBeans)
• Быстрое создание классов
13. Zend_Reflection для генерации полей
После $post = new Model_Post();
свойство $_data будет выглядеть следующим образом:
class Model_Post extends Geometria_Model
{
protected $_data = array(
'id' => null,
'title' => null,
'body' => null,
'hidden' => null,
'date' => null,
);
}
14. Доступ к полям
• Внешний доступ к полям обеспечивается через
магические методы __get() и __set()
• Можно реализовать методы get<поле> и set<поле>,
чтобы изменить логику установки/получения значения
поля.
15. /**
* ...
* @property integer $date Unix timestamp
*/
class Model_Post extends Geometria_Model
...
public function setDate($value)
{
if ($value instanceof Zend_Date) {
$value = $value->getTimestamp();
}
$this->_data['date'] = $value;
}
16. Установка значения по умолчанию
class Model_Post extends Geometria_Model
...
public function getDate($value)
{
if (null === $this->_data['date']) {
$this->_data['date'] = time();
}
return $this->_data['date'];
}
17. Как хранить модель?
Используем DataMapper
• Маппер знает все о модели и о том, как и где еѐ
хранить.
• Модель ничего не знает о хранилище.
• Логика домена отделена от persist логики
• Можно менять структуру бд или даже сменить
хранилище, не меняя логику модели, всего лишь
изменив маппер.
• Маппер выполняет CRUD операции
• Можно использовать любое хранилище: MySQL, Mongo,
Redis, Config file, RESTApi и др.
18. Интерфейс маппера
interface Geometria_Model_Mapper_Interface
{
public function create(Geometria_Model $model);
public function update(Geometria_Model $model);
public function delete(Geometria_Model $model);
public function fetchOne($cond, $sort);
public function fetchAll($cond, $sort, $limit, $skip);
public function getCount($cond);
}
19. Работа с моделью
$post = new Model_Post();
$post->title = 'hello world!';
$post->body = 'foo bar';
$postMapper = new Model_Post_Mapper();
$postMapper->create($post);
echo $post->id; // 1 маппер сам проставил в модели
id
20. Выборки
• Условие $cond - простой массив
имя поля => значение
• Сортировка $sort - тоже просто массив
имя поля => (bool) направление сортировки
• Для более сложных выборок пишем отдельный метод
Выбрать 10 скрытых постов, начиная с самых новых
$mapper->fetchAll(
array('hidden' => true),
array('date' => false),
10
);
21. Делаем ActiveRecord
Рассказываем модели, что у нее есть маппер.
• Делаем статический метод getMapper() который из
специального контейнера
Geometria_Model_Mapper_Manager достает нужный ей
маппер
• Делаем у модели методы create(), update(), delete()
public function create()
{
return self::getMapper()->create($this);
}
22. Теперь создание модели выглядит так:
$post = new Model_Post();
$post->title = 'hello world!';
$post->body = 'foo bar';
$post->create();
echo $post->id; // 1
А пост можно получить в одну строчку:
$post = Model_Post::getMapper()->fetchOne(
array('id' => 1)
);
или так
$post = Model_Post::getMapper()->fetch(1);
23. Что вернет fetchAll()? Коллекцию!
• аналог Zend_Db_Table_Rowset
• Паттерн Record Set
• Позволяет выполнять массовые действия с набором
моделей
interface Geometria_Model_Collection_Interface
extends Iterator, Countable
{
public function append(Geometria_Model $model);
public function prepend(Geometria_Model $model);
public function populate(array $data);
public function clear();
public function toArray();
}
25. Нужен Paginator?
class Geometria_Paginator_Adapter_Mapper
...
public function getItems($offset, $limit)
{
return $this->_mapper->fetchAll(
$this->_cond,
$this->_sort,
$limit,
$offset
);
}
public function count()
{
return $this->_mapper->getCount(
$this->_cond,
$this->_sort
);
}
26. Хотим кешировать, логировать и тд.
• Используем декоратор для маппера
• Декортатор - это матрешка: в конструктор первого
декоратора передаем маппер, в конструктор второго
передаем первый декоратор и так далее
• Декоратор перехватывает "интересующие" его методы,
и изменяет результат на ему угодный, остальные
методы просто пропускает дальше.
• При инициализации маппера маппер-менеджер
спрашивает у маппера, хочет ли он
задекорироваться и оборачивает
во все указанные декоратры
27. Примеры декораторов
Cache
• fetchOne(), fetchAll() - на основании переданного условия
берет данные из кеша, или же просит маппер выполнить
запрос и кеширует его результат.
• create(), update(), delete() - сбрасывает соответсвующий
кеш.
Profiler
• Декоратор пропускает все запросы через себя,
записывая в лог время выполнения запроса.
Identity Map
• Кеширует результаты в памяти, чтобы маппер не
выполнял одинаковые запросы дважды
28. Отношения
Раз уж строим ORM, то должны быть отношения между
сущностями.
• Отношения так же, как и поля, задаются в DocBlock
• Параметры описываются в спец формате
• При создании модели, создаются объекты-менеджеры
отношений
• При обращении к полю, ссылающемуся на внешнюю
сущность, объект-менеджер отношения делает запрос к
внешнему мапперу и возвращает полученый объект.
29. Пример работы с отношениями
/**
* ...
* @property integer $authorId
* @property Model_Author $author
[relation=belongsTo;localKey=authorId]
*/
class Model_Post extends Geometria_Model
{...}
$post = Model_Post::getMapper()->fetchOne(array('authorId'
=> 5));
$author = $post->author; // Model_Author
Менеджер отношения в данном случае выполнит запрос
Model_Author::getMapper()->fetchOne(array('id' => 5));
30. Виды отношений
• hasOne - one-to-one отношение
• belongsTo - тоже что и hasOne, но требует
обязательного наличия объекта
• hasMany - one-to-many отношение
31. Полиморфические связи
Обеспечивают связь с несколькими видами сущностей, то
на какой тип сущности стоит ссылка определяет параметр
ownerType, в то время как параметр ownerTypeId
определяет id сущности.
/**
* @property string $ownerType
* @property integer $ownerId
* @property Model_User|Model_Post $owner
[relation=polymorhic; localKey=ownerType;
localTypeKey=ownerId]
*/
class Model_Comment extends Geometria_Model
{..}
32. Тонкости отношений
$posts = Model_Post::getMapper()->fetchAll();
foreach ($posts as $post) {
echo $post->title . ' by ' . $post->author;
}
Автор запрашивается при каждой итерации.
Если у нас 10 постов, значит мы сделаем 1 запрос на
получение постов и 10 запросов на получение авторов.
Итого 11 запросов - плохо!
34. Тонкости отношений
А если у автора есть связь с картинкой-аватаркой?
$posts->fetchRelations('author', 'picture');
Что означает, что перед тем, как "распихать" всех авторов
по постам, у полученной коллекции авторов будет
вызван метод:
$authors->fetchRelations('picture');
35. Каскадные операции
У отношений можно прописать действие, которое будет
выполняться при удалении модели onDelete:
• CASCADE - удалить все связанные зависимые модели
• SET NULL - очистить значения внешних ключей
Это позволяет сохранять целостность связей внутри
нашей системы.
36. Жизнь без Join'ов
Как сделать выборку постов, написанных женщинами, если
посты используют одно хранилище, а авторы другое, и нет
возможности сделать join?
Использовать sphinx.
• Создаем индекс в сфинксе для такого рода выборки.
• Индексируем данные.
• Создаем sphinx декоратор
• Декоратор ищет id документов, удовлетворяющих
поисковому запросу. И по этом списку id маппер
возвращает коллекцию с результатом.
37. Что дало внедрение ORM
• Существенное ускорение разработки
• Время вхождения в чужой код значительно
уменьшилось
• Использование Domain Driven Design позволяет
говорить на языке предметной области, что повышает
читаемость кода
• Логика приложения вынесена в отдельный слой
сервисов. Что позволяет использовать ее не только в
MVC, но и в CLI, например.
• Размер экшенов в контроллерах сократился до 10 строк.