Внешние ключи
Что такое FK
Foreign Key — колонка, ссылающаяся на первичный ключ другой таблицы.
Обеспечивает целостность: нельзя создать запись с несуществующей ссылкой.
Создание — способ 1
foreignId() + constrained():
$table->foreignId('user_id')->constrained();
Что создаёт:
- Колонку
user_id BIGINT UNSIGNED. - Внешний ключ на
users.id.
Как определяется таблица: из имени колонки.
user_id→users.category_id→categories.author_id→authors.
Способ 2 — явное имя
$table->foreignId('author_id')->constrained('users');
Когда имя модели не совпадает с колонкой.
Способ 3 — вручную
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
Полный контроль: можно указать несвойственные колонки.
Способ 4 — с каскадом
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();
Поведение при удалении
cascadeOnDelete() — при удалении родителя удалить связанные.
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
Что происходит:
DELETE FROM users WHERE id = 5;
→ автоматически DELETE FROM posts WHERE user_id = 5
nullOnDelete() — обнулить FK.
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
⚠️ Требует nullable(). Иначе — ошибка.
restrictOnDelete() — запретить удаление, если есть связанные.
$table->foreignId('user_id')->constrained()->restrictOnDelete();
noActionOnDelete() — ничего не делать (по умолчанию).
setNullOnDelete() — то же, что nullOnDelete.
Поведение при обновлении
->cascadeOnUpdate()
->restrictOnUpdate()
->nullOnUpdate()
Обычно не нужно — ID не меняются.
Полный синтаксис
$table->foreign('user_id')
->references('id')
->on('users')
->onDelete('cascade')
->onUpdate('cascade');
Значения onDelete: cascade, set null, restrict, no action, set default.
Составной FK
Ссылка на составной PK:
$table->foreign(['user_id', 'role_id'])
->references(['id', 'role_id'])
->on('users');
Редкий случай.
nullable FK
Если связь опциональна:
$table->foreignId('category_id')
->nullable()
->constrained()
->nullOnDelete();
Порядок важен: nullable до constrained.
Именование
По умолчанию: posts_user_id_foreign.
Своё имя:
$table->foreignId('user_id')
->constrained('users', 'custom_fk_name');
Удаление FK
$table->dropForeign(['user_id']);
$table->dropForeign('posts_user_id_foreign'); // по имени
Через dropConstrainedForeignId:
$table->dropConstrainedForeignId('user_id');
Что делает: удаляет FK и колонку.
Порядок создания таблиц
⚠️ FK требует существования родителя.
Проблема:
posts создаётся до users → FK fails
Решение 1 — миграция users раньше:
Дата в имени файла определяет порядок. Создай create_users_table до create_posts_table.
Решение 2 — отключить проверки:
Schema::disableForeignKeyConstraints();
// миграции
Schema::enableForeignKeyConstraints();
⚠️ Опасно — можно создать битую схему.
Практика
Blog:
// 1. users (если ещё нет)
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
});
// 2. categories
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
// 3. posts (ссылается на users и categories)
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('category_id')
->nullable()
->constrained()
->nullOnDelete();
$table->string('title');
$table->text('body');
$table->timestamps();
});
// 4. comments
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->text('body');
$table->timestamps();
});
Что произойдёт при удалении пользователя:
- Его посты → удалены (cascade).
- Его комментарии → удалены (cascade).
- Комментарии к его постам → удалены (cascade).
Что произойдёт при удалении категории:
category_idу постов → NULL.
Частые ошибки
1. Забыли constrained:
$table->foreignId('user_id'); // только колонка, без FK
// Правильно:
$table->foreignId('user_id')->constrained();
2. nullOnDelete без nullable:
$table->foreignId('user_id')->constrained()->nullOnDelete();
// Error: FK не может быть NULL
// Правильно:
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
3. FK до создания таблицы:
posts создаётся до users → FK fails
4. Удаление родителя без каскада:
DELETE FROM users WHERE id = 5;
→ ERROR: FK constraint violation
Решение: cascadeOnDelete() или сначала удалить связанные.
5. Тип FK не совпадает с PK:
$table->unsignedBigInteger('user_id'); // BIGINT
// а users.id — INT (в старых проектах)
Правильно: используй foreignId.
Итог
foreignId('user_id')->constrained()— основной способ.cascadeOnDelete— удалить связанные.nullOnDelete— обнулить.restrictOnDelete— запретить.- Порядок таблиц — родитель раньше.
foreignIdвсегда unsigned BIGINT — совпадает сid.- Правило: для сильной связи — cascade, для опциональной — null.
Дальше — изменение существующих таблиц.