Middle: питання на співбесіді з теми «Міграції»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
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;
Загальне правило: розділяйте зміну схеми й заповнення даних. Міграція має бути швидкою операцією над структурою, а перенесення даних - командою, яку можна запустити, зупинити й продовжити.
Кожен запуск php artisan migrate записує виконані міграції в таблицю migrations з одним номером пакета (batch).
php artisan migrate:rollback # відкотити останній пакет цілком
php artisan migrate:rollback --step=1 # рівно одну останню міграцію
php artisan migrate:rollback --batch=3 # конкретний пакет
php artisan migrate:status # що виконано і в якому пакеті
Відкат викликає down(), тож вона має справді повертати попередній стан: видалити створену колонку, відновити змінений тип.
Інші команди - і чим вони небезпечні:
migrate:reset- відкотити все;migrate:refresh- відкотити все й виконати знову (проходить усіdown());migrate:fresh- видалити всі таблиці й виконати міграції з нуля,down()не викликається.
Останні три знищують дані - на продакшені їх не запускають, а локально з копією прод-бази - теж обережно.
На продакшені відкат міграції, яка видалила колонку з даними, не поверне дані. Тому руйнівні зміни роблять окремим релізом пізніше, а відкат релізу - новою міграцією вперед, а не rollback.
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignIdFor(User::class)->nullable()->constrained()->nullOnDelete();
$table->timestamps();
});
foreignId('post_id')->constrained()- колонкаunsignedBigIntegerплюс обмеження наposts.id(таблицю Laravel виводить з назви колонки);foreignIdFor(User::class)- назву колонки бере з моделі.
Що буде з дітьми при видаленні батька - вирішують явно:
cascadeOnDelete()- видалити разом (коментарі поста);nullOnDelete()- обнулити посилання (автор видалив акаунт, коментар лишається анонімним; колонка має бутиnullable);restrictOnDelete()- заборонити видалення, поки є діти (клієнт із рахунками).
Що варто знати:
- обмеження працює на рівні бази й обходить Eloquent - події моделей для каскадно видалених рядків не спрацюють;
- м'яке видалення батька каскад не запускає - це звичайний
UPDATE; - MySQL сам створює індекс для зовнішнього ключа, PostgreSQL - ні, тож там індекс на
post_idдодають окремо; - видалити ключ:
$table->dropForeign(['post_id']).