Питання на співбесіді: Міграції
Найпопулярніші питання з реальних Laravel/PHP співбесід для всіх рівнів
3 питання
Міграція - це контроль версій для структури бази даних. Замість того щоб писати SQL вручну й узгоджувати його між розробниками, ви описуєте схему PHP-кодом, який комітиться в git і застосовується однаково в усіх середовищах.
Міграції зберігаються в директорії database/migrations. Кожен файл - це клас із двома методами:
up()- описує зміни, які треба застосувати (створити таблицю, додати колонку, індекс).down()- описує зворотну операцію для відкату (видалити таблицю чи колонку).
Створити міграцію:
php artisan make:migration create_posts_table
Згенерований файл заповнюють у методі up():
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
Основні команди:
php artisan migrate- застосувати нові міграції.php artisan migrate:rollback- відкотити останній «батч».php artisan migrate:fresh --seed- перестворити всі таблиці й засіяти даними.
Навіщо: будь-який розробник на новій машині отримує ідентичну схему БД однією командою, а зміни структури проходять через code review разом із кодом.
migrate:rollback відкочує останній «батч» міграцій, викликаючи їхні методи down():
php artisan migrate:rollback # останній батч
php artisan migrate:rollback --step=1 # рівно одну міграцію
migrate:reset відкочує всі міграції по черзі. migrate:refresh - відкочує все й накатує заново. migrate:fresh - видаляє всі таблиці й накатує міграції з нуля, взагалі не заглядаючи в down().
Що з цього небезпечне в проді: усі чотири. Кожна знищує дані, а migrate:fresh робить це найшвидше й без шансу на down(). У проді припустимий лише php artisan migrate.
Laravel сам питає підтвердження в продакшн-середовищі, і саме тому --force не варто вписувати в скрипти «щоб не заважало».
Практика, що рятує:
- Перевіряйте відкат локально одразу після написання:
migrate→migrate:rollback --step=1→migrate. Половина міграцій має неробочийdown(), і виявляється це в найгірший момент. - Пишіть
down()чесно, а якщо відкат неможливий - хай кидає виняток, це краще за мовчазну порожню реалізацію. - Не редагуйте вже застосовану міграцію - додавайте нову. Виправлений файл не перезастосується там, де він уже відпрацював.
- Видалення колонки й перейменування - окремими релізами від коду, що їх читає, інакше деплой ламає працюючий застосунок у проміжку.
На таблиці в кілька мільйонів рядків необережна міграція блокує запис на хвилини, і застосунок лежить.
Що блокує довго:
- Додавання колонки
NOT NULLзі значенням за замовчуванням у старих версіях MySQL переписує таблицю цілком. У MySQL 8 і PostgreSQL 11+ це вже метадані, але перевіряти варто саме свою версію. - Створення індексу звичайним
CREATE INDEXтримає таблицю на час побудови. - Зміна типу колонки майже завжди означає перезапис.
Безпечний порядок для нової колонки:
// 1. Спершу nullable - миттєво, без перезапису.
Schema::table('vacancies', function (Blueprint $table) {
$table->string('short_name')->nullable()->after('title');
});
Далі заповнити дані пачками - окремою командою, не в міграції:
Vacancy::whereNull('short_name')->chunkById(500, function ($vacancies) {
foreach ($vacancies as $vacancy) {
$vacancy->update(['short_name' => Str::limit($vacancy->title, 40, '')]);
}
});
І лише потім, якщо потрібно, робити колонку обовʼязковою - окремою міграцією, коли даних без значення вже немає.
Індекс без блокування:
// PostgreSQL
DB::statement('CREATE INDEX CONCURRENTLY vacancies_level_index ON vacancies (level)');
CONCURRENTLY не можна виконувати всередині транзакції, тож у міграції потрібно вимкнути обгортання:
public $withinTransaction = false;
Загальне правило: розділяйте зміну схеми й заповнення даних. Міграція має бути швидкою операцією над структурою, а перенесення даних - командою, яку можна запустити, зупинити й продовжити.