Active Record против Data Mapper-а для сохранения данных

Эти 2 шаблона проектирования описаны в книге Мартина Фаулера «Шаблоны корпоративных приложений» и представляют собой способы работы с сохранением данных в объектно-ориентированном программировании.

Пример шаблона Active Record


class Foo
{
    protected $db;
    public $id;
    public $bar;
     
    public function __construct(PDO $db)
    {
        $this->db = $db;
    }
 
    public function do_something()
    {
        $this->bar .= uniqid();
    }
 
    public function save()
    {
        if ($this->id) {
            $sql = "UPDATE foo SET bar = :bar WHERE id = :id";
            $statement = $this->db->prepare($sql);
            $statement->bindParam("bar", $this->bar);
            $statement->bindParam("id", $this->id);
            $statement->execute();
        }
        else {
            $sql = "INSERT INTO foo (bar) VALUES (:bar)";
            $statement = $this->db->prepare($sql);
            $statement->bindParam("bar", $this->bar);
            $statement->execute();
            $this->id = $this->db->lastInsertId();
        }
    }
}
 
//Insert
$foo = new Foo($db);
$foo->bar = 'baz';
$foo->save();

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

Преимущества Active Record


  • Писать код с Active Record получается быстро и легко, в том случае, когда свойства объекта прямо соотносятся с колонками в базе данных.
  • Сохранение происходит в одном месте, что позволяет легко изучить, как это работает.

Недостатки Active Record


  • Модели Active Record нарушаю принципы SOLID. В частности, принцип единой ответственности (SRP — «S» в принципах SOLID). Согласно принципу, доменный объект должен иметь только одну зону ответственности, то есть только свою бизнес-логику. Вызывая его для сохранения данных, вы добавляете ему дополнительную зону ответственности, увеличивая сложность объекта, что усложняет его поддержку и тестирование.
  • Реализации сохранения данных тесно связана с бизнес-логикой, а это означает, что если вы позже захотите использовать другую абстракцию для сохранения данных (например для хранения данных в XML-файле, а не в базе данных), то вам придется делать рефакторинг кода.

Пример Data Mapper-а


class Foo
{
    public $id;
    public $bar;
 
    public function do_something()
    {
        $this->bar .= uniqid();
    }
}
 
class FooMapper
{
    protected $db;
 
    public function __construct(PDO $db)
    {
        $this->db = $db;
    }
    public function saveFoo(Foo &$foo)
    {
        if ($foo->id) {
            $sql = "UPDATE foo SET bar = :bar WHERE id = :id";
            $statement = $this->db->prepare($sql);
            $statement->bindParam("bar", $foo->bar);
            $statement->bindParam("id", $foo->id);
            $statement->execute();
        }
        else {
            $sql = "INSERT INTO foo (bar) VALUES (:bar)";
            $statement = $this->db->prepare($sql);
            $statement->bindParam("bar", $foo->bar);
            $statement->execute();
            $foo->id = $this->db->lastInsertId();
        }
    }
}
 
//Insert
$foo = new Foo();
$foo->bar = 'baz';
$mapper = new FooMapper($db);
$mapper->saveFoo($foo);

В данном случае, класс Foo намного проще и должен беспокоиться только о своей бизнес-логике. Он не только не должен сохранять собственные данные, он даже не знает и не заботится о том, все ли его данные были сохранены.

Преимущества Data Mapper-а


  • Каждый объект имеет свою зону ответственности, тем самым следую принципам SOLID и сохраняя каждый объект простым и по существу.
  • Бизнес-логика и сохранение данных связаны слабо, и если вы хотите сохранять данные в XML-файл или какой-нибудь другой формат, вы можете просто написать новый Mapper, не притрагиваясь к доменному объекту.

Недостатки Data Mapper-а


  • Вам придется гораздо больше думать, перед тем как написать код.
  • В итоге у вас больше объектов в управлении, что немного усложняет код и его отладку.

Сервис-объекты


При использовании шаблона проектирования Data Mapper, вызывающий код должен выбрать Mapper и бизнес-объект и связать их вместе. Если это код вызова в контроллере, то в конечном счете ваша модель «утекает» в контроллер, что может вызвать большие проблемы при поддержке и юнит-тестировании. Эта проблема может быть решена путем введения объекта-сервиса. Сервис является воротами между контроллером и моделью и связывает доменный объект с Mapper-ом по мере необходимости.

Следует помнить, что M в MVC, представляет собой слой абстракции модели, а не объект модели. Так может быть несколько типов объектов в одной модели (в приведенном выше примере, у вас может быть объект сервиса, доменный объект и объект Mapper-а, выступающие в роли единой модели). С другой стороны, если вы используете модели Active Record, ваша модель может быть представлена лишь одним объектом.

Варианты использования


Объекты Active Record исторически были очень популярны из-за того, что они проще, легче в понимании и быстрее в написании, поэтому многие фреймворки и ORM используют Active Record по умолчанию.

Если вы уверены, что вам никогда не понадобиться менять слой сохранения данных (если вы имеете дело с объектом, который представляет из себя INI-файл, например), или вы имеете дело с очень простыми объектами, в которых не так много бизнес-логики, или просто предпочитаете держать все в небольшом количестве классов, тогда шаблон Active Record это то, что вам нужно.

Использование Data Mapper-а хотя и ведет к более чистому, простому в тестировании и поддержке коду, и обеспечивает большую гибкость, — цена этому, — повышение сложности. Если вы еще не пробовали его использовать, то дайте ему шанс, — вам должно понравиться.

Это перевод статьи Рассела Волкера.
Метки:
Поделиться публикацией
Похожие публикации
Комментарии 62
  • –1
    А почему топик не обозначить как «перевод»?

    Во вторых выводы какие-то оч странные. Как будто это мы сами всегда пишем реализации ОРМ.
    Если мы работаем с Доктриной, мы вряд ли будем дописывать к ней слой ActiveRecord, работая с ОРМ в других фреймворках (Laravel, Yii), дописывай сколько угодно, а обьект всё равно будет знать как себя сохранить, что совершенно нивелирует паттерн датамаппер. О чем эта статья? Репозитории — это хорошо, ооокей?

    — Я кстати подозреваю что статью писал ларавельщик. У них очень печально с аргументацией зачем нужен датамаппер и почему он лучше.
    • 0
      Ну плюсы/минусы спорные, наверное, вы правы.
      Мне больше была интересна часть про их принципиальные отличия, ибо я очень слабо их различал.
      Я не нашел как обозначить топик переводом, у меня есть только вариант «Обучающий материал» или я просто не там ищю, подскажите?
      • 0
        Как-то так: minus.com/l4aPf83jiwIIZ

        Да, если что я плюсик вашему посту поставил. А мои претензии к автору оригинального поста. Обзор паттернов это хорошо, но «нужно юзать репозиторий, потому что это модно» — явно не аргумент. Хотелось бы, чтобы в РНР сообществе прекратили делить паттерны на модные/не модные.
        • +1
          Спасибо, но у меня этого нет, наверное потому что пост перекочевал из песочницы.
          Да я то с вами согласен, я вообще против фанатизма любого рода, надо как-то больше на задачи ориентироваться, — где-то data mapper, где-то active record использовать, а где-то и вообще PDO и SQL хватит и не нужно усложнять.
          • +1
            Здесь не вопрос Модно-Немодно.
            Выбор конкретного подхода Active Record vs Data Mappers + Repository должен вытекать из бизнес задач.
            Дело в том, что Active Record является фактический неотъемлемой частью RAD (rapid application development), и цель его быстро развернуть уровень доменных моделей (читай бизнес логики) по существующей схеме базы (как правило, небольшой). Фактически все RAD проекты строятся по принципу «Database First» — т.е. мы вначале проектируем схему БД, как-то ее декларируем (Json, Yaml, SQL, ini) и через эту декларацию фреймворк генерирует нам доменные модели с уже реализованым CRUD функционалом через механизм scaffolding-a.
            Это хорошо работает на маленьких-средних проектах с небольшими зависимостями. Если же проект большой и людей, которые работают на нем много, и чтобы поддерживать все в рабочем состоянии эти люди хотят покрывать код тестами, то тут начинаются проблемы…
            Поэтому в последнее время такую большую популярность и обрел DDD (Domain driven design), который декларирует одним из своих основных принципов persistence ignorance и «Code First». При этому мы получаем все те плюшки, которые автор описал в статье (SOLID, доменные модели не знают вообще, что их куда-то сохраняют, поэтому становятся тонкими и содержат только бизнес логику). Развивая идею дальше мы приходим к тому, что в таком виде можно легко реализовать Unit of Work паттерн, и разгрузить слой приложения (читай контроллеров) от обязанностей сохранять и контролировать что модели сохраняются и обновляются корректно. А эта обязанность переходит в инфраструктурный уровень. Но при этом, да, сложность проекта становится выше.
            • 0
              Ваш комментарий стоит всего этого поста. Если бы тот же уровень аргументации был в основном посте… Но увы, в самом посте нет никаких намеков на то что автор хорошо понимает оба паттерны и все вытекающие. «дайте ему шанс, — вам должно понравиться.» звучит как «попробуй, это модно в нынешнем сезоне!»
      • +1
        Недостатки DataMapper надуманные.
        Дольше думать над чем? Управление сущностями ничем не отличается, кроме $dm->save($user); $user->save(); Но это на мой взгляд вообще не отличие. Ровно как и преимущество DM над AR в том что невозможно измнить mapper, просто не надо хардкодить PDO в конструкторе вот и все. На основе той же доктрины можно сделать AR, они сами описали в кукбуке этот момент.
        • 0
          В целом я с вами согласен, что при использовании, по большому счету, все равно, у какого объекта вы save() вызываете. Но все же, если не рассматривать экзотические варианты, вроде AR поверх Doctrinы, то как правило, DM — получается более сложной абстракцией со всеми вытекающими последствиями. Но, наверное, это для вас будет иметь значение только, если вы разработчик, а не пользователь.
          • 0
            Не понимаю какие вытекающие, если честно)
            • 0
              Уровень абстракции повышается, сложность стало быть. Но это больше разработчиков касается конечно, чем пользователей.
              • 0
                Сложность бывает разная. Имхо, десять простых классов поддерживать намного проще, чем один «божественный».
                • 0
                  Я согласен, что в конечном счете все от реализации зависит, но более высокий уровень абстракции так или иначе повышает, скажем так, базовую сложность.
                  • +1
                    Все проблемы программирования можно решить дополнительным слоем абстракции… кроме проблемы избыточной абстракции ©
                    • 0
                      да, это я и имел в виду)
        • –1
          У Active Record постоянно возникают проблемы при сериализации, так как подключение к Базе данных сериализовать нельзя. Это одна из кючевых аргументов против Active Record. При этом, если подключение не сериализовать (__sleep, __wakeup), то нужно далее вручную его подсунуть, а при условии использования ServiceLocator'ов это становится проблематичным.
          • +1
            оО вот уж не думал что это проблема. Зачем тогда придумали интерфейс Serializable? Просто раз уж AR — кеширование должно быть через сам объект статическим методом
            • 0
              Есть ситуация, нужно Entity засунуть в Сессию (Entity юзверя залогиненного).
              Если использовать Active Record, то нужно при сериализации убрать оттуда подключение к БД.
              При восстановлении из сериализованных данных, соединения в User Entity нет. Его можно взять из ServiceLocator'а, но в момент восстановления данных ServiceLocator неоткуда получить (Singleton — не предлагать).
              В итоге получается ситуация, что соединение засовывается вручную, и это нужно постоянно контролировать.
              • 0
                А в чем проблема инжектить в AR-сущность кешер и загружать из кеша?
                • –2
                  А зачем танцевать с бубном, если можно задачу просто облегчить? У Entity есть Аннотации, по которым можно спокойно определить, и что за таблица, и управляющий Репозиторий.
                  Рекомендую почитать про Doctrine ORM, и причины почему Symfony сделала по дефолту (вместо Prorel) именно Doctrine.

                  Конечно в реальных проектах Entity Manager у Doctrine ORM очень прожорливый (ну уж очень), но на основе Аннотаций можно быстро написать свою ORM, с блекджеком и…
                  • +1
                    Я целиком и полностью за дата-мэппер, и юзаю доктрину) Но я все это пишу к тому, что AR по этим аргументам никак не проигрывает DM.
                    DM на мой взгляд идеологически более правильна, но к сожалению доктрина тяжела и потому думаю не особо распространена.
                    • 0
                      Рекомендую почитать про Doctrine ORM, и причины почему Symfony сделала по дефолту (вместо Prorel) именно Doctrine.

                      Насколько я помню, то тут есть нюансы:
                      * пропел давным давно как мертв (ок, ещё немнго трепыхается, но не суть)
                      * это произошло ещё в версии 1.2 симфони, когда была доктрина 1.х с реализацией ActiveRecord.
                      * доктрина единственная серьезная ОРМ которую можно юзать без привязки к фреймворку.
                      • 0
                        есть propel 2 кстати)
                        • 0
                          Уже два года как, или даже больше…
                          Но релиза как не было так и нет.
                        • 0
                          Propel 2 легко можно использовать.
                        • +1
                          Что мешает использовать аннотации в Active Record? Что мешает в Active Record назвать таблицу репозиторием?

                          Имхо, вы совсем не понимаете сути data mapper'а. Она не в синтаксическом сахаре, она в отделении данных от логики их сохранения. Более того, она вообще в правильном использовании ООП.

                          И плюсы её очевидны (но совсем не те, про которые писали вы) — гибкость и разделение ответственности.
                      • 0
                        В Yii в Active Record есть метод, возвращающий имя таблицы, и при желании можно указать префикс, обозначающий коннект, который обеспечивает доступ к источнику

                        class User {
                            function table() { return 'db1.user'; }
                        }
                        

                        «table» не сериализуется, он же метод
                        При восстановлении объекта коннект восстанавливается лениво через локатор Yii::app()->db1 только при чтении или записи объекта

                        При тестировании в локатор можно подсунуть настройки тестовой базы или мок

                        Я думаю, это достаточно гибко
                  • 0
                    В книге на самом деле 4 шаблона, ну да ладно… Касательно DataMapper лучше всего добавить в объект поле для хранения ссылки на экземпляр мапера, что позволит использовать просто $object->save() и не задумываться о том как был создан объект (эта связь все равно понадобится если захочется реализовать прозрачную выборку связанных объектов)

                    Давно мучает небольшой вопрос: а какая библиотека позволяет отслеживать изменение свойств объектов и динамически менять связанные объекты? (см код)

                    // Объект
                    $object = new MyObject([id = 2, parent_id = 1]);
                    
                    // Родительский объект (ленивая загрузка связанного объекта)
                    $parent = $object->parent; // MyObject([id = 1, parent_id = null]);
                    
                    // Дочерние объекты (ленивая загрузка связанного объекта)
                    $childs = $parent->childs; // [MyObject([id = 2, parent_id = 1]), MyObject([id = 4, parent_id = 1])]
                    
                    // "Магия"
                    $object->parent_id = 3;               
                    // $object->parent === MyObject([id = 3]) - новый родитель
                    // $parent->childs === [MyObject([id = 4, parent_id = 1])] - объект удалился у предыдущего родителя
                    
                    $object->parent    = new MyObject([id = 3]);
                    // $object->parent_id === 3
                    // $parent->childs    === [MyObject([id = 4, parent_id = 1])]
                    
                    • 0
                      Вы опять спустились до уровня SQL. ID — это свойство сущности, но вы работаете с объектами. Доктрина позволяет делать так:
                      $user = new User();
                      $user->setFriend($em->find(3, 'Blabla\User'));
                      

                      В данном случае на место друга встанет прокси объект с единственным определенным свойством — id:3, до тех пор пока вы не запросите какие-то свойства друга — ничего из базы загружено не будет. Вроде как можно даже что-то поменять и опять же грузиться ничего не должно, только обновится при $em->flush()
                      • 0
                        Нет, скорее перешел к полноценному ООП: если меняются свойство то это изменение глобально и действует сразу, без всякого $em->flush() и подобных. Т.е. если меняем $object->parent то в $object->parent->childs должен сразу добавиться $object*

                        * — понятно что это возможно только если все объекты хранятся в памяти, но если их немного это не проблема.
                        • 0
                          В ООП вы управляете объектами. Представим что вы — объект, человек. От того что я у вас номер на машине поменял — у вас машина тут же не обновилась до соответсвующего номера? Более того невозможно проверить существует ли машина с таким номером без запроса в базу.
                          ID — это то, что связывает сущность внутри приложения с строкой в таблице в базе. Это нельзя менять вот так вот просто
                          • 0
                            Изначально $object->parent->childs содержит какой то набор объектов («первый набор»), меняем $object->parent и… и ничего, $object->parent->childs по прежнему содержит «первый набор», хотя в реальности этот набор уже изменился — в этом то и проблема (обычно она решается ручным релодом $object->parent->childs, что неудобно и не логично)
                            • 0
                              Ничего не понял, можно в коде пример привести? Вы имеете ввиду что $user->setGroup($group) а потом нужно $group->addUser($user)?
                              • 0
                                Вы имеете ввиду что $user->setGroup($group) а потом нужно $group->addUser($user)?

                                Именно (только вот это $group->addUser($user) должно быть полностью прозрачно).
                                • 0
                                  • 0
                                    Похоже на то что нужно, но немного непонятно: будут ли обновлены объекты до вызова flush() в примере с OneToMany?
                                    • 0
                                      Эта ваша ответственность следить за правильностью ссылок из объектов друг на друга. Если ваш код поддерживает это (т.е в коде метода addUser вы устанавливаете $user группу), то никаких проблем с Doctrine у вас быть не должно.
                                      • 0
                                        Зачем делать вручную что-то что может быть автоматизировано? ;) И потом, это очень удобно и позволяет полностью забыть о самом мапере и просто использовать нужные объекты со всегда актуальными свойствами.
                                        • 0
                                          В Doctrine они и так будут актуальными, если вы правильно устанавливаете связи между объектами, как написали вам здесь.
                            • 0
                              Это особенности реализации, а не свойство паттернов. Было бы прекрасно, если бы все можно было вот просто так взять и поменять — проблема в том, что это очень сложно реализовать.
                            • 0
                              Парадигма ООП не вводит понятие аспекта.
                              В данном случае вы пытаетесь ввести аспект связей(древовидной) объектов и сохранить его целостность.
                              А это, увы, уже задача бизнес-логики.
                              • 0
                                Возможно, но при наличии данных об этих связях целостность может быть обеспечена автоматически, что гораздо удобнее чем везде обеспечивать её вручную.
                                • 0
                                  ООП не про связи объектов, так же как не про здоровье программистов. Это лишь парадигма программирования, в которой чётко указаны термины и теоремы.
                                  • 0
                                    Скорее не только про (или aгрегация тоже не относится к ООП?)
                                    • 0
                                      Агрегация — это не связь объектов, это отношения классов.
                                      • 0
                                        Эти отношения классов, являются способом реализации полиморфизма.
                                        • 0
                                          Как раз таки связь (один объект вложен в другой).
                                          • 0
                                            Не надо смешивать семантическую связь и механизмы отношений.
                                          • 0
                                            А чем связь отличается от отношения в контексте ООП?
                                            • 0
                                              Связь — семантические отношения между экземплярами классов, другими словами это уже некие отношения основанные на значении класса для программиста в контексте задачи, а не в контексте теории ООП.
                                              Агрегация, Композиция и Наследование — отношения между объектами/классами в контексте ООП, без учета семантики.
                                  • 0
                                    public function setParent($parent)
                                    {
                                        $this->parent->removeChild($this);
                                        $parent->addChild($this);
                                        $this->parent = $parent;
                                    }
                                    


                                    А у вас не ООП, а каша. С таким уровнем «магии» ваш проект уже завтра станет неподдериваемой шляпой.
                                    • 0
                                      Т.е. вы всерьез хотите сказать что неактуальность текущей иерархии объектов это правильный ООП подход? :) Ну как хотите, если нравиться самому синхронизировать все объекты — синхронизируйте, кто ж запрещает…
                                      • 0
                                        Опять же, не смешивайте ООП и целостность связей объектов бизнес-логики.
                                        ООП не про это.
                                  • 0
                                    Встанет таки полный объект, для прокси нужно использовать em->getReference().
                                • 0
                                  Чем в приведённом вами примере DataMapper отличается от TableGateway?
                                  • 0
                                    На сколько я понимаю, Table Gateway объект является Data Mapper объектом, но не наоборот.
                                    • 0
                                      Table Gateway — это частный случай, в данном случае он и есть. Data Mapper — более общая вещь, он мапит на что угодно — совершенно необязательно в целевом хранилище вообще есть понятие таблицы.
                                      • 0
                                        Ну это я понял, спасибо :)
                                        Я как бы намекал, что в статье, по сути, сравнивается Active Record и Table Gateway.
                                        Тема Data Mapper'а раскрыта не полностью.
                                        • 0
                                          На мой взгляд в статье лишь сравниваются два подхода. Не стояла задача полностью рассказать о каком либо из них.
                                    • 0
                                      Не упомянут важная фича DataMapper'a — он тесно привязан к соответствующей сущности, и это увеличивает связанность (coupling), в то время как сущность, сама себя сохраняющая, наоборот, обладает более высокой связностью (cohesion). С другой стороны, мапперы примитивны, лежат в отдельном слое, друг с другом не связаны, и модифицировать их достаточно просто.
                                      • 0
                                        Недостатки: Вам придется гораздо больше думать, перед тем как написать код.
                                        я бы не назвал это недостатком
                                        • 0
                                          Сейчас почти все AR позволяют легко менять сторадж, за это ответственен DBAL. Даже в примерах их статье видно, что для DM оказывается так же зависим от PDO.
                                          • +2
                                            Гм. Это всё прекрасно, но вот у меня например возникает такой вопрос: а что думает DataMapper о прозрачной массовой ленивой подгрузке данных для коллекции объектов?
                                            Тут правда вопрос, что об этом думают и существующие AR?
                                            Сам подход — это я у себя обычно применяю, идея его в том, что объект помнит, частью какой коллекции является, и при обращении к геттеру свойства, требующего подгрузки данных из БД, подгружает и прописывает свойство сразу для всех объектов в «своей» коллекции.
                                            Получается, что и писать удобно (не надо явно просить подгрузку свойств), и работает быстро — и лениво, и никогда нет сотни селектов в цикле, отдельных для каждого объекта.

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