Миграции: версионирование базы данных

Миграции — это «версии» структуры базы данных в виде PHP-кода: они позволяют создавать и менять таблицы предсказуемо и одинаково на всех машинах команды.

Суть: вместо ручного SQL вы описываете таблицы PHP-кодом в файлах-миграциях. Метод up() применяет изменения, down() — откатывает. Команда php artisan migrate прогоняет их.

Как синхронизировать структуру базы между разработчиками? Если каждый вручную создаёт таблицы через GUI, рано или поздно у всех будут разные схемы, и проект сломается. Миграции решают это: структура БД описывается кодом, который хранится в git вместе с проектом. Любой разработчик запускает одну команду — и получает точно такую же базу.

Миграция — это файл в database/migrations с временной меткой в имени (порядок применения). Внутри два метода: up() описывает, что сделать (создать таблицу, добавить колонку), down() — как откатить. Благодаря этому изменения можно безопасно применять и отменять.

Создание миграции

# создать миграцию для таблицы products
php artisan make:migration create_products_table

# применить все новые миграции
php artisan migrate

# откатить последнюю партию
php artisan migrate:rollback
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();                       // первичный ключ
            $table->string('name');             // VARCHAR
            $table->text('description')->nullable();
            $table->decimal('price', 8, 2);     // цена
            $table->boolean('is_active')->default(true);
            $table->timestamps();               // created_at, updated_at
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Как работает под капотом

Laravel хранит специальную таблицу migrations, где отмечает, какие файлы уже применены. Когда вы запускаете migrate, фреймворк смотрит, каких миграций ещё нет в этой таблице, и выполняет их по порядку временных меток, превращая методы Schema в SQL-команды CREATE TABLE. Откат читает ту же таблицу и выполняет down() в обратном порядке.

  database/migrations/
  +-----------------------------------+
  | 2024_01_01_create_users_table     | -> applied
  | 2024_01_02_create_products_table  | -> applied
  | 2024_01_03_add_sku_to_products    | -> PENDING
  +-----------------------------------+
            |
   php artisan migrate
            v
  выполняет только PENDING, пишет в таблицу migrations

Смоделируем учёт применённых миграций на Python: применяем только те, которых ещё нет в журнале.

Попробуй сам ▶

# Менеджер миграций: применяет только новые
all_migrations = [
    '2024_01_01_create_users',
    '2024_01_02_create_products',
    '2024_01_03_add_sku',
]
applied = {'2024_01_01_create_users', '2024_01_02_create_products'}

def migrate(all_m, applied):
    for m in all_m:
        if m in applied:
            print('пропуск (уже есть):', m)
        else:
            print('ПРИМЕНЯЮ        :', m)
            applied.add(m)

migrate(all_migrations, applied)
print('Итого применено:', len(applied))

Частые ошибки

  • Править применённую миграцию. Если миграция уже выполнена у коллег, меняйте схему новой миграцией, а не редактируйте старую.
  • Пустой down(). Без корректного отката migrate:rollback не сработает.
  • migrate:fresh на проде. Эта команда удаляет все таблицы — на боевом сервере это потеря данных.

Best practices

  • Делайте каждую миграцию атомарной: одно логическое изменение на файл.
  • Всегда заполняйте down(), чтобы откат работал.
  • Используйте nullable(), default() и индексы прямо в схеме.

Миграции описывают не только создание таблиц, но и их изменение. Команда php artisan make:migration add_sku_to_products_table создаёт миграцию, где в up() вызывают Schema::table('products', ...) и добавляют колонку, а в down() — удаляют её. Так структура развивается версия за версией, и каждый шаг обратим. Отдельная важная тема — внешние ключи: метод $table->foreignId('user_id')->constrained()->cascadeOnDelete() создаёт колонку, связывает её с таблицей users и настраивает каскадное удаление. Это переносит часть логики целостности на уровень базы, где она надёжнее всего. Полезны и индексы: $table->index('email') ускоряет поиск, а $table->unique('email') гарантирует уникальность на уровне СУБД. Продумывание индексов на этапе миграции избавляет от мучительной оптимизации запросов в будущем, когда таблица разрастётся.

Итог: миграции версионируют структуру БД кодом, синхронизируя её между всеми разработчиками. Дальше познакомимся с Eloquent — слоем, который превращает строки таблиц в удобные PHP-объекты.

Проверьте себя
1. Что делает метод down() в миграции?
AСоздаёт таблицу
BОткатывает изменения, сделанные в up()
CЗапускает сервер
DУдаляет модель
2. Как Laravel понимает, какие миграции уже применены?
AПо дате файла
BХранит запись в специальной таблице migrations
CСпрашивает разработчика
DНикак, применяет все каждый раз