Студия разработки сайтов и приложений

Netspark.ru

Заметки и разработки

Drupal

Laravel relationship tricks

Отношения в Laravel в принципе довольно удобно сделаны и работать с ними приятно. Но бывают ситуации, решения для которых находятся в поиске и доках не сразу. Приведу пару небольших примерчиков.

Метод morphWith

Конкретно этот метод есть в доках, но он появился совсем недавно, в версии 5.8.22 (а текущая — 5.8.25) и не сразу нашёлся именно поэтому. Суть вот в чем. Допустим, у нас есть модель Event, которая радостно полиморфирует с другими моделями. Для неё отношения описаны так:

class Event extends Model
{
    protected $with = ['parent'];

    public function parent() {
        return $this->morphTo();
    }

    // ...
}

Как мы видим, благодаря свойству $with модель будет сразу загружаться с родителем, независимо от того, что это за родитель. Однако что, если нам нужно разом загрузить и какие-то отношения этого (неопределенного пока) родителя с другими моделями. Допустим, модели Payment и Deadline связаны полиморфно с Event, и в то же время модель Payment связана с моделью Order, а Deadline — скажем, с моделью User. Примерно так:

class Payment extends Model
{
    public function events() {
        return $this->morphMany('App\Event', 'parent');
    }

    public function order() {
        return $this->belongsTo(Order::class);
    }

    // ...
}

// (в другом, конечно, файле)

class Deadline extends Model
{
    public function events() {
        return $this->morphMany('App\Event', 'parent');
    }

    public function User() {
        return $this->belongsTo(User::class);
    }

    // ...
}

И мы хотим, чтобы платежи грузились с заказами, а дедлайны — с пользователями. Конечно, мы могли бы воспользоваться свойством $with для моделей Payment и Deadline. Но что если в Order и User уже используется свойство $with для загрузки платежей и дедлайнов? Тогда мы не сможем воспользоваться этим свойством, поскольку уйдем в рекурсию.

Тут-то и поможет нам метод morphWith():

$events = Event::query()
            ->with(['parent' => function (MorphTo $morphTo) {
                $morphTo->morphWith([
                    Payment::class => ['order'],
                    Deadline::class => ['user'],
                ]);
            }])
            ->get();

То есть что мы тут делаем? Мы вызываем для отношения MorphTo метод morphWith() и для каждого класса родителя, для которого нужно грузить его отношения, указываем массив из этих отношений. И они грузятся.

А что если надо отменить eager loading

Может возникнуть и прямо противоположная проблема. Допустим, мы автоматически грузим модель Payment из Order через свойство $with — это будет работать всякий раз, когда модель заказа загружается. Но что если нам в данном конкретном случае нужно автоматически загрузить заказы для группы платежей, и не попасть при этом ни в рекурсию, ни в «проблему N+1 запросов»?

Очевидно, нужно отключить eager-loading. Вот два примера, как можно это сделать.

1) Если может возникнуть рекурсия из-за $with, можно отключить автозагрузку прямо в объявлении отношения:

public function order() {
    return $this->belongsTo(Order::class)->setEagerLoads([]);
}

Здесь мы говорим, что при загрузке заказов для данных моделей, не нужно автоматически грузить никакие модели, связанные с заказами (и перечисленные в Order::$with). Кстати, вместо пустого массива мы можем передать массив из отношений, которые всё же нужно загрузить, ограничив таким образом автозагрузку заданным списком.

2) Если же мы прогружаем отношения явным образом с помощью метода load(), можно отключить автозагрузку вот так:

$payment->load(['order' => function ($query) {
    $query->setEagerLoads([]);
}]);

Напоследок замечу вот еще что. Хотя свойство $with и довольно удобно в целом, так как работает автоматически, бездумное его использование представляется частным случаем преждевременной оптимизации. Когда мы не знаем, как часто и какие зависимости нам потребуются, велик соблазн пихнуть их все в массив $with и забыть. Однако позже, при описании уже реальных кейсов, вполне может возникнуть ситуация, когда отношения, перечисленные в автозагрузке придётся удалять и переделывать одно за другим.

Не нужно, в общем, торопиться с оптимизацией.

Комментарии