Мы прошли долгий путь, с тех дней когда мы в ручную писали SQL запросы в наших веб приложения. Инструменты, такие как Laravel’ий Eloquent ORM позволяют нам работать с базой данных на более высоком уровне, освобождают нас от деталей более низкого уровня — таких как синтаксис запросов и безопасность.


Когда вы начнете работать с Eloquent, вы неизбежно придете к таким операторам как where и join. Для более продвинутых есть заготовки запросов (scopes), читатели (accessors), мутаторы (mutators) — предлагающие более выразительные альтернативы, старому способу построения запросов.


Давайте рассмотрим другие альтернативы, которые могут быть использованы как замена часто повторяющемуся оператору where и заготовкам запросов (scopes). Эта технология заключается в создание новой модели Eloquent которая будет наследоваться от другой модели. Такая модель будет наследовать весь функционал родительской модели, сохраняя возможность добавлять собственные методы, заготовки запросов (scopes), слушателей (listeners), и т.д. Обычно такое называется "Однотабличное Наследование" (Single Table Inheritance), но я предпочитаю называть это "Модельным Наследованием" (Model Inheritance).


Пример


Большинство веб приложений имеют концепцию "администратор." Администратор это обычный пользователь с повышенными правами и доступом в служебные части приложения. Для того чтобы отличить обычных пользователей от администраторов мы пишем что то подобное:


$admins = User::where('is_admin', true)->get();

Когда выражение where часто повторяется в вашем приложение, его полезно заменить на локальную заготовку запроса (local scope). Внедрив заготовку запроса isAdmin в модель User, мы сможем писать более выразительный и переиспользуемый код:


$admins = User::isAdmin()->get();

// Реализация:
class User extends Model
{
    public function scopeIsAdmin($query)
    {
        $query->where('is_admin', true);
    }
}

Давайте пойдем дальше и используем наследование модели. Наследуясь от модели User и добавляя глобальную заготовку запроса, мы достигаем более аккуратного результата чем получали прежде, но сейчас с совершенно новым объектом. Этот объект (Admin) может иметь собственные методы, заготовки запросов, и другие функциональные возможности.


$admins = Admin::all();

// Реализация:
class Admin extends User
{
    protected $table = 'users';

    public static function boot()
    {
        parent::boot();

        static::addGlobalScope(function ($query) {
            $query->where('is_admin', true);
        });
    }
}

Примечание: переменная protected $table = ‘users’ необходима для правильной работы запросов. Eloquent использует имя класса модели для определения имени таблицы. Следовательно Eloquent предполагает что имя таблицы “admins” вместо “users”, что приведет к ошибке Base table or view not found.

Теперь когда у вас есть модель Admin вам будет проще разделять функциональность с моделью User. Например:


Нотификации


Простые операции, такие как отправка нотификаций всем администраторам, стала проще с новой моделью Admin.


Notification::send(Admin::all(), NewSignUp($user));

Проверка


Всегда когда операции с моделью User ограничиваются администратором, нам требуется проверить что пользователь олицетворяет администратора.


// Проверка
if ($admin = User::find($id)->is_admin !== true) {
      throw new Exception;
}

$admin->impersonate($user);

Так как Admin’ая глобальная заготовка запроса ограничивает нас только администраторами, метод impersonate можно вызывать сразу для класса Admin.


Admin::findOrFail($id)->impersonate($user);

Фабрики моделей


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


$admin = factory(User::class)->create(['is_admin' => true]);

// // Реализация фабрики пользователя
$factory->define(User::class, function () {
    return [
        ...
          'is_admin' => false,
    ];
});

Мы можем улучшить этот код добавив состояние для фабрики инкапусулировав то — что пользователь является администратором.


$admin = factory(User::class)->states('admin')->create();

// Реализация состояния администратора 
$factory->state(User::class, 'admin', function () {
    return ['is_admin' => true];
});

Стало несомненно лучше, но мы по прежнему получаем экземпляр модели User. Определив новую фабрику для модели Admin, мы также получим пользователя с правами администратора, но теперь фабрика будет возвращать экземпляр модели Admin.


$admin = factory(Admin::class)->create();

// Реализация фабрики администратора
$factory->define(Admin::class, function () {
    return ['is_admin' => true]
          + factory(User::class)->raw();
});

Отношения не работают.


Аналогично тому как Eloquent определяет имена таблиц, имена класса моделей используется для для определения внешних ключей и промежуточных таблиц. Следовательно доступ к отношениям из модели Admin проблематичен.


Admin::first()->posts;
// Бросит исключение: Unknown column 'posts.admin_id'

// Не рабочая реализация:
class Admin extends User {
    //
}

class User extends Model {
    public function posts() {
          return $this->hasMany(Post::class);
    }
}

Eloquent не может получить доступ к отношению так как предполагает что каждый экземпляр модели Post имеет поле admin_id вместо поля user_id. Мы можем исправить это передав внешний ключ user_id в модели User:


// Рабочая реализация:
class Admin extends User {
    //
}

class User extends Model {
    public function posts() {
          return $this->hasMany(Post::class, 'user_id');
    }
}

Эта же проблема существует в отношение многие ко многим. Eloquent предполагает что имя промежуточной таблицы соответствует имени текущего класса модели:


Admin::first()->tags;
// Бросает исключение: Table 'admin_tag' doesn't exist

// Не рабочая реализация:
class Admin extends User {
    //
}

class User extends Model {
    public function tags() {
          return $this->belongsToMany(Tag::class);
    }
...

Мы так же мы можем решить эту проблему явно указав имя сводной таблицы и имя удаленного ключа:


// Рабочая реализация:
class Admin extends User {
    //
}

class User extends Model {
    public function tags() {
          return $this->belongsToMany(Tag::class, 'user_tag', 'user_id');
    }
...

Несмотря на то что явное определение удаленных ключей и сводных таблиц позволит модели Admin получить доступ к отношениям модели User, это решение далеко от идеального. Существование этих на вид не нужных определений, не улучшает наш код.


Однако, вы можете создать трейт HasParentModel который автоматически решит эту проблему. Данный трейт заменит имя класса модели на имя класса родительской модели. Код трейта GitHub.


Можно пойти дальше, и заставить Laravel лучше работать с однотабличным наследованием. Мы создали пакет который упростит создание моделей в вашем Laravel приложения, и готовы выпустить его со дня на день. Следите за нашим твитером чтобы не пропустить анонс!

Давайте посмотрим на новую модель Admin которая использует этот трейт:


use App\Abilities\HasParentModel;

class Admin extends User
{
    use HasParentModel;
      // Больше не нужна переменная: protected $table = 'users'

    public static function boot()
    {
        parent::boot();

        static::addGlobalScope(function ($query) {
            $query->where('is_admin', true);
        });
    }
}

Сейчас наши отношения модели User могут вернуться к тому состоянию когда они полагались на значения по умолчанию.


// Рабочая реализация:
class User extends Model
{
    public function posts() {
        return $this->hasMany(Post::class);
    }

    public function tags() {
        return $this->belongsToMany(Tag::class);
    }
}

Трейт HasParentModel очищает нашу модель и дает разработчику понять что что-то особенное происходит внутри нее.


Наследование моделей


Мы выявили общие характеристики Eloquent модели и сделали их чище, используя их наследование. Эта технология позволяет создавать нам более лучшие имена объектов и инкапсулировать их в нашем приложение. Помните что наследование доступно для всех моделей Eloquent'а, не только для Users и Admins. Возможности безграничны!


Творите, получайте удовольствие и делитесь полученными знаниями. Поделитесь со мной, как вы используете этот паттерн в ваших проектах! (Твитер @calebporzio и @tightenco)


Удачи!

Комментарии (13)


  1. maslyaev
    14.12.2017 22:35

    В реальной жизни схемы баз данных — на полстены. Или на всю стену. Как повезёт. И запросы к ним — на сотни строк SQL-кода. Интересно, как в таких бесчеловечных условиях будет работать Eloquent ORM?

    Вообще, конечно, мне кажется, ORM-проблема решения пока что не имеет. Если посмотреть в корень неприятностей (иногда не нужно отказывать себе в удовольствии это делать), то можно заметить, что в основе реляционных БД лежит математика, а конкретно исчисление предикатов, а в основе ООП не лежит ничего кроме интуитивно как-бы понятных, но абсолютно никак не формализованных утверждений, в большинстве своём слепо принимаемых на веру и служащих поводом для безысходных холиваров. ORM — это, по сути, мэппинг математики на лирику. Для того, чтобы одно с другим качественно единообразно поженить, видимо, нужно или положить ООП на математическую основу (за полвека это не удалось, и перспектив не очень видно), или выкинуть из реляционных баз идейную основу и превратить их в болото (NoSQL?).


    1. VolCh
      14.12.2017 23:00

      А что за проблема с ОРМ? ОРМ — это решение проблемы связи объектов на рсубд и обратно.


      1. redfs
        15.12.2017 08:26

        ОРМ — это решение проблемы связи объектов на рсубд и обратно.
        Точная, короткая, но ёмкая формулировка.
        К сожалению многие (включая автора данной статьи) видят в ОРМ просто новый уровень абстракции для замены синтаксиса SQL запросов.
        Мы прошли долгий путь, с тех дней когда мы в ручную писали SQL запросы в наших веб приложения. Инструменты, такие как Laravel’ий Eloquent ORM позволяют нам работать с базой данных на более высоком уровне, освобождают нас от деталей более низкого уровня — таких как синтаксис запросов и безопасность.
        Поэтому понятно непонимание maslyaev
        В реальной жизни схемы баз данных — на полстены. Или на всю стену. Как повезёт. И запросы к ним — на сотни строк SQL-кода. Интересно, как в таких бесчеловечных условиях будет работать Eloquent ORM?
        А никак. Кмк ОРМ предназначен для решения совсем другого класса задач.


        1. VolCh
          15.12.2017 09:43

          Говорим "ORM" подразумеваем "DBAL"? Тогда понятнее.


        1. r-moiseev
          16.12.2017 21:11

          Я вам страшное скажу, ORM вообще не отвечает непосредственно за SQL запросы. Она их мапит на объектную модель.


          Те кто пропагандируют отказ от ORM из за SQL генератора кажется этого не понимают. Никто не запрещает писать запросы в 100 строк чистого SQL и использовать ORM


      1. qRoC
        15.12.2017 09:21

        ОРМ — это решение проблемы связи объектов на рсубд и обратно.

        Никакой связи здесь нет, есть только преобразование. Вы не можете написать:
        $user->withdrawMoney(10);

        т.к. состояние сущности никак не синхронизируется с БД.


        1. VolCh
          15.12.2017 09:42

          Я именно так и пишу и почему-то всё синхронизруется. Наверное, потому, что я об этом позаботился хотя бы одной строчкой кода типа $user->save() (или даже $this->save() в методе User::withdrawMoney() ) при использовании каких-то AR без UoW или типа $om->flush(); при использовании UoW. Причём в последнем случае сохранение можно сделать автоматическим где-то в shutdown обработчике.


          Или вы про конкретные примеры в посте?


          1. qRoC
            15.12.2017 13:22

            Я про то что на момент вызова save() у вас состояние модели может быть неактуальным. И запрос вида

            SET balance=:balance

            в корне неверный. Таким образом нужно вводить некую версионность, и на выходе получать запрос
            SET balance=:balance WHERE version=:version

            что не сработает при высокой активности работы с данной записью в таблице. Корректным решением будет переносом логики в сам запрос:
            balance=balance+:sum WHERE balance+:sum>0

            но тогда ORM у нас используется для стартового преобразования БД->КОД. По этой причине я и сказал что ORM — инструмент для преобразования объектов в SQL код и наоборот, но никакой связи между объектом в коде и объектом в БД здесь нет. ORM облегчает работу с БД в CRUD приложениях.


            1. VolCh
              15.12.2017 13:36

              у вас состояние модели может быть неактуальным

              Есть транзакции с различными видами блокировок. Но, вообще, да, универсальные ОРМ плохо работают, если хочется заметную часть логики перенести в РСУБД. Выхода четыре основных:


              • не переносить
              • отказаться от ОРМ в принципе, то есть от проецирования реляционной модели на объектную, скорее всего за счёт отказа от объектной, ну или от реляционной :)
              • разработать свою ОРМ, хорошо знающей о логике в РСУБД. Или доработать универсальную.
              • изменить обе модели так, чтобы маппинг проблем не создавал особых, например путём перехода к EventSourcing, где маппиться будут не сущности, а события, а на стороне СУБД например триггерами формироваться проекции сущностей.


      1. maslyaev
        15.12.2017 14:25

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

        Естественно, в каждом конкретном случае мы выкручиваемся. При помощи велосипедов, заплаток, костылей и километров говнокода. Системы создаются, эксплуатируются, развиваются. Всё в порядке. Но когда выкатывают очередную «серебряную пулю» и говорят, что она легко лечит тот геморрой, который уже десятки лет не смог вылечить никто, да ещё и иллюстрируют примерами детсадовского уровня — вот это реально бесит.

        Мы знаем, что такое в РСУБД таблица, строка, колонка, ячейка, первичный ключ, внешний ключ. Все эти понятия имеют чёткую математическую интерпретацию, и поэтому реляционная алгебра — это именно алгебра, а не искусствоведение или, например, политология. Можно ли что-то подобное сказать про ООП? Неа, нельзя.

        Что такое объект? Что такое класс? Что значит «потомок класса»? Можно ли от этих терминов протянуть понятийный мостик, к чему-то математическому? Можно попытаться сказать, что класс — это множество, объект — его элемент, а потомок — это подмножество. Но дальше начинаются чудеса. Например, обнаруживается, что массив объектов — это тоже множество. «Массив» и «класс» — ну совсем разные вещи, но они обе типа про множества. Или вдруг выясняется, что в ООП объект может принадлежать только одному классу (и транзитивно его предкам через наследование). А теория множеств такого ограничения не предполагает никак. Там вполне нормальна ситуация, когда «существуют мюмзики которые также являются лямзиками, а некоторые лямзики не являются мюмзиками», и при этом никаких ограничений на то, что эти же объекты заодно могут быть бумзиками и грумзиками. В ООП такие ситуации реализуются через чрезвычайно уродливую миксинную технологию (паттерн «стратегия»), но это иначе как архитектурным костылём язык не поворачивается назвать. Короче, нет никакой внятной идейной основы у ООП, и поэтому ОО-программрование ближе к искусству и политологии, чем к нормальной инженерии.

        Искусство с искусством (например, музыку с литературой) правильно поженить можно, науку с наукой — тоже, но совмещать несовместимое без велосипедов и костылей — никак.


    1. jonic
      15.12.2017 03:45

      я для Eloquent ORM сделал надслойку где в модели в конструкторе (ну или где то еще, но удобнее там) описывается схема (вот прям select) полей, споддержкой переименовывания на лету, так же описывались поля с функциями сереализации/десереализации, записывались отношения( join таблиц как моделей) и все это ради того что бы получить list/item, а самое главное, одним ajax запросом создать или изменить данные в этой модели с автоматической раскладкой данных по зависимостям. Причем поддерживались как One2One, так и One2Many.
      А учитывая что как субд использовался постгрес, то кастомный select решал. Ну и я опустил подробности допуска к полям и моделям ролей на чтение/запись. По факту у меня получился конструктор, которому не хватает визуального редактора схемы. и к этому конструктору шел фронт на Backbone, который так же по сути состоял из кирпичиков… И там были только UCollection и UModel для загрузки/обновления данных через описаный выше метод.


    1. hack3p
      15.12.2017 11:33

      Используйте ORM для CRUD. А для всего остального QueryBuilder.


      1. VolCh
        15.12.2017 12:38

        Используйте ORM для Command, а для Query что-то другое :)