Базы данных: миграции и Query Builder
Миграции, схема, Query Builder, транзакции, сиды и фабрики.
Junior Что такое миграции и как с ними работать?
Миграции — версионирование схемы БД в коде: описание изменений, которые можно накатывать и откатывать в любом окружении.
php artisan make:migration create_posts_table
php artisan migrate # накатить новые
php artisan migrate:rollback # откатить последний батч
php artisan migrate:fresh --seed # дропнуть всё и накатить заново + сиды
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->boolean('published')->default(false);
$table->timestamps(); // created_at / updated_at
$table->softDeletes(); // deleted_at
$table->index('title');
});
}
Изменение таблицы: Schema::table('posts', ...) — ->nullable(), ->change(), dropColumn, renameColumn. Отслеживание накатанных — таблица migrations (батчи).
Junior Какие типы отношений внешних ключей и каскадов доступны в миграциях?
$table->foreignId('user_id')->constrained(); // users.id
$table->foreignId('user_id')->constrained('authors'); // authors.id
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
// Поведение при удалении/обновлении родителя:
->cascadeOnDelete(); // удалить дочерние
->restrictOnDelete(); // запретить удаление родителя
->nullOnDelete(); // обнулить FK (поле nullable)
->cascadeOnUpdate();
// Полный синтаксис:
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
Составные и обычные индексы:
$table->index('email');
$table->unique(['tenant_id', 'email']);
$table->fullText('body'); // полнотекстовый
$table->index(['a', 'b'], 'name'); // составной — порядок колонок важен
Помнить: foreignId сам по себе индекс не создаёт в некоторых СУБД — но constrained() в MySQL создаёт индекс автоматически.
Junior Что такое сиды и фабрики? Как их используют в разработке и тестах?
Сиды — наполнение БД начальными данными:
php artisan make:seeder UserSeeder
php artisan db:seed --class=UserSeeder
php artisan migrate:fresh --seed
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([RoleSeeder::class, UserSeeder::class]);
}
}
Фабрики — генерация моделей с фейковыми данными (Faker):
php artisan make:factory PostFactory
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'user_id' => User::factory(),
'published' => fake()->boolean(80),
];
}
public function published(): static // состояние
{
return $this->state(['published' => true]);
}
}
Использование:
Post::factory()->count(10)->published()->create(); // в БД
User::factory()->make(); // без сохранения
User::factory()->has(Post::factory(3))->create(); // со связями
В тестах — вместе с RefreshDatabase для детерминированных данных.
Middle Чем Query Builder отличается от Eloquent и сырых запросов? Когда что использовать?
Query Builder (DB::table()) — fluent-интерфейс поверх PDO, без моделей:
DB::table('users')
->where('active', true)
->orderByDesc('created_at')
->limit(10)
->get(); // коллекция stdClass
Eloquent (User::where(...)->get()) — модели с отношениями, событиями, мутаторами, но дороже по памяти и медленнее на массовых операциях.
Сырые запросы:
DB::select('SELECT * FROM users WHERE id = ?', [$id]); // биндинги — обязательно!
DB::statement('DROP TABLE ...');
DB::raw('COUNT(*) as total'); // фрагмент внутри builder'а
Когда что:
- CRUD с бизнес-логикой, отношения → Eloquent;
- массовые update/delete, отчёты, сложные join'ы → Query Builder;
- то, что не выразить builder'ом (специфика СУБД) → raw с биндингами.
Никогда не подставлять пользовательский ввод в raw-строки конкатенацией — SQL-инъекция.
Middle Как работают транзакции в Laravel?
DB::transaction(function () {
$order = Order::create([...]);
$order->items()->createMany($items);
// любое исключение → откат обеих операций
}, 5); // 5 попыток при deadlock
Ручное управление:
DB::beginTransaction();
try {
// ...
DB::commit();
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
Важные нюансы:
- исключение внутри замыкания автоматически откатывает транзакцию и пробрасывается дальше;
returnиз замыкания — нормальный commit;- события моделей (created и т.п.) и queued jobs внутри транзакции опасны: джоба может выполниться до commit →
afterCommit()илиBus::dispatch(...)->afterCommit(); - вложенные транзакции — через savepoint'ы;
- DDL в MySQL не транзакционен — миграции не откатываются транзакциями.
Middle Как выполнить пагинацию и в чём разница paginate(), simplePaginate() и cursorPaginate()?
$users = User::paginate(15); // с общим числом страниц
$users = User::simplePaginate(15); // только «назад/вперёд», без COUNT(*)
$users = User::cursorPaginate(15); // по курсору (WHERE id > ?)
В шаблоне: {{ $users->links() }}.
paginate()делает дополнительныйCOUNT(*)— нужен для «страница 3 из 120», но дорог на больших таблицах;simplePaginate()без COUNT — дешевле, но нет номеров страниц;cursorPaginate()использует упорядоченный курсор (WHERE id > X LIMIT n) — O(1) независимо от глубины, защищён от «сдвига» данных при вставках, но нельзя перепрыгнуть на произвольную страницу. Подходит для бесконечных лент и API.
Для больших выгрузок без HTTP — chunk() / chunkById() / cursor() (lazy-итерация, одна запись в памяти за раз).