Миграции в Laravel: первая таблица на живом примере
Разберём миграции Laravel на таблице заказов: как создать, изменить и откатить схему без ручного SQL. С командами, кодом и разбором частых ошибок.

Вы завели проект на Laravel и вам нужна таблица для заказов. Можно открыть клиент к базе и написать CREATE TABLE руками, но тогда коллега на другой машине получит другую схему, а вы через месяц не вспомните, почему у поля status именно такая длина. Миграции решают это: схема лежит в коде, версионируется в Git и применяется одной командой. Ниже — рабочий пример на Laravel 11.x: создаём таблицу заказов, добавляем в неё поле, откатываем изменение и разбираем, где всё это ломается.
Очевидное решение — держать SQL-скрипт в репозитории и накатывать его вручную — даёт проблему на первом же изменении схемы. Никто не знает, применён скрипт на конкретной базе или нет. Миграции хранят это состояние сами, в служебной таблице.
Что делает миграция и где она живёт
Миграция — это PHP-класс с двумя методами: up() применяет изменение, down() откатывает его. Файлы лежат в database/migrations, имя начинается с временной метки, поэтому Laravel применяет их в порядке создания.
Состояние Laravel хранит в таблице migrations: там записано, какие файлы уже применены и в каком «батче» (пачке). Именно по этой таблице команда migrate понимает, что накатывать, а migrate:rollback — что откатывать.
Миграция описывает изменение схемы, а не её конечное состояние. Поэтому таблицу нельзя один раз описать и потом править этот же файл — новое изменение всегда новый файл.
Создаём первую таблицу
Для практики нам нужна таблица заказов: id, сумма, статус, идентификатор клиента и стандартные метки времени.
Шаг 1. Сгенерировать файл миграции
Команда Artisan создаёт заготовку с уже подставленным именем таблицы:
php artisan make:migration create_orders_table
В консоли появится строка вроде INFO Migration [database/migrations/2024_XX_XX_XXXXXX_create_orders_table.php] created successfully. Файл с этим именем открываем в редакторе.
Шаг 2. Описать схему
Заполняем метод up(). Через объект Blueprint добавляем колонки нужных типов:
<?php
// database/migrations/..._create_orders_table.php
// Laravel 11.x
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('orders', function (Blueprint $table) {
$table->id(); // BIGINT автоинкремент, первичный ключ
$table->unsignedBigInteger('customer_id'); // ссылка на клиента
$table->decimal('amount', 10, 2); // сумма: 10 знаков, 2 после запятой
$table->string('status', 32)->default('new'); // статус заказа
$table->timestamps(); // created_at и updated_at
});
}
public function down(): void
{
Schema::dropIfExists('orders');
}
};
Обратите внимание: decimal для денег, не float — иначе на округлениях получите потерянные копейки. Метод down() должен полностью отменять то, что сделал up(): создали таблицу — значит, удаляем.
Шаг 3. Применить миграцию
php artisan migrate
В консоли вы увидите DONE напротив имени файла. Если это первый запуск на чистой базе, Laravel сам создаст служебную таблицу migrations. Проверить результат можно командой:
php artisan db:table orders
Она выведет список колонок с типами. Появились id, customer_id, amount, status, created_at, updated_at — значит, таблица создана.
Изменяем таблицу: добавляем поле
Через неделю понадобилось поле «дата оплаты». Старый файл править нельзя — на боевой базе миграция уже применена, и Laravel её не тронет. Создаём отдельную миграцию:
php artisan make:migration add_paid_at_to_orders_table
В ней не пересоздаём таблицу, а меняем существующую через Schema::table:
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
// nullable: у старых заказов даты оплаты нет
$table->timestamp('paid_at')->nullable()->after('status');
});
}
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('paid_at');
});
}
Новое поле делаем nullable(): если в таблице уже есть строки, а колонка объявлена NOT NULL без значения по умолчанию, база откажется её добавлять. Запускаем php artisan migrate — применится только новый файл, старый Laravel пропустит.
Откат: rollback, reset и refresh
Ошиблись в схеме — откатываем. Здесь три команды с разным охватом, и их часто путают.
| Команда | Что делает | Когда применять |
|---|---|---|
migrate:rollback | Откатывает последний батч миграций (все файлы из последнего migrate) | Ошибка в только что применённой миграции |
migrate:rollback --step=1 | Откатывает одну миграцию, а не весь батч | В батче было несколько файлов, а откатить нужно один |
migrate:reset | Откатывает вообще все миграции проекта | Полная очистка схемы через down() |
migrate:refresh | reset + повторный migrate | Пересобрать схему с нуля на dev-базе |
migrate:fresh | Дропает все таблицы напрямую и накатывает миграции заново | Локальная разработка, когда down() ненадёжны |
Разница между refresh и fresh практическая: refresh идёт через методы down(), а fresh удаляет таблицы напрямую и на down() не смотрит. Если ваши down() написаны небрежно, fresh всё равно очистит базу — но именно поэтому fresh опасен на боевом сервере: он снесёт данные без разговоров.
Если не получилось
- SQLSTATE[42S01] ... Table 'orders' already exists. Таблица уже создана, но в служебной
migrationsзаписи нет — например, вы создавали её руками. Удалите таблицу или синхронизируйте состояние, аккуратно добавив запись вmigrations. - Nothing to migrate. Все файлы уже применены. Проверьте, что новый файл лежит в
database/migrationsи имеет свежую временную метку в имени. - SQLSTATE[HY000]: General error: 1364 Field 'paid_at' doesn't have a default value. Добавляете
NOT NULL-колонку в непустую таблицу без значения по умолчанию. Сделайте полеnullable()или задайтеdefault(). - Cannot add foreign key constraint. Тип колонки внешнего ключа не совпадает с типом первичного ключа связанной таблицы.
id()создаётunsignedBigInteger— ключ на него объявляйте тем же типом. - SQLSTATE[08006] / Connection refused. Laravel не достучался до базы. Проверьте
DB_HOST,DB_PORT,DB_DATABASEв файле.envи что сервер БД запущен.
Что делать дальше
Когда схема готова, следующий шаг — наполнить таблицу тестовыми данными через сидеры (make:seeder) и фабрики, чтобы не вбивать строки руками при каждом fresh. Дальше стоит посмотреть на внешние ключи через foreignId()->constrained() — это удобнее ручного unsignedBigInteger плюс отдельного foreign. Отдельная большая тема — миграции на боевой базе с данными: как менять колонки без блокировок и простоя; здесь мы её не трогаем. Точный список методов Blueprint для вашей версии всегда сверяйте в официальной документации, раздел Database: Migrations.
IT-архитектор / SQL-оптимизатор: Поиск N+1, оптимизация запросов, code review, поиск багов.
Спросить за 20 ₽Источники: Laravel Documentation — Database: Migrations, Laravel Documentation — Migrations: Available Column Types (Blueprint)
Частые вопросы
А если у меня Laravel 10 или 9, код сработает?
Почему нельзя просто отредактировать старый файл миграции?
Можно ли запускать migrate:fresh на боевом сервере?
Что будет, если миграция упала на середине?
Как посмотреть, какие миграции уже применены?
Обязательно ли писать метод down()?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.