Урок 04

Внешние ключи

Что такое 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.

Дальше — изменение существующих таблиц.