Download - Толстая модель. История разработки ORM
Толстая модельИстория разработки ORM
Шамин МихаилGeometria.ruВедущий разработчик
Geometria.ru
• Главный фотохроникер страны• 8 лет на рынке• Представительство в 150 городах России, СНГ и
Прибалтики • Ежедневно 80 000 пользователей / 600 000 просмотров• В понедельник 110 000 / 1 000 000• Более 500 000 репортажей• 15 000 000 фотографий • 800 000 зарегистрированных пользователей
Почему понадобился свой ORM
Было • Наследство в виде залежей кода-лапши• Практически вся бизнес-логика в контроллерах• Вплоть до формирования select ов!• Некоторые экшены размером в 200 строк! • В роли модели - Zend_Db_Table
Почему понадобился свой ORM
Стало • Стали использовать NoSQL решения, такие как Redis и
Mongo• Понадобилось решение, готовое работать с любым
хранилищем, а не только SQL• Есть ли что-то на рынке? • Doctrine2 в alphа, еще сырая - страшно.• Что делать?• Пишем свой велосипед!
Выбор дизайна
Открываем книгу
Мартина Фаулера(Martin Fowler)
"Шаблоны корпоративных приложений"
("Patterns of Enterprise Application Architecture")
Выбор дизайна
И находим то что нужно.
Domain Modelили
Модель предметной области
Поля модели. Как задавать ?
• В Zend_Db_Table_Row поля не прописаны явно, а берутся из схемы таблицы БД
• В Doctrine2 через задание private/protected свойств и
генерацию getter/setter методов.
Поля модели. Решение:
Использовать DocBlock Профиты: • Готовый шаблон для типов данных • Сразу в аннотации класса видны все поля модели• Автокомплит в IDE (Zend Studio, PhpStorm, NetBeans)• Быстрое создание классов
Zend_Reflection для генерации полей
/** * @property integer $id * @property string $title * @property string $body * @property boolean $hidden * @property integer $date **/class Model_Post extends Geometria_Model{}
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, );}
Доступ к полям
• Внешний доступ к полям обеспечивается через магические методы __get() и __set()
• Можно реализовать методы get<поле> и set<поле>,
чтобы изменить логику установки/получения значения поля.
/** * ... * @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;}
Установка значения по умолчанию
class Model_Post extends Geometria_Model...public function getDate($value){ if (null === $this->_data['date']) { $this->_data['date'] = time(); } return $this->_data['date'];}
Как хранить модель?
Используем DataMapper
• Маппер знает все о модели и о том, как и где её хранить.
• Модель ничего не знает о хранилище.• Логика домена отделена от persist логики • Можно менять структуру бд или даже сменить
хранилище, не меняя логику модели, всего лишь изменив маппер.
• Маппер выполняет CRUD операции• Можно использовать любое хранилище: MySQL, Mongo,
Redis, Config file, RESTApi и др.
Интерфейс маппера
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);}
Работа с моделью
$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
Выборки
• Условие $cond - простой массивимя поля => значение
• Сортировка $sort - тоже просто массивимя поля => (bool) направление сортировки
• Для более сложных выборок пишем отдельный метод Выбрать 10 скрытых постов, начиная с самых новых
$mapper->fetchAll( array('hidden' => true), array('date' => false), 10 );
Делаем ActiveRecord
Рассказываем модели, что у нее есть маппер. • Делаем статический метод getMapper() который из
специального контейнера Geometria_Model_Mapper_Manager достает нужный ей маппер
• Делаем у модели методы create(), update(), delete()
public function create(){ return self::getMapper()->create($this);}
Теперь создание модели выглядит так:$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);
Что вернет fetchAll()? Коллекцию!
• аналог Zend_Db_Table_Rowset • Паттерн Record Set• Позволяет выполнять массовые действия с набором
моделей interface Geometria_Model_Collection_Interfaceextends 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();}
Нужен Paginator?class Geometria_Paginator_Adapter_Mapper implements Zend_Paginator_Adapter_Interface{ public function __construct( Geometria_Model_Mapper_Interface $mapper, array $cond = null, array $sort = null ) { $this->_mapper = $mapper; $this->_cond = $cond; $this->_sort = $sort; }}
Нужен 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 ); }
Хотим кешировать, логировать и тд.• Используем декоратор для маппера• Декортатор - это матрешка: в конструктор первого
декоратора передаем маппер, в конструктор второго передаем первый декоратор и так далее
• Декоратор перехватывает "интересующие" его методы, и изменяет результат на ему угодный, остальные методы просто пропускает дальше.
• При инициализации маппера маппер-менеджер спрашивает у маппера, хочет ли онзадекорироваться и оборачиваетво все указанные декоратры
Примеры декораторовCache• fetchOne(), fetchAll() - на основании переданного условия
берет данные из кеша, или же просит маппер выполнить запрос и кеширует его результат.
• create(), update(), delete() - сбрасывает соответсвующий кеш.
Profiler• Декоратор пропускает все запросы через себя,
записывая в лог время выполнения запроса. Identity Map• Кеширует результаты в памяти, чтобы маппер не
выполнял одинаковые запросы дважды
Отношения
Раз уж строим ORM, то должны быть отношения между сущностями. • Отношения так же, как и поля, задаются в DocBlock• Параметры описываются в спец формате• При создании модели, создаются объекты-менеджеры
отношений• При обращении к полю, ссылающемуся на внешнюю
сущность, объект-менеджер отношения делает запрос к внешнему мапперу и возвращает полученый объект.
Пример работы с отношениями/** * ... * @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));
Виды отношений
• hasOne - one-to-one отношение• belongsTo - тоже что и hasOne, но требует
обязательного наличия объекта• hasMany - one-to-many отношение
Полиморфические связи
Обеспечивают связь с несколькими видами сущностей, то на какой тип сущности стоит ссылка определяет параметр ownerType, в то время как параметр ownerTypeId определяет id сущности. /** * @property string $ownerType * @property integer $ownerId * @property Model_User|Model_Post $owner [relation=polymorhic; localKey=ownerId; localTypeKey=ownerType] */class Model_Comment extends Geometria_Model{..}
Тонкости отношений
$posts = Model_Post::getMapper()->fetchAll();
foreach ($posts as $post) { echo $post->title . ' by ' . $post->author;}
Автор запрашивается при каждой итерации.
Если у нас 10 постов, значит мы сделаем 1 запрос на получение постов и 10 запросов на получение авторов.Итого 11 запросов - плохо!
Тонкости отношений
Решение:
$posts->fetchRelations('author');
Просим relation-manager получить всех авторов одним запросом и проставить во всех постах коллекции. Итого: 2 запроса, независимо от количества постов.
Тонкости отношений
А если у автора есть связь с картинкой-аватаркой? $posts->fetchRelations('author', 'picture'); Что означает, что перед тем, как "распихать" всех авторов по постам, у полученной коллекции авторов будет вызван метод: $authors->fetchRelations('picture');
Каскадные операции
У отношений можно прописать действие, которое будет выполняться при удалении модели onDelete:• CASCADE - удалить все связанные зависимые модели• SET NULL - очистить значения внешних ключей
Это позволяет сохранять целостность связей внутри нашей системы.
Жизнь без Join'ов
Как сделать выборку постов, написанных женщинами, если посты используют одно хранилище, а авторы другое, и нет возможности сделать join? Использовать sphinx. • Создаем индекс в сфинксе для такого рода выборки.• Индексируем данные.• Создаем sphinx декоратор• Декоратор ищет id документов, удовлетворяющих
поисковому запросу. И по этом списку id маппер возвращает коллекцию с результатом.
Что дало внедрение ORM
• Существенное ускорение разработки• Время вхождения в чужой код значительно
уменьшилось• Использование Domain Driven Design позволяет
говорить на языке предметной области, что повышает читаемость кода
• Логика приложения вынесена в отдельный слой сервисов. Что позволяет использовать ее не только в MVC, но и в CLI, например.
• Размер экшенов в контроллерах сократился до 10 строк.
Будет ли open source?
Будет, но позже )
Спасибо
Twitter: @munk13 МойКруг: http://munkie.moikrug.ru