Skip to content

Latest commit

 

History

History
1930 lines (1337 loc) · 123 KB

eloquent-relationships.md

File metadata and controls

1930 lines (1337 loc) · 123 KB

git 6ac13f37adbed3ce6a6532fd790f70bd731b8571


Eloquent · Отношения

Введение

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

Определение отношений

Отношения Eloquent определяются как методы в классах модели Eloquent. Поскольку отношения реализованы поверх построителей запросов, использование отношений как методов (к примеру, не ->posts, а ->posts()) обеспечивает возможность создания цепочек методов и запросов. Например, мы можем добавить к результатам отношения дополнительное ограничение:

$user->posts()->where('active', 1)->get();

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

Один к одному

Отношения «один-к-одному» – это очень простой тип отношений базы данных. Например, модель User может быть связана с одной моделью Phone. Чтобы определить это отношение, мы поместим метод phone в модель User. Метод phone должен вызывать метод hasOne и возвращать его результат. Метод hasOne доступен для вашей модели через базовый класс Illuminate\Database\Eloquent\Model модели:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Получить телефон, связанный с пользователем.
     */
    public function phone()
    {
        return $this->hasOne(Phone::class);
    }
}

Первым аргументом, передаваемым методу hasOne, является имя связанного класса модели. Как только связь определена, мы можем получить связанную запись, используя динамические свойства Eloquent. Динамические свойства позволяют получить доступ к методам отношений, как если бы они были свойствами, определенными в модели:

$phone = User::find(1)->phone;

Eloquent определяет внешний ключ отношения на основе имени родительской модели. В этом случае автоматически предполагается, что модель Phone имеет внешний ключ user_id. Если вы хотите переопределить это соглашение, вы можете передать второй аргумент методу hasOne:

return $this->hasOne(Phone::class, 'foreign_key');

Кроме того, Eloquent предполагает, что внешний ключ должен иметь значение, соответствующее столбцу первичного ключа родительского элемента. Другими словами, Eloquent будет искать значение столбца id пользователя в столбце user_id модели Phone. Если вы хотите, чтобы отношение использовало значение первичного ключа, отличное от id или свойства вашей модели $primaryKey, вы можете передать третий аргумент методу hasOne:

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

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

Итак, мы можем получить доступ к модели Phone из нашей модели User. Затем давайте определим отношение в модели Phone, позволяющее нам получить доступ к пользователю, которому принадлежит телефон. Мы можем определить инверсию отношения hasOne с помощью метода belongsTo:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
    /**
     * Получить пользователя, владеющего телефоном.
     */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

При вызове метода user, Eloquent попытается найти модель User, у которой есть id, который соответствует столбцу user_id в модели Phone.

Eloquent определяет имя внешнего ключа, анализируя имя метода отношения и добавляя к имени метода суффикс _id. Итак, в этом случае Eloquent предполагает, что модель Phone имеет столбец user_id. Однако, если внешний ключ в модели Phone не является user_id, вы можете передать собственное имя ключа в качестве второго аргумента методу belongsTo:

/**
 * Получить пользователя, владеющего телефоном.
 */
public function user()
{
    return $this->belongsTo(User::class, 'foreign_key');
}

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

/**
 * Получить пользователя, владеющего телефоном.
 */
public function user()
{
    return $this->belongsTo(User::class, 'foreign_key', 'owner_key');
}

Один ко многим

Отношение «один-ко-многим» используется для определения отношений, в которых одна модель является родительской для одной или нескольких дочерних моделей. Например, пост в блоге может содержать бесконечное количество комментариев. Как и все другие отношения Eloquent, отношения «один-ко-многим» определяются путем определения метода в вашей модели Eloquent:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Получить комментарии к посту блога.
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Помните, что Eloquent автоматически определит правильный столбец внешнего ключа для модели Comment. По соглашению Eloquent берет имя родительской модели в «змеином регистре» и добавляет к нему суффикс _id. Итак, в этом примере Eloquent предполагает, что столбец внешнего ключа в модели Comment именуется post_id.

Как только метод отношения определен, мы можем получить доступ к коллекции связанных комментариев, используя свойство comments. Поскольку Eloquent обеспечивает «динамические свойства отношений», то мы можем получить доступ к методам отношений, как если бы они были определены как свойства в модели:

use App\Models\Post;

$comments = Post::find(1)->comments;

foreach ($comments as $comment) {
    //
}

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

$comment = Post::find(1)->comments()
                    ->where('title', 'foo')
                    ->first();

Подобно методу hasOne, вы также можете переопределить внешние и локальные ключи, передав дополнительные аргументы методу hasMany:

return $this->hasMany(Comment::class, 'foreign_key');

return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

Определение обратной связи Один ко многим

Теперь, когда мы можем получить доступ ко всем комментариям поста, давайте определим отношение, чтобы разрешить комментарию доступ к его родительскому посту. Чтобы определить инверсию отношения hasMany, определите метод отношения в дочерней модели, который вызывает метод belongsTo:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Получить пост, которому принадлежит комментарий.
     */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

Как только связь определена, мы можем получить родительский пост комментария, обратившись к «динамическому свойству отношения» post:

use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

В приведенном выше примере Eloquent попытается найти модель Post, у которой есть id, который соответствует столбцу post_id в модели Comment.

Eloquent определяет имя внешнего ключа по умолчанию, анализируя имя метода отношения и добавляя к имени метода суффикс _, за которым следует имя столбца первичного ключа родительской модели. Итак, в этом примере Eloquent предполагает, что внешний ключ модели Post в таблице comments – это post_id.

Однако, если внешний ключ для ваших отношений не соответствует этим соглашениям, вы можете передать свое имя внешнего ключа в качестве второго аргумента методу belongsTo:

/**
 * Получить пост, которому принадлежит комментарий.
 */
public function post()
{
    return $this->belongsTo(Post::class, 'foreign_key');
}

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

/**
 * Получить пост, которому принадлежит комментарий.
 */
public function post()
{
    return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');
}

Модели по умолчанию

Отношения belongsTo, hasOne, hasOneThrough и morphOne позволяют вам определить модель по умолчанию, которая будет возвращена, если данное отношение равно null. Этот шаблон часто называют шаблоном нулевого объекта, который поможет удалить условные проверки в вашем коде. В следующем примере отношение user вернет пустую модель App\Models\User, если к модели Post не привязан ни один user:

/**
 * Получить автора поста.
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault();
}

Чтобы заполнить модель по умолчанию атрибутами, вы можете передать массив или замыкание методу withDefault:

/**
 * Получить автора поста.
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

/**
 * Получить автора поста.
 */
public function user()
{
    return $this->belongsTo(User::class)->withDefault(function ($user, $post) {
        $user->name = 'Guest Author';
    });
}

Запрос отношений Один К

При запросе дочерних элементов отношения "принадлежит" (belongs to) вы можете вручную создать предложение where для получения соответствующих моделей Eloquent:

use App\Models\Post;

$posts = Post::where('user_id', $user->id)->get();

Однако вам может быть удобнее использовать метод whereBelongsTo, который автоматически определит правильные отношения и внешний ключ для данной модели:

$posts = Post::whereBelongsTo($user)->get();

По умолчанию Laravel будет определять отношения, связанные с данной моделью, на основе имени класса модели; однако вы можете указать имя отношения вручную, указав его в качестве второго аргумента метода whereBelongsTo:

$posts = Post::whereBelongsTo($user, 'author')->get();

Один из многих

Иногда модель может иметь множество связанных моделей, но вы хотите легко получить "самую последнюю" или "самую старую" связанную модель. Например, модель User может быть связана со многими моделями Order, но вы хотите определить удобный способ взаимодействия с последним заказом пользователя. Для этого можно использовать тип отношения hasOne в сочетании с методами ofMany:

/**
 * Получить последний (самый новый) заказ пользователя
 */
public function latestOrder()
{
    return $this->hasOne(Order::class)->latestOfMany();
}

Аналогично, вы можете определить метод для получения "самой старой" (первой) связанной модели отношения:

/**
 * Получить самый старый заказ пользователя
 */
public function oldestOrder()
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

По умолчанию методы latestOfMany и oldestOfMany извлекают самую последнюю или самую старую связанную модель на основе первичного ключа модели. Этот ключ должен быть сортируемым. Чтобы использовать данные другого столбца, используйте метод ofMany. Он принимает в качестве первого аргумента сортируемый столбец и то, какую агрегатную функцию (min или max) применить при запросе связанной модели.

Например, так можно получить самый дорогой заказ пользователя:

/**
 * Получить самый дорогой заказ пользователя
 */
public function largestOrder()
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

{note} Поскольку PostgreSQL не поддерживает выполнение функции MAX для столбцов UUID, в настоящее время невозможно использовать отношения "один-из-многих" в сочетании со столбцами UUID PostgreSQL.

Продвинутые возможности отношения Один-из-многих

Критерии сортировки при выборе могут быть сложными. Например, модель Product может иметь множество связанных с ней моделей Price, которые сохраняются в системе даже после публикации новых цен - например, чтобы можно было посмотреть динамику изменения цены продукта. Кроме того, новые данные о ценах на продукт могут быть опубликованы заранее, чтобы вступить в силу в определённую дату - через колонку published_at.

Таким образом, нам нужно получить последние опубликованные цены, если дата публикации не находится в будущем. Кроме того, если две цены имеют одинаковую дату публикации, мы предпочтем цену с наибольшим ID. Для этого в метод ofMany нужно передать массив, который содержит сортируемые столбцы, определяющие последнюю цену. Кроме того, в качестве второго аргумента метода ofMany передаётся функция, которая будет отвечать за добавление дополнительного фильтра по дате публикации:

/**
 * Получить актуальную цену на продукт
 */
public function currentPricing()
{
    return $this->hasOne(Price::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Один через отношение

Отношение «один-через-отношение» определяет отношение «один-к-одному» с другой моделью. Однако, это отношение указывает на то, что декларируемую модель можно сопоставить с одним экземпляром другой модели, связавшись через третью модель.

Например, в приложении автомастерской каждая модель Mechanic может быть связана с одной моделью Car, и каждая модель Car может быть связана с одной моделью Owner. В то время как механик и владелец не имеют прямых отношений в базе данных, механик может получить доступ к владельцу через модель Car. Давайте посмотрим на таблицы, необходимые для определения этой связи:

mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Mechanic extends Model
{
    /**
     * Получить владельца машины.
     */
    public function carOwner()
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

Первый аргумент, передаваемый методу hasOneThrough – это имя последней модели, к которой мы хотим получить доступ, а второй аргумент – это имя промежуточной (сводной) модели.

Соглашения по именованию ключей отношения Один через отношение

Типичные соглашения о внешнем ключе Eloquent будут использоваться при выполнении запросов отношения. Если вы хотите изменить ключи отношения, вы можете передать их в качестве третьего и четвертого аргументов методу hasOneThrough. Третий аргумент – это имя внешнего ключа сводной модели. Четвертый аргумент – это имя внешнего ключа окончательной модели. Пятый аргумент – это локальный ключ, а шестой аргумент – это локальный ключ сводной модели:

class Mechanic extends Model
{
    /**
     * Получить владельца машины.
     */
    public function carOwner()
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // Внешний ключ в таблице `cars` ...
            'car_id', // Внешний ключ в таблице `owners` ...
            'id', // Локальный ключ в таблице `mechanics` ...
            'id' // Локальный ключ в таблице `cars` ...
        );
    }
}

Многие через отношение

Отношение «многие-через-отношение» обеспечивает удобный способ доступа к отдаленным отношениям через промежуточное отношение. Например, предположим, что мы создаем платформу развертывания, такую как Laravel Vapor. Модель Project может получить доступ ко многим моделям Deployment через сводную модель Environment. Используя этот пример, вы можете легко собрать все развертывания (deployments) для конкретного проекта. Давайте посмотрим на таблицы, необходимые для определения этой связи:

projects
    id - integer
    name - string

environments
    id - integer
    project_id - integer
    name - string

deployments
    id - integer
    environment_id - integer
    commit_hash - string

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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    /**
     * Получить все развертывания для проекта.
     */
    public function deployments()
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

Первый аргумент, передаваемый методу hasManyThrough – это имя последней модели, к которой мы хотим получить доступ, а второй аргумент – это имя сводной модели.

Хотя таблица модели Deployment не содержит столбца project_id, отношение hasManyThrough обеспечивает доступ к deployments проекта через $project->deployments. Чтобы получить эти модели, Eloquent проверяет столбец project_id в сводной таблице модели Environment. После нахождения соответствующих идентификаторов environments они используются для запроса таблицы модели Deployment.

Соглашения по именованию ключей отношения Многие через отношение

Типичные соглашения о внешнем ключе Eloquent будут использоваться при выполнении запросов отношения. Если вы хотите изменить ключи отношения, вы можете передать их в качестве третьего и четвертого аргументов методу hasManyThrough. Третий аргумент – это имя внешнего ключа сводной модели. Четвертый аргумент – это имя внешнего ключа окончательной модели. Пятый аргумент – это локальный ключ, а шестой аргумент – это локальный ключ сводной модели:

class Project extends Model
{
    public function deployments()
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'project_id', // Внешний ключ в таблице `environments` ...
            'environment_id', // Внешний ключ в таблице `deployments` ...
            'id', // Локальный ключ в таблице `projects` ...
            'id' // Локальный ключ в таблице `environments` ...
        );
    }
}

Отношения Многие ко многим

Отношения «многие-ко-многим» немного сложнее, чем отношения hasOne и hasMany. Примером отношения «многие-ко-многим» является пользователь, у которого много ролей, и эти роли также используются другими пользователями в приложении. Например, пользователю могут быть назначены роли «Автор» и «Редактор»; однако эти роли также могут быть назначены другим пользователям. Итак, у пользователя много ролей, а у роли много пользователей.

Структура таблицы Многие ко многим

Чтобы определить эту связь, необходимы три таблицы базы данных: users, roles, и role_user. Таблица role_user является производной от имен связанных моделей в алфавитном порядке и содержит столбцы user_id и role_id. Эта таблица используется как промежуточная таблица, связывающая пользователей и роли.

Помните, поскольку роль может принадлежать многим пользователям, мы не можем просто разместить столбец user_id в таблице role. Это означало бы, что роль могла принадлежать только одному пользователю. Для обеспечения поддержки ролей, назначаемых нескольким пользователям, необходима таблица role_user. Мы можем резюмировать структуру таблицы отношений следующим образом:

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

Структура модели Многие ко многим

Отношения «многие-ко-многим» определяются путем написания метода, который возвращает результат метода belongsToMany. Метод belongsToMany обеспечен базовым классом Illuminate\Database\Eloquent\Model, который используется всеми моделями Eloquent вашего приложения. Например, давайте определим метод roles в нашей модели User. Первым аргументом, передаваемым этому методу, является имя класса сводной модели:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Роли, принадлежащие пользователю.
     */
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Как только связь определена, вы можете получить доступ к ролям пользователя, используя динамическое свойство связи roles:

use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    //
}

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

$roles = User::find(1)->roles()->orderBy('name')->get();

Чтобы определить имя промежуточной таблицы отношения, Eloquent соединит имена двух связанных моделей в алфавитном порядке. Однако вы можете изменить это соглашение, передав название таблицы в виде второго аргумента метода belongsToMany:

return $this->belongsToMany(Role::class, 'role_user');

В дополнение к переопределению имени промежуточной таблицы, вы также можете изменить имена столбцов ключей в таблице, передав дополнительные аргументы методу belongsToMany. Третий аргумент – это имя внешнего ключа модели, для которой вы определяете отношение. Четвертый аргумент – это имя внешнего ключа модели, к которой вы присоединяетесь:

return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');

Определение обратной связи Многие ко многим

Чтобы определить «обратное» отношение «многие-ко-многим», вы должны определить метод в связанной модели, который также возвращает результат метода belongsToMany. Чтобы завершить наш пример пользователи / роли, давайте определим метод users в модели Role:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * Пользователи, принадлежащие к роли.
     */
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

Как видите, отношение определяется точно так же, как и его аналог в модели User, за исключением ссылки на модель App\Models\User. Поскольку мы повторно используем метод belongsToMany, все стандартные параметры настройки таблиц и ключей доступны при определении «обратных» отношений «многие-ко-многим».

Получение столбцов сводной таблицы

Как вы уже узнали, для работы с отношениями «многие-ко-многим» требуется наличие промежуточной таблицы. Eloquent предлагает несколько очень полезных способов взаимодействия с этой таблицей. Например, предположим, что наша модель User имеет много моделей Role, с которыми она связана. После доступа к этой связи мы можем получить доступ к промежуточной таблице с помощью атрибута pivot в моделях:

use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

Обратите внимание, что каждой модели Role, которую мы получаем, автоматически назначается атрибут pivot. Этот атрибут содержит модель, представляющую промежуточную таблицу.

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

return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

Если вы хотите, чтобы ваша промежуточная таблица имела временные метки created_at и updated_at, которые автоматически поддерживаются Eloquent, вызовите метод withTimestamps при определении отношения:

return $this->belongsToMany(Role::class)->withTimestamps();

{note} Промежуточные таблицы, использующие автоматически поддерживаемые временные метки Eloquent, должны иметь столбцы временных меток created_at и updated_at.

Корректировка имени атрибута pivot

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

Например, если ваше приложение содержит пользователей, которые могут подписаться на подкасты, вы, вероятно, имеете отношение «многие-ко-многим» между пользователями и подкастами. По желанию можно переименовать атрибут pivot промежуточной таблицы на subscription. Это можно сделать с помощью метода as при определении отношения:

return $this->belongsToMany(Podcast::class)
                ->as('subscription')
                ->withTimestamps();

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

$users = User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->subscription->created_at;
}

Фильтрация запросов по столбцам сводной таблицы

Вы также можете отфильтровать результаты, возвращаемые запросами отношения belongsToMany, используя методы wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull и wherePivotNotNull при определении отношения:

return $this->belongsToMany(Role::class)
                ->wherePivot('approved', 1);

return $this->belongsToMany(Role::class)
                ->wherePivotIn('priority', [1, 2]);

return $this->belongsToMany(Role::class)
                ->wherePivotNotIn('priority', [1, 2]);

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotBetween('created_at', ['2020-01-01 00:00:00', '2020-12-31 00:00:00']);

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNull('expired_at');

return $this->belongsToMany(Podcast::class)
                ->as('subscriptions')
                ->wherePivotNotNull('expired_at');                     

Определение пользовательских моделей сводных таблиц

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

Явные сводные модели отношения «многие-ко-многим» должны расширять класс Illuminate\Database\Eloquent\Relations\Pivot, в то время как явные полиморфные сводные модели отношения «многие-ко-многим» должны расширять класс Illuminate\Database\Eloquent\Relations\MorphPivot. Например, мы можем определить модель Role, которая использует явную сводную модель RoleUser:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
     * Пользователи, принадлежащие к роли.
     */
    public function users()
    {
        return $this->belongsToMany(User::class)->using(RoleUser::class);
    }
}

При определении модели RoleUser вы должны расширять класс Illuminate\Database\Eloquent\Relations\Pivot:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class RoleUser extends Pivot
{
    //
}

{note} Сводные модели не могут использовать трейт SoftDeletes. Если вам нужно программно удалить сводные записи, подумайте о преобразовании вашей сводной модели в реальную модель Eloquent.

Пользовательские сводные модели и автоинкрементные идентификаторы

Если вы определили отношение «многие-ко-многим», которое использует явную сводную модель, и эта сводная модель имеет автоинкрементный первичный ключ, то вы должны убедиться, что ваш класс явной сводной модели определяет свойство $incrementing, для которого установлено значение true.

/**
 * Указывает, что идентификаторы модели являются автоинкрементными.
 *
 * @var bool
 */
public $incrementing = true;

Полиморфные отношения

Полиморфные отношения позволяют дочерней модели принадлежать более чем к одному типу модели с использованием одной ассоциации. Например, представьте, что вы создаете приложение, которое позволяет пользователям делиться постами и видео в блогах. В таком приложении модель Comment может принадлежать как к моделям Post, так и к Video.

Один к одному (полиморфное)

Структура таблицы отношения Один к одному (полиморфное)

Полиморфное отношение «один-к-одному» похоже на типичное «один-к-одному» отношение; однако, дочерняя модель может принадлежать более чем к одному типу моделей с помощью одной ассоциации. Например, блог Post и User могут иметь полиморфное отношение С моделью Image. Использование полиморфного «один-к-одному» отношения позволяет вам иметь единую таблицу уникальных изображений, которые могут быть связаны с постами и пользователями. Сначала рассмотрим структуру таблицы:

posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

Обратите внимание на столбцы imageable_id и imageable_type в таблице images. Столбец imageable_id будет содержать значение идентификатора поста или пользователя, а столбец imageable_type будет содержать имя класса родительской модели. Столбец imageable_type используется Eloquent для определения того, какой «тип» родительской модели возвращать при доступе к отношению imageable. В этом случае столбец будет содержать либо App\Models\Post, либо App\Models\User.

Структура модели отношения Один к одному (полиморфное)

Давайте рассмотрим определения модели, необходимые для построения этой связи:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    /**
     * Получить родительскую модель (пользователя или поста), к которой относится изображение.
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Получить изображение поста.
     */
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

class User extends Model
{
    /**
     * Получить изображение пользователя.
     */
    public function image()
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Получение отношения Один к одному (полиморфное)

Как только ваша таблица базы данных и модели определены, вы можете получить доступ к отношениям через свои модели. Например, чтобы получить изображение для поста, мы можем обратиться к динамическому свойству связи image:

use App\Models\Post;

$post = Post::find(1);

$image = $post->image;

Вы можете получить родительский объект полиморфной модели, обратившись к имени метода, который выполняет вызов morphTo. В данном случае это метод imageable модели Image. Итак, мы будем обращаться к этому методу как к динамическому свойству отношения:

use App\Models\Image;

$image = Image::find(1);

$imageable = $image->imageable;

Отношение imageable в модели Image будет возвращать экземпляр Post или User, в зависимости от того, к какому типу модели относится изображение.

Соглашения по именованию ключей отношения Один к одному (полиморфное)

Если необходимо, то вы можете указать имя столбцов id и type, используемых вашей полиморфной дочерней моделью. Если вы это сделаете, то убедитесь, что вы всегда передаете имя отношения в качестве первого аргумента методу morphTo. Обычно это значение должно совпадать с именем метода, поэтому вы можете использовать константу __FUNCTION__ PHP:

/**
 * Получить родительскую модель, к которой относится изображение.
 */
public function imageable()
{
    return $this->morphTo(__FUNCTION__, 'imageable_type', 'imageable_id');
}

Один ко многим (полиморфное)

Структура таблицы отношения Один ко многим (полиморфное)

Полиморфное отношение «один-ко-многим» похоже на типичное отношение «один-ко-многим»; однако, дочерняя модель может принадлежать более чем к одному типу моделей с помощью одной ассоциации. Например, представьте, что пользователи вашего приложения могут «комментировать» посты и видео. Используя полиморфные отношения, вы можете использовать одну таблицу comments, чтобы хранить комментарии как для постов, так и для видео. Во-первых, давайте рассмотрим структуру таблицы, необходимую для построения этой связи:

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

Структура модели отношения Один ко многим (полиморфное)

Давайте рассмотрим определения модели, необходимые для построения этой связи:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Получить родительскую модель (поста или видео), к которой относится комментарий.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Получить все комментарии поста.
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Video extends Model
{
    /**
     * Получить все комментарии видео.
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Получение отношения Один ко многим (полиморфное)

После того как ваша таблица базы данных и модели определены, вы можете получить доступ к отношениям через динамические свойства отношений вашей модели. Например, чтобы получить доступ ко всем комментариям к постам, мы можем использовать динамическое свойство comments:

use App\Models\Post;

$post = Post::find(1);

foreach ($post->comments as $comment) {
    //
}

Вы также можете получить родительскую модель полиморфной дочерней модели, обратившись к имени метода, который выполняет вызов morphTo. В данном случае это метод commentable в модели Comment. Итак, мы будем обращаться к этому методу как к динамическому свойству связи, чтобы получить доступ к родительской модели комментария:

use App\Models\Comment;

$comment = Comment::find(1);

$commentable = $comment->commentable;

Отношение commentable в модели Comment вернет либо экземпляр Post, либо Video, в зависимости от того, какой тип модели является родительским для комментария.

Один из многих (полиморфное)

Иногда модель может иметь множество связанных моделей, но вы хотите легко получить "самую последнюю" или "самую старую" связанную модель. Например, модель User может быть связана со многими моделями Image, но вы хотите определить удобный способ взаимодействия с последним загруженным пользователем изображением. Для этого можно использовать тип отношения morphOne в сочетании с методами ofMany:

/**
 * Get the user's most recent image.
 */
public function latestImage()
{
    return $this->morphOne(Image::class, 'imageable')->latestOfMany();
}

Аналогично вы можете определить метод для получения "самой старой" (первой) связанной модели отношения:

/**
 * Get the user's oldest image.
 */
public function oldestImage()
{
    return $this->morphOne(Image::class, 'imageable')->oldestOfMany();
}

По умолчанию методы latestOfMany и oldestOfMany извлекают самую последнюю или самую старую связанную модель на основе первичного ключа модели. Этот ключ должен быть сортируемым. Чтобы использовать данные другого столбца, используйте метод ofMany. Он принимает в качестве первого аргумента сортируемый столбец и то, какую агрегатную функцию (min или max) применить при запросе связанной модели.

/**
 * Получаем изображение, у которого больше всего лайков.
 */
public function bestImage()
{
    return $this->morphOne(Image::class, 'imageable')->ofMany('likes', 'max');
}

{tip} Можно построить более сложные отношения "один из многих". Для получения дополнительной информации обратитесь к [данному разделу документации].(#advanced-has-one-of-many-relationships).

Многие ко многим (полиморфное)

Структура таблицы отношения Многие ко многим (полиморфное)

Полиморфные отношения «многие-ко-многим» немного сложнее, чем полиморфные отношения «один-к-одному» и «один-ко-многим». Например, модель Post и модель Video могут иметь полиморфное отношение к модели Tag. Использование полиморфного отношения «многие-ко-многим» в этой ситуации позволит вашему приложению иметь единую таблицу уникальных тегов, которые могут быть связаны с постами или видео. Во-первых, давайте рассмотрим структуру таблицы, необходимую для построения этой связи:

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

{tip} Прежде чем погрузиться в полиморфные отношения «многие-ко-многим», вам может быть полезно прочитать документацию по типичным отношениям «многие-ко-многим».

Структура модели отношения Многие ко многим (полиморфное)

Далее, мы готовы определить отношения в моделях. Обе модели Post и Video будут содержать метод tags, который вызывает метод morphToMany, предоставляемый базовым классом модели Eloquent.

Метод morphToMany принимает имя связанной модели, а также «имя отношения». В зависимости от имени, которое мы присвоили имени нашей промежуточной таблицы и содержащихся в ней ключей, мы будем называть эту связь taggable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Получить все теги поста.
     */
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

Определение обратной связи Многие ко многим (полиморфное)

Затем в модели Tag вы должны определить метод для каждой из ее возможных родительских моделей. Итак, в этом примере мы определим метод posts и метод videos. Оба метода должны возвращать результат метода morphedByMany.

Метод morphedByMany принимает имя связанной модели, а также «имя отношения». В зависимости от имени, которое мы присвоили имени нашей промежуточной таблицы и содержащихся в ней ключей, мы будем называть эту связь taggable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    /**
     * Получить все посты, которым присвоен этот тег.
     */
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    /**
     * Получить все видео, которым присвоен этот тег.
     */
    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }
}

Получение отношения Многие ко многим (полиморфное)

Как только ваша таблица базы данных и модели определены, вы можете получить доступ к отношениям через свои модели. Например, чтобы получить доступ ко всем тегам для публикации, вы можете использовать динамическое свойство связи tags:

use App\Models\Post;

$post = Post::find(1);

foreach ($post->tags as $tag) {
    //
}

Вы можете получить родительскую модель полиморфного отношения из полиморфной дочерней модели, обратившись к имени метода, который выполняет вызов morphedByMany. В данном случае это методы posts или videos в модели Tag:

use App\Models\Tag;

$tag = Tag::find(1);

foreach ($tag->posts as $post) {
    //
}

foreach ($tag->videos as $video) {
    //
}

Именование полиморфных типов

По умолчанию Laravel будет использовать полное имя класса для хранения «типа» связанной модели. Например, учитывая приведенный выше пример отношения «один-ко-многим», где модель Comment может принадлежать модели Post или Video, по умолчанию commentable_type будет либо 'App\Models\Post', либо 'App\Models\Video', соответственно. По желанию можно отделить эти значения от внутренней структуры вашего приложения.

Например, вместо использования названий моделей в качестве «типа» мы можем использовать простые строки, такие как post и video. Таким образом, значения столбца полиморфного «типа» в нашей базе данных останутся действительными, даже если модели будут переименованы:

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

Вы можете зарегистрировать enforceMorphMap в методе boot вашего класса App\Providers\AppServiceProvider или создать отдельный сервис-провайдер для этого, если хотите.

Вы можете определить псевдоним полиморфного типа конкретной модели во время выполнения, используя метод модели getMorphClass. И наоборот, вы можете определить полное имя класса, связанное с псевдонимом полиморфного типа, используя метод Relation::getMorphedModel:

use Illuminate\Database\Eloquent\Relations\Relation;

$alias = $post->getMorphClass();

$class = Relation::getMorphedModel($alias);

{note} При добавлении «карты полиморфных типов» в существующее приложение каждое значение столбца *_type в вашей базе данных, которое все еще содержит полностью определенный класс, необходимо преобразовать в его псевдоним, указанный в «карте полиморфных типов».

Динамические отношения

Вы можете использовать метод resolveRelationUsing для определения отношений между моделями Eloquent во время выполнения скрипта. Хотя обычно это не рекомендуется для нормальной разработки приложений, но иногда это может быть полезно при разработке пакетов Laravel.

Метод resolveRelationUsing принимает желаемое имя отношения в качестве своего первого аргумента. Второй аргумент, передаваемый методу, должен быть замыканием, которое принимает экземпляр модели и возвращает допустимое определение отношения Eloquent. Как правило, вы должны настроить динамические отношения в методе boot поставщика служб:

use App\Models\Order;
use App\Models\Customer;

Order::resolveRelationUsing('customer', function ($orderModel) {
    return $orderModel->belongsTo(Customer::class, 'customer_id');
});

{note} При определении динамических отношений всегда предоставляйте явные аргументы имени ключа методам связи Eloquent.

Запросы отношений

Поскольку все отношения Eloquent определяются с помощью методов, вы можете вызывать эти методы для получения экземпляра отношения, не выполняя фактического запроса для загрузки связанных моделей. Кроме того, все типы отношений Eloquent также служат в качестве построителей запросов, позволяя вам продолжать связывать ограничения в запросе отношений, прежде чем окончательно выполнить запрос SQL к вашей базе данных.

Например, представьте себе приложение для блога, в котором модель User имеет множество связанных моделей Post:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Получить все посты пользователя.
     */
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

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

use App\Models\User;

$user = User::find(1);

$user->posts()->where('active', 1)->get();

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

Создание цепочки выражений orWhere после отношений

Как показано в приведенном выше примере, вы можете добавлять дополнительные ограничения к отношениям при их запросе. Однако, будьте осторожны при создании цепочек выражений orWhere с отношением, поскольку предложения orWhere будут логически сгруппированы на том же уровне, что и ограничение отношения:

$user->posts()
        ->where('active', 1)
        ->orWhere('votes', '>=', 100)
        ->get();

В приведенном выше примере будет сгенерирован следующий SQL. Как видите, выражение or предписывает запросу возвращать любого пользователя с более чем 100 голосами. Запрос больше не ограничен конкретным пользователем:

select *
from posts
where user_id = ? and active = 1 or votes >= 100

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

use Illuminate\Database\Eloquent\Builder;

$user->posts()
        ->where(function (Builder $query) {
            return $query->where('active', 1)
                         ->orWhere('votes', '>=', 100);
        })
        ->get();

В приведенном выше примере будет получен следующий SQL. Обратите внимание, что логическая группировка правильно сгруппировала ограничения, и запрос остается ограниченным для конкретного пользователя:

select *
from posts
where user_id = ? and (active = 1 or votes >= 100)

Методы отношений против динамических свойств

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

use App\Models\User;

$user = User::find(1);

foreach ($user->posts as $post) {
    //
}

Динамические свойства отношений выполняют «отложенную загрузку», что означает, что они будут загружать данные своих отношений только при фактическом доступе к ним. Из-за этого разработчики часто используют жадную загрузку для предварительной загрузки отношений, которые, как они знают, будут доступны после загрузки модели. Жадная загрузка обеспечивает значительное сокращение количества SQL-запросов, которые необходимо выполнить для загрузки отношений модели.

Запрос наличия отношений

При извлечении записей модели бывает необходимо ограничить результаты в зависимости от наличия связи. Например, представьте, что вы хотите получить все посты блога, содержащие хотя бы один комментарий. Для этого вы можете передать имя отношения методам has и orHas:

use App\Models\Post;

// Получить все посты, в которых есть хотя бы один комментарий ...
$posts = Post::has('comments')->get();

Вы также можете указать оператор и значение счетчика для уточнения запроса:

// Получить посты, в которых есть 3 или более комментариев ...
$posts = Post::has('comments', '>=', 3)->get();

Вы можете использовать «точечную нотацию» для выполнения запроса к вложенным отношениям. Например, вы можете получить все посты, в которых есть хотя бы один комментарий с хотя бы одним изображением:

// Получить посты, в которых есть хотя бы один комментарий с изображениями ...
$posts = Post::has('comments.images')->get();

Если вам нужно еще больше возможностей, вы можете использовать методы whereHas и orWhereHas для определения дополнительных ограничений запроса для ваших has-запросов, например, для проверки содержимого комментария:

use Illuminate\Database\Eloquent\Builder;

// Получить посты с хотя бы одним комментарием, содержащим `code%` ...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

// Получить посты с как минимум десятью комментариями, содержащими `code%` ...
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
}, '>=', 10)->get();

{note} Eloquent в настоящее время не поддерживает запросы о существовании отношений между базами данных. Отношения должны существовать в одной базе данных.

Однострочные запросы наличия отношений

Если вы хотите запросить существование отношения с одним простым условием, вам может быть удобнее использовать методы whereRelation и whereMorphRelation. Например, мы можем запросить все посты с не одобренными комментариями:

use App\Models\Post;

$posts = Post::whereRelation('comments', 'is_approved', false)->get();

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

$posts = Post::whereRelation(
    'comments', 'created_at', '>=', now()->subHour()
)->get();

Запрос отсутствия отношений

При извлечении записей модели бывает необходимо ограничить результаты на основании отсутствия связи. Например, представьте, что вы хотите получить все посты блога, которые не имеют комментариев. Для этого вы можете передать имя отношения методам doesntHave и orDoesntHave:

use App\Models\Post;

$posts = Post::doesntHave('comments')->get();

Если вам нужно еще больше возможностей, вы можете использовать методы whereDoesntHave и orWhereDoesntHave для определения дополнительных ограничений запроса для ваших doesntHave-запросов, например, для проверки содержимого комментария:

use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

Вы можете использовать «точечную нотацию» для выполнения запроса к вложенным отношениям. Например, следующий запрос будет извлекать все посты, у которых нет комментариев. Однако, посты с комментариями от авторов, которые не забанены, будут включены в результаты:

use Illuminate\Database\Eloquent\Builder;

$posts = Post::whereDoesntHave('comments.author', function (Builder $query) {
    $query->where('banned', 0);
})->get();

Запрос полиморфных отношений Morph To

Чтобы узнать о существовании полиморфных «один-к» отношений, вы можете использовать методы whereHasMorph и whereDoesntHaveMorph. Эти методы принимают имя отношения в качестве своего первого аргумента. Затем методы принимают имена связанных моделей, которые вы хотите включить в запрос. Наконец, вы можете предоставить замыкание, которое ограничивает запрос отношения:

use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Eloquent\Builder;

// Получить комментарии, связанные с постами или видео с заголовком, содержащими `code%` ...
$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

// Получить комментарии, связанные с постами с заголовком, не содержащим `code%` ...
$comments = Comment::whereDoesntHaveMorph(
    'commentable',
    Post::class,
    function (Builder $query) {
        $query->where('title', 'like', 'code%');
    }
)->get();

Иногда требуется добавить ограничения запроса в зависимости от «типа» связанной полиморфной модели. Замыкание, переданное методу whereHasMorph, может получить значение $type в качестве второго аргумента. Этот аргумент позволяет вам создавать запрос на основе «типа»:

use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph(
    'commentable',
    [Post::class, Video::class],
    function (Builder $query, $type) {
        $column = $type === Post::class ? 'content' : 'title';

        $query->where($column, 'like', 'code%');
    }
)->get();

Запрос всех связанных моделей

Допускается использование метасимвола подстановки * в качестве значения при передаче массива возможных полиморфных моделей. Это проинструктирует Laravel извлечь все возможные полиморфные типы из базы данных. Laravel выполнит дополнительный запрос, чтобы выполнить эту операцию:

use Illuminate\Database\Eloquent\Builder;

$comments = Comment::whereHasMorph('commentable', '*', function (Builder $query) {
    $query->where('title', 'like', 'foo%');
})->get();

Агрегирование связанных моделей

Подсчет связанных моделей

Иногда требуется подсчитать количество связанных моделей для отношения, не загружая модели. Для этого вы можете использовать метод withCount. Метод withCount помещает атрибут {relation}_count в результирующие модели:

use App\Models\Post;

$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

Передавая массив методу withCount, вы можете добавить «счетчики» для нескольких отношений, а также добавить дополнительные ограничения к запросам:

use Illuminate\Database\Eloquent\Builder;

$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
    $query->where('content', 'like', 'code%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

Вы также можете использовать псевдоним результата подсчета отношений, разрешив несколько подсчетов для одной и той же связи:

use Illuminate\Database\Eloquent\Builder;

$posts = Post::withCount([
    'comments',
    'comments as pending_comments_count' => function (Builder $query) {
        $query->where('approved', false);
    },
])->get();

echo $posts[0]->comments_count;
echo $posts[0]->pending_comments_count;

Отложенная загрузка подсчета связанных моделей

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

$book = Book::first();

$book->loadCount('genres');

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

$book->loadCount(['reviews' => function ($query) {
    $query->where('rating', 5);
}])

Подсчет отношений и пользовательские операторы SELECT

Если вы комбинируете withCount с оператором SELECT, убедитесь, что вы вызываете withCount после метода select:

$posts = Post::select(['title', 'body'])
                ->withCount('comments')
                ->get();

Другие агрегатные функции

Помимо метода withCount, Eloquent содержит методы withMin, withMax, withAvg, withSum и withExists. Эти методы добавят атрибут {relation}_{function}_{column} в ваши результирующие модели:

use App\Models\Post;

$posts = Post::withSum('comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->comments_sum_votes;
}

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

$posts = Post::withSum('comments as total_comments', 'votes')->get();

foreach ($posts as $post) {
    echo $post->total_comments;
}    

Как и метод loadCount, также доступны отложенные версии этих методов. Эти дополнительные агрегированные операции могут выполняться на уже полученных моделях Eloquent:

$post = Post::first();

$post->loadSum('comments', 'votes');

Если вы комбинируете эти агрегатные методы с оператором select, убедитесь, что вы вызываете их после метода select:

$posts = Post::select(['title', 'body'])
                ->withExists('comments')
                ->get();

Подсчет связанных моделей отношений Morph To

Если вы хотите загрузить полиморфное отношение «один-к», а также связанные счетчики моделей для различных сущностей, которые могут быть возвращены этим отношением, то вы можете использовать метод with в сочетании с отношениями morphTo – метод morphWithCount.

В этом примере предположим, что модели Photo и Post могут создавать модели ActivityFeed. Предположим, что модель ActivityFeed определяет полиморфное отношение «один-к» с именем parentable, которое позволяет нам получить родительскую модель Photo или Post для переданного экземпляра ActivityFeed. Кроме того, предположим, что модели Photo имеют много моделей Tag, а модели Post имеют много моделей Comment.

Теперь давайте представим, что мы хотим получить экземпляры ActivityFeed и загрузить родительские модели для каждого экземпляра ActivityFeed. Кроме того, мы хотим получить количество тегов, связанных с каждой родительской фотографией, и количество комментариев, связанных с каждым родительским постом:

use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::with([
    'parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWithCount([
            Photo::class => ['tags'],
            Post::class => ['comments'],
        ]);
    }])->get();

Отложенная загрузка подсчета связанных моделей отношений Morph To

Предположим, мы уже получили набор моделей ActivityFeed и теперь хотим загрузить счетчики вложенных отношений для различных родительских (parentable) моделей, связанных с ActivityFeed. Для этого вы можете использовать метод loadMorphCount:

$activities = ActivityFeed::with('parentable')->get();

$activities->loadMorphCount('parentable', [
    Photo::class => ['tags'],
    Post::class => ['comments'],
]);

Жадная (eager) загрузка

При доступе к отношениям Eloquent как к свойствам, связанные модели загружаются «отложенно». Это означает, что данные отношения фактически не загружаются, пока вы впервые не затребуете доступ к свойству. Однако Eloquent может «жадно» загрузить отношения во время запроса родительской модели. Жадная загрузка позволяет избежать проблем «N+1» с запросами. Чтобы проиллюстрировать проблему «N+1» запроса, рассмотрим модель Book, которая «принадлежит» модели Author:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * Получить автора книги.
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}

Теперь давайте получим все книги и их авторов:

use App\Models\Book;

$books = Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}

Этот цикл выполнит один запрос для получения всех книг из таблицы базы данных, затем еще один запрос для каждой книги, чтобы получить автора книги. Итак, если у нас есть 25 книг, приведенный выше код будет запускать 26 запросов: один для исходной книги и 25 дополнительных запросов для получения автора каждой книги.

К счастью, мы можем использовать жадную загрузку, чтобы сократить эту операцию до двух запросов. При построении запроса вы можете указать, какие отношения должны быть загружены с помощью метода with:

$books = Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}

Для этой операции будут выполнены только два запроса – один запрос для получения всех книг и один запрос – для получения всех авторов для всех книг:

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

Жадная загрузка множественных отношений

Иногда вам может понадобиться загрузить несколько разных отношений. Для этого просто передайте массив отношений методу with:

$books = Book::with(['author', 'publisher'])->get();

Вложенная жадная загрузка

Чтобы жадно загрузить отношения отношений, вы можете использовать «точечную нотацию». Например, давайте загрузим всех авторов книги и все личные контакты авторов:

$books = Book::with('author.contacts')->get();

Вложенная жадная загрузка отношений Morph To

Если вы хотите загрузить полиморфное отношение «один-к», а также вложенные отношения для различных сущностей, которые могут быть возвращены этим отношением, то вы можете использовать метод with в сочетании с отношениями morphTo – метод morphWith. Чтобы проиллюстрировать этот метод, давайте рассмотрим следующую модель:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * Получить родительский элемент записи ленты активности.
     */
    public function parentable()
    {
        return $this->morphTo();
    }
}

В этом примере предположим, что модели Event, Photo и Post могут создавать модели ActivityFeed. Кроме того, предположим, что модели Event принадлежат модели Calendar, модели Photo связаны с моделями Tag, а модели Post принадлежат модели Author.

Используя эти определения моделей и отношения, мы можем получить экземпляры модели ActivityFeed и жадно загрузить все родительские (parentable) модели и их соответствующие вложенные отношения:

use Illuminate\Database\Eloquent\Relations\MorphTo;

$activities = ActivityFeed::query()
    ->with(['parentable' => function (MorphTo $morphTo) {
        $morphTo->morphWith([
            Event::class => ['calendar'],
            Photo::class => ['tags'],
            Post::class => ['author'],
        ]);
    }])->get();

Жадная загрузка определенных столбцов

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

$books = Book::with('author:id,name,book_id')->get();

{note} При использовании этого функционала вы всегда должны включать столбец id и любые соответствующие столбцы внешнего ключа в список столбцов, которые вы хотите получить.

Жадная загрузка по умолчанию

Иногда требуется постоянная загрузка некоторых отношений при извлечении модели. Для этого вы можете определить свойство $with в модели:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * Отношения, которые всегда должны быть загружены.
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * Получить автора книги.
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }

    /**
     * Получить жанр книги
     */
    public function genre()
    {
        return $this->belongsTo(Genre::class);
    }
}

Если вы хотите удалить элемент из свойства $with для одного запроса, вы можете использовать метод without:

$books = Book::without('author')->get();

Если вы хотите переопределить все элементы свойства $with для одного запроса, вы можете использовать метод withOnly:

$books = Book::withOnly('genre')->get();    

Ограничение жадной загрузки

Иногда требуется жадная загрузка отношения с указанием дополнительного условия. Вы можете сделать это, передав массив отношений методу with, где ключ массива – это имя отношения, а значение массива – это функция, которая добавляет дополнительные ограничения к запросу жадной загрузки:

use App\Models\User;

$users = User::with(['posts' => function ($query) {
    $query->where('title', 'like', '%code%');
}])->get();

В этом примере Eloquent будет загружать только те посты, столбец title которых содержит слово code. Вы можете использовать и другие методы построителя запросов:

$users = User::with(['posts' => function ($query) {
    $query->orderBy('created_at', 'desc');
}])->get();

{note} Методы limit и take построителя запросов нельзя использовать при ограничении жадной загрузки.

Ограничение жадной загрузки отношений Morph To

Если вы хотите жадно загрузить полиморфное отношение «один-к», Eloquent выполнит несколько запросов для получения каждого типа связанной модели. Вы можете добавить дополнительные ограничения к каждому из этих запросов, используя метод constrain полиморфного отношения «один-к»:

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;

$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
    $morphTo->constrain([
        Post::class => function (Builder $query) {
            $query->whereNull('hidden_at');
        },
        Video::class => function (Builder $query) {
            $query->where('type', 'educational');
        },
    ]);
}])->get();

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

Жадная пост-загрузка

Иногда требуется жадно загрузить отношение только после получения родительской модели. Например, это может быть полезно, если вам нужно динамически решать, загружать ли связанные модели:

use App\Models\Book;

$books = Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}

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

$author->load(['books' => function ($query) {
    $query->orderBy('published_date', 'asc');
}]);

Чтобы загрузить отношение только в том случае, если оно еще не было загружено, используйте метод loadMissing:

$book->loadMissing('author');

Вложенная жадная пост-загрузка и отношения Morph To

Если вы хотите жадно загрузить полиморфное отношение «один-к», а также вложенные отношения для различных сущностей, которые могут быть возвращены этим отношением, вы можете использовать метод loadMorph.

Этот метод принимает имя полиморфного отношения «один-к» в качестве своего первого аргумента и массив пар модель / отношение в качестве второго аргумента. Чтобы проиллюстрировать этот метод, давайте рассмотрим следующую модель:

<?php

use Illuminate\Database\Eloquent\Model;

class ActivityFeed extends Model
{
    /**
     * Получить родительский элемент записи ленты активности.
     */
    public function parentable()
    {
        return $this->morphTo();
    }
}

В этом примере предположим, что модели Event, Photo и Post могут создавать модели ActivityFeed. Кроме того, предположим, что модели Event принадлежат модели Calendar, модели Photo связаны с моделями Tag, а модели Post принадлежат модели Author.

Используя эти определения моделей и отношения, мы можем получить экземпляры модели ActivityFeed и жадно загрузить все родительские (parentable) модели и их соответствующие вложенные отношения:

$activities = ActivityFeed::with('parentable')
    ->get()
    ->loadMorph('parentable', [
        Event::class => ['calendar'],
        Photo::class => ['tags'],
        Post::class => ['author'],
    ]);

Предотвращение ленивой загрузки

Как уже говорилось ранее, жадная загрузка отношений часто может обеспечить значительный выигрыш в производительности. Поэтому, если вы хотите, вы можете указать Laravel всегда предотвращать ленивую загрузку отношений. Для этого вы можете вызвать метод preventLazyLoading, предлагаемый базовым классом модели Eloquent. Обычно этот метод вызывается в методе boot класса AppServiceProvider вашего приложения.

Метод preventLazyLoading принимает необязательный аргумент boolean, который указывает, следует ли предотвратить ленивую загрузку. Например, вы можете отключить ленивую загрузку только в девелопмент-среде, чтобы ваша продакшн-среда продолжала нормально функционировать, если лениво загруженные отношения случайно присутствуют в коде на сервере:

use Illuminate\Database\Eloquent\Model;

/**
 * Bootstrap any application services.
 *
 * @return void
 */
public function boot()
{
    Model::preventLazyLoading(! $this->app->isProduction());
}

После отключения ленивой загрузки Eloquent будет выбрасывать исключение Illuminate\Database\LazyLoadingViolationException, когда ваше приложение попытается лениво загрузить любое отношение Eloquent.

Вы можете настроить это поведение с помощью метода handleLazyLoadingViolationsUsing. Например, используя этот метод, вы можете указать, что нарушения надо только регистрировать, а не выбрасывать исключение:

Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
    $class = get_class($model);

    info("Attempted to lazy load [{$relation}] on model [{$class}].");
});

Вставка и обновление связанных моделей

Метод Save

Eloquent содержит удобные методы для добавления новых моделей в отношения. Например, возможно, вам нужно добавить новый комментарий к посту. Вместо того чтобы вручную задавать атрибут post_id в модели Comment, вы можете вставить комментарий, используя метод отношения save:

use App\Models\Comment;
use App\Models\Post;

$comment = new Comment(['message' => 'A new comment.']);

$post = Post::find(1);

$post->comments()->save($comment);

Обратите внимание, что мы не обращались к связи comments как к динамическому свойству. Вместо этого мы вызвали метод comments, чтобы получить экземпляр отношения. Метод save автоматически добавит соответствующее значение post_id в новую модель Comment.

Если вам нужно сохранить несколько связанных моделей, вы можете использовать метод saveMany:

$post = Post::find(1);

$post->comments()->saveMany([
    new Comment(['message' => 'A new comment.']),
    new Comment(['message' => 'Another new comment.']),
]);

Методы save и saveMany не будут добавлять новые модели ни в какие отношения, хранимые в памяти, прежде загруженные в родительскую модель. Если вы планируете получить доступ к отношениям после использования методов save или saveMany, то вы можете использовать метод refresh для перезагрузки модели и ее отношений:

$post->comments()->save($comment);

$post->refresh();

// Все комментарии, включая только что сохраненный комментарий ...
$post->comments;

Рекурсивное сохранение моделей и отношений

Если вы хотите сохранить вашу модель и все связанные с ней отношения, вы можете использовать метод push. В этом примере будет сохранена модель Post, а также ее комментарии и авторы этих комментариев:

$post = Post::find(1);

$post->comments[0]->message = 'Message';
$post->comments[0]->author->name = 'Author Name';

$post->push();

Метод Create

В дополнение к методам save и saveMany вы также можете использовать метод create, который принимает массив атрибутов, создает модель и вставляет ее в базу данных. Разница между save и create в том, что save принимает полный экземпляр модели Eloquent, а create принимает простой массив PHP. Вновь созданная модель будет возвращена методом create:

use App\Models\Post;

$post = Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

Вы можете использовать метод createMany для создания нескольких связанных моделей:

$post = Post::find(1);

$post->comments()->createMany([
    ['message' => 'A new comment.'],
    ['message' => 'Another new comment.'],
]);

Вы также можете использовать методы findOrNew, firstOrNew, firstOrCreate, и updateOrCreate для создания и обновления моделей отношений.

{tip} Перед использованием метода create обязательно ознакомьтесь с документацией о массовом присвоении атрибутов.

Обновление отношений Один К

Если вы хотите назначить дочернюю модель новой родительской модели, вы можете использовать метод associate. В этом примере модель User определяет отношение belongsTo к модели Account. Метод associate установит внешний ключ дочерней модели:

use App\Models\Account;

$account = Account::find(10);

$user->account()->associate($account);

$user->save();

Чтобы удалить родительскую модель из дочерней модели, вы можете использовать метод dissociate. Этот метод установит для внешнего ключа отношения значение null:

$user->account()->dissociate();

$user->save();

Обновление отношений Многие ко многим

Присоединение / Отсоединение отношений Многие ко многим

Eloquent также содержит методы, которые делают работу с отношениями «многие-ко-многим» более удобной. Например, представим, что у пользователя может быть много ролей, а у роли может быть много пользователей. Вы можете использовать метод attach, чтобы присоединить роль к пользователю, вставив запись в промежуточную таблицу отношения:

use App\Models\User;

$user = User::find(1);

$user->roles()->attach($roleId);

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

$user->roles()->attach($roleId, ['expires' => $expires]);

Иногда может потребоваться удалить роль пользователя. Чтобы удалить запись отношения «многие-ко-многим», используйте метод detach. Метод detach удалит соответствующую запись из промежуточной таблицы; однако обе модели останутся в базе данных:

// Отсоединяем одну роль от пользователя ...
$user->roles()->detach($roleId);

// Отсоединяем от пользователя все роли ...
$user->roles()->detach();

Для удобства attach и detach также принимают в качестве входных данных массивы идентификаторов:

$user = User::find(1);

$user->roles()->detach([1, 2, 3]);

$user->roles()->attach([
    1 => ['expires' => $expires],
    2 => ['expires' => $expires],
]);

Синхронизация ассоциаций отношений Многие ко многим

Вы также можете использовать метод sync для построения ассоциаций «многие-ко-многим». Метод sync принимает массив идентификаторов для размещения в промежуточной таблице. Любые идентификаторы, которых нет в данном массиве, будут удалены из промежуточной таблицы. После завершения этой операции в промежуточной таблице будут существовать только ID из переданного массива:

$user->roles()->sync([1, 2, 3]);

Вы также можете вместе с ID передать дополнительные значения промежуточной таблицы:

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

Если вы хотите вставить одинаковые значения в промежуточную таблицу для каждого ID, вы можете использовать метод syncWithPivotValues:

$user->roles()->syncWithPivotValues([1, 2, 3], ['active' => true]);

Если вы не хотите удалять существующие связи, идентификаторы которых отсутствуют в переданном массиве, то вы можете использовать метод syncWithoutDetaching:

$user->roles()->syncWithoutDetaching([1, 2, 3]);

Переключение ассоциаций отношений Многие ко многим

Отношение «многие-ко-многим» также содержит метод toggle, который «переключает» статус присоединения указанных идентификаторов связанных моделей. Если переданный идентификатор в настоящее время присоединен, он будет отсоединен. Аналогично, если он в настоящее время отсоединен, то он будет присоединен:

$user->roles()->toggle([1, 2, 3]);

Обновление записи сводной таблицы отношений Многие ко многим

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

$user = User::find(1);

$user->roles()->updateExistingPivot($roleId, [
    'active' => false,
]);

Затрагивание временных меток родителя

Когда в модели определены методы belongsTo или belongsToMany по отношению к другой модели, например Comment, который принадлежит Post, то иногда бывает необходимо обновить временную метку родителя при обновлении дочерней модели.

Например, когда модель Comment обновляется, то вы можете автоматически «затронуть» временную метку updated_at родительской модели Post, чтобы она была установлена на текущую дату и время. Для этого вы можете добавить свойство $touches к дочерней модели, содержащее имена отношений, для которых должны обновляться временные метки updated_at при обновлении дочерней модели:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Все отношения, временные метки которых должны быть затронуты.
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * Получить пост, к которому принадлежит комментарий.
     */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

{note} Временные метки родительской модели будут обновлены только в том случае, если дочерняя модель обновлена с помощью метода save Eloquent.