39
Толстая модель История разработки ORM Шамин Михаил Geometria.ru Ведущий разработчик

Толстая модель. История разработки ORM

Embed Size (px)

DESCRIPTION

Доклад на ZFConf2011

Citation preview

Page 1: Толстая модель. История разработки ORM

Толстая модельИстория разработки ORM

Шамин МихаилGeometria.ruВедущий разработчик

Page 2: Толстая модель. История разработки ORM
Page 3: Толстая модель. История разработки ORM

Geometria.ru

• Главный фотохроникер страны• 8 лет на рынке• Представительство в 150 городах России, СНГ и

Прибалтики • Ежедневно 80 000 пользователей / 600 000 просмотров• В понедельник 110 000 / 1 000 000• Более 500 000 репортажей• 15 000 000 фотографий • 800 000 зарегистрированных пользователей

Page 4: Толстая модель. История разработки ORM
Page 5: Толстая модель. История разработки ORM

Почему понадобился свой ORM

Было  • Наследство в виде залежей кода-лапши• Практически вся бизнес-логика в контроллерах• Вплоть до формирования select ов!• Некоторые экшены размером в 200 строк! • В роли модели - Zend_Db_Table

Page 6: Толстая модель. История разработки ORM

Почему понадобился свой ORM

Стало • Стали использовать NoSQL решения, такие как Redis и

Mongo• Понадобилось решение, готовое работать с любым

хранилищем, а не только SQL• Есть ли что-то на рынке? • Doctrine2 в alphа, еще сырая - страшно.• Что делать?• Пишем свой велосипед!

 

Page 7: Толстая модель. История разработки ORM

Выбор дизайна

Открываем книгу 

Мартина Фаулера(Martin Fowler)

 "Шаблоны корпоративных приложений"

  ("Patterns of Enterprise Application Architecture") 

Page 8: Толстая модель. История разработки ORM
Page 9: Толстая модель. История разработки ORM

Выбор дизайна

И находим то что нужно. 

Domain Modelили

Модель предметной области

Page 10: Толстая модель. История разработки ORM

Поля модели. Как задавать ?

• В Zend_Db_Table_Row поля не прописаны явно, а берутся из схемы таблицы БД

 • В Doctrine2 через задание private/protected свойств и

генерацию getter/setter методов.

Page 11: Толстая модель. История разработки ORM

Поля модели. Решение:

Использовать DocBlock Профиты:  • Готовый шаблон для типов данных • Сразу в аннотации класса видны все поля модели• Автокомплит в IDE (Zend Studio, PhpStorm, NetBeans)• Быстрое создание классов

 

Page 12: Толстая модель. История разработки ORM

Zend_Reflection для генерации полей

/**  * @property integer $id  * @property string  $title  * @property string  $body  * @property boolean $hidden  * @property integer $date  **/class Model_Post extends Geometria_Model{}

Page 13: Толстая модель. История разработки ORM

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,    );}

Page 14: Толстая модель. История разработки ORM

Доступ к полям

• Внешний доступ к полям обеспечивается через магические методы __get() и __set()

 • Можно реализовать методы get<поле> и set<поле>,

чтобы изменить логику установки/получения значения поля.

Page 15: Толстая модель. История разработки ORM

/**  * ...  * @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;}

Page 16: Толстая модель. История разработки ORM

Установка значения по умолчанию

class Model_Post extends Geometria_Model...public function getDate($value){    if (null === $this->_data['date']) {         $this->_data['date'] = time();    }    return $this->_data['date'];}

Page 17: Толстая модель. История разработки ORM

Как хранить модель?

Используем DataMapper

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

• Модель ничего не знает о хранилище.• Логика домена отделена от persist логики • Можно менять структуру бд или даже сменить

хранилище, не меняя логику модели, всего лишь изменив маппер.

• Маппер выполняет CRUD операции• Можно использовать любое хранилище: MySQL, Mongo,

Redis, Config file, RESTApi и др.

Page 18: Толстая модель. История разработки ORM

Интерфейс маппера

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);}

Page 19: Толстая модель. История разработки ORM

Работа с моделью

$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

Page 20: Толстая модель. История разработки ORM

Выборки

• Условие $cond - простой массивимя поля => значение

• Сортировка $sort - тоже просто массивимя поля => (bool) направление сортировки

• Для более сложных выборок пишем отдельный метод  Выбрать 10 скрытых постов, начиная с самых новых

$mapper->fetchAll(    array('hidden' => true),    array('date' => false),    10 );  

Page 21: Толстая модель. История разработки ORM

Делаем ActiveRecord

Рассказываем модели, что у нее есть маппер.  • Делаем статический метод getMapper() который из

специального контейнера Geometria_Model_Mapper_Manager достает нужный ей маппер

• Делаем у модели методы create(), update(), delete()

public function create(){    return self::getMapper()->create($this);}

Page 22: Толстая модель. История разработки ORM

Теперь создание модели выглядит так:$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);

Page 23: Толстая модель. История разработки ORM

Что вернет 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();}

Page 24: Толстая модель. История разработки ORM

Нужен 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;  }}  

Page 25: Толстая модель. История разработки ORM

Нужен 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    );   }

Page 26: Толстая модель. История разработки ORM

Хотим кешировать, логировать и тд.• Используем декоратор для маппера• Декортатор - это матрешка: в конструктор первого

декоратора передаем маппер, в конструктор второго передаем первый декоратор и так далее

• Декоратор перехватывает "интересующие" его методы, и изменяет результат на ему угодный, остальные методы просто пропускает дальше.

• При инициализации маппера маппер-менеджер спрашивает у маппера, хочет ли онзадекорироваться и оборачиваетво все указанные декоратры

Page 27: Толстая модель. История разработки ORM

Примеры декораторовCache• fetchOne(), fetchAll() - на основании переданного условия

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

• create(), update(), delete() - сбрасывает соответсвующий кеш. 

 Profiler• Декоратор пропускает все запросы через себя,

записывая в лог время выполнения запроса. Identity Map• Кеширует результаты в памяти, чтобы маппер не

выполнял одинаковые запросы дважды

Page 28: Толстая модель. История разработки ORM

Отношения

Раз уж строим ORM, то должны быть отношения между сущностями. • Отношения так же, как и поля, задаются в DocBlock• Параметры описываются в спец формате• При создании модели, создаются объекты-менеджеры

отношений• При обращении к полю, ссылающемуся на внешнюю

сущность, объект-менеджер отношения делает запрос к внешнему мапперу и возвращает полученый объект.

 

 

Page 29: Толстая модель. История разработки ORM

Пример работы с отношениями/**  * ...  * @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));

Page 30: Толстая модель. История разработки ORM

Виды отношений

• hasOne - one-to-one отношение• belongsTo - тоже что и hasOne, но требует

обязательного наличия объекта• hasMany - one-to-many отношение

 

Page 31: Толстая модель. История разработки ORM

Полиморфические связи

Обеспечивают связь с несколькими видами сущностей, то на какой тип сущности стоит ссылка определяет параметр 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{..}

Page 32: Толстая модель. История разработки ORM

Тонкости отношений

$posts = Model_Post::getMapper()->fetchAll();

foreach ($posts as $post) {    echo $post->title . ' by ' . $post->author;}

Автор запрашивается при каждой итерации.

Если у нас 10 постов, значит мы сделаем 1 запрос на получение постов и 10 запросов на получение авторов.Итого 11 запросов - плохо!

Page 33: Толстая модель. История разработки ORM

Тонкости отношений

Решение:

$posts->fetchRelations('author');

Просим relation-manager получить всех авторов одним запросом и проставить во всех постах коллекции. Итого: 2 запроса, независимо от количества постов.

Page 34: Толстая модель. История разработки ORM

Тонкости отношений

А если у автора есть связь с картинкой-аватаркой? $posts->fetchRelations('author', 'picture'); Что означает, что перед тем, как "распихать" всех авторов по постам, у полученной коллекции авторов будет вызван  метод: $authors->fetchRelations('picture');

Page 35: Толстая модель. История разработки ORM

Каскадные операции

У отношений можно прописать действие, которое будет выполняться при удалении модели onDelete:• CASCADE - удалить все связанные зависимые модели• SET NULL - очистить значения внешних ключей

 Это позволяет сохранять целостность связей  внутри нашей системы.

Page 36: Толстая модель. История разработки ORM

Жизнь без Join'ов

Как сделать выборку постов, написанных женщинами, если посты используют одно хранилище, а авторы другое, и нет возможности сделать join? Использовать sphinx. • Создаем индекс в сфинксе для такого рода выборки.• Индексируем данные.• Создаем sphinx декоратор• Декоратор ищет id документов, удовлетворяющих

поисковому запросу. И по этом списку id маппер возвращает коллекцию с результатом.

Page 37: Толстая модель. История разработки ORM

Что дало внедрение ORM

• Существенное ускорение разработки• Время вхождения в чужой код значительно

уменьшилось• Использование Domain Driven Design позволяет

говорить на языке предметной области, что повышает читаемость кода

• Логика приложения вынесена в отдельный слой сервисов. Что позволяет использовать ее не только в MVC, но и в CLI, например.

• Размер экшенов в контроллерах сократился до 10 строк.

Page 38: Толстая модель. История разработки ORM

Будет ли open source?

Будет, но позже )

Page 39: Толстая модель. История разработки ORM

Спасибо

Twitter: @munk13 МойКруг: http://munkie.moikrug.ru