Подготовка: внешние ключи в миграциях:
Связи опираются на внешние ключи в базе, так что начинаем с миграции (как в главе 4). Допустим, у нас блог: таблица users уже есть из коробки, добавим posts. Создаём модель сразу с миграцией:
Код: Выделить всё
php artisan make:model Post -mКод: Выделить всё
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->timestamps();
});hasMany и belongsTo:
Это пара, описывающая связь "один ко многим" с двух сторон. У пользователя много постов, пост принадлежит одному пользователю:
Код: Выделить всё
// app/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasMany;
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
// app/Models/Post.php
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}Дальше связь доступна как свойство:
Код: Выделить всё
$user = User::find(1);
foreach ($user->posts as $post) {
echo $post->title;
}
$post = Post::find(10);
echo $post->user->name;Код: Выделить всё
$drafts = $user->posts()->where('published', false)->get();
$user->posts()->create([
'title' => 'Новый пост',
'body' => 'Текст',
]);Код: Выделить всё
$post->user()->associate($user); // проставит user_id
$post->save();
$post->user()->dissociate(); // user_id станет null
$post->save();hasOne и belongsToMany:
hasOne работает как hasMany, но возвращает одну модель вместо коллекции. Классический пример: у пользователя один профиль с настройками. Объявляется так же: return $this->hasOne(Profile::class), тип возврата HasOne.
belongsToMany описывает "многие ко многим" и требует промежуточную таблицу. Пост может иметь несколько тегов, тег вешается на несколько постов. По соглашению промежуточная таблица называется из двух имён моделей в единственном числе и в алфавитном порядке: post_tag. В ней два столбца, post_id и tag_id. Отдельной модели под неё не нужно, поэтому генерируем чистую миграцию:
Код: Выделить всё
php artisan make:migration create_post_tag_tableКод: Выделить всё
Schema::create('post_tag', function (Blueprint $table) {
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['post_id', 'tag_id']);
});Код: Выделить всё
$post->tags()->attach([1, 3]);
$post->tags()->sync([2, 3]); // в итоге останутся ровно эти два тегаПроблема N+1 и жадная загрузка:
Самая дорогая ошибка новичка. Выводим список из 50 постов с именами авторов: Post::all(), затем в цикле $post->user->name. Получается 51 запрос к базе: один за постами и по одному за каждым автором на каждой итерации. Лечится методом with(), он подгрузит всех авторов одним дополнительным запросом:
Код: Выделить всё
$posts = Post::with('user')->get();Код: Выделить всё
$posts = Post::with('user.profile')->get(); // авторы вместе с их профилями
$posts->load('tags'); // догрузить ещё одну связь у готовой коллекцииЧтобы N+1 не доезжал до прода, включите штатный страж, он есть с Laravel 8.43:
Код: Выделить всё
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}Типичные грабли:
Имена внешних ключей. Eloquent угадывает ключ по имени метода: belongsTo(User::class) в методе user() ищет колонку user_id. Назовёте метод author(), и Eloquent будет искать author_id. Либо называйте колонку соответственно, либо передайте ключ вторым аргументом: belongsTo(User::class, 'user_id').
Забытый with(). Поставьте laravel-debugbar в dev-окружении и поглядывайте на счётчик запросов. Сотня запросов на простом списке почти всегда означает N+1. А preventLazyLoading() из раздела выше превратит каждое такое место в явное исключение.
Имя промежуточной таблицы. Назовёте её tag_post вместо post_tag, и при первом же обращении к связи получите QueryException вида "Base table or view not found" (в SQLite "no such table"): Eloquent построит JOIN на несуществующую таблицу post_tag. Либо соблюдайте алфавитный порядок, либо укажите имя вторым аргументом: belongsToMany(Tag::class, 'tag_post').
Массовое заполнение через связь. $user->posts()->create() уважает $fillable модели Post так же, как обычный create(). Тонкость в том, когда именно прилетит ошибка: MassAssignmentException бросается, только если модель закрыта полностью (пустой $fillable при дефолтном $guarded). Если $fillable заполнен, но какого-то поля в нём нет, Eloquent по умолчанию молча отбросит это поле, и запись уедет в базу без него. Чтобы тихие потери стали ошибками, включите рядом с preventLazyLoading() ещё Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()), метод доступен с Laravel 9.28.
Итог:
Теперь вы умеете связывать модели: hasMany и belongsTo для "один ко многим", hasOne для "один к одному", belongsToMany для "многие ко многим". И знаете, как with(), load() и preventLazyLoading() спасают от N+1. Есть и более редкие типы вроде hasManyThrough и полиморфных связей, они строятся на тех же принципах, доберётесь по мере надобности. В следующей главе займёмся формами и валидацией, и связи пригодятся сразу: будем сохранять данные из форм в связанные модели.