Plnění dat přes Seeders, Eager Loading with() a prevence problému N+1
Jak efektivně plnit databázi výchozími daty pomocí Seederů a jak se vyhnout nejčastější výkonnostní pasti relačních databází: N+1 dotazům pomocí Eager Loadingu with().
Idempotentní Seeders a zabiják výkonu: Problém N+1 dotazů
Správa databázových dat zahrnuje dva klíčové koncepty: jak data spolehlivě do databáze dostat (Seeders) a jak je při čtení načítat tak, aby aplikace nezkolabovala pod tíhou stovek zbytečných SQL dotazů (Eager Loading).
V této lekci prozkoumáme kód našeho DatabaseSeeder a kontroléru CourseController.
1. Zlaté pravidlo Seederů: Vždy pište idempotentní kód
Při nasazení aplikace a v CI/CD pipelinách potřebujete spouštět seeder opakovaně bez obav, že databáze selže na chybě Integrity constraint violation: UNIQUE constraint failed.
Proto v našich seederech nepoužíváme CourseLesson::create([...]), ale metodu updateOrCreate():
$module = CourseModule::updateOrCreate(
['slug' => 'postgresql-migrace-a-eloquent-orm'], // Hledací kritérium
[
'number' => 6,
'title' => '...',
// ... Aktualizované atributy
]
);
Pokud modul se zadaným slug existuje, Eloquent jej pouze aktualizuje novými texty z kódu. Pokud neexistuje, vytvoří jej. Stejně tak u kvízových otázek nejprve provedeme $lesson->quizQuestions()->delete() a následně $lesson->quizQuestions()->createMany([...]). Seeder je tak 100% idempotentní – můžete jej spustit stokrát za sebou a výsledek bude vždy identický.
2. Noční můra vývojářů: N+1 Query Problem
Představte si, že v šabloně vypisujete přehled všech 10 modulů a u každého chcete zobrazit seznam jeho lekcí a u každé lekce počet kvízových otázek:
{{-- ❌ ŠPATNĚ: Typická N+1 past --}}
@foreach($modules as $module)
<h2>{{ $module->title }}</h2>
@foreach($module->lessons as $lesson)
<p>{{ $lesson->title }} (otázek: {{ $lesson->quizQuestions->count() }})</p>
@endforeach
@endforeach
Co se stane, pokud v controlleru načtete data prostým $modules = CourseModule::all();?
- Spustí se 1 dotaz na načtení modulů:
SELECT * FROM course_modules;(vrátí např. 10 řádků). - Při průchodu cyklem se pro každý modul spustí samostatný SQL dotaz:
SELECT * FROM course_lessons WHERE course_module_id = 1;(10 dotazů). - Uvnitř pro každou z celkových 30 lekcí se spustí další dotaz:
SELECT * FROM quiz_questions WHERE course_lesson_id = 1;(30 dotazů).
Výsledek? Pro zobrazení jedné jednoduché stránky odeslala aplikace do PostgreSQL 41 samostatných SQL dotazů! Při větším počtu dat se aplikace fatálně zpomalí kvůli síťové latenci spojení s databází.
3. Řešení: Eager Loading s metodou with()
Eloquent nabízí elegantní řešení zvané Eager Loading (dopředné načtení). Místo líného dotazování v cyklu (Lazy Loading) řekneme Eloquentu předem, jaké vazby budeme potřebovat.
Podívejte se do našeho souboru app/Http/Controllers/CourseController.php:
public function index(Request $request, StudentProgressService $progressService): View
{
$modules = CourseModule::with(['lessons.quizQuestions'])
->orderBy('number')
->get();
// ...
}
Díky tečkové notaci lessons.quizQuestions v metodě with() Eloquent provede přesně 3 SQL dotazy:
SELECT * FROM course_modules ORDER BY number;SELECT * FROM course_lessons WHERE course_module_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) ORDER BY number;SELECT * FROM quiz_questions WHERE course_lesson_id IN (1, 2, 3, ..., 30) ORDER BY position;
Ať už máte v databázi 10 nebo 10 000 lekcí, počet SQL dotazů zůstává konstantní: přesně 3. Eloquent načtené objekty sám v paměti pospojuje do příslušných relačních kolekcí.
// app/Http/Controllers/CourseController.php
public function index(
Request $request,
StudentProgressService $progressService,
AnatomyInspectorService $inspectorService,
): View {
// Eager Loading eliminuje N+1 problém:
// Načte všechny moduly, lekce i jejich kvízy v POUHÝCH 3 SQL dotazech!
$modules = CourseModule::with(['lessons.quizQuestions'])
->orderBy('number')
->get();
$totalLessons = (int) $modules->sum(fn (CourseModule $m): int => $m->lessons->count());
$rawToken = $request->cookie(StudentProgressService::COOKIE_NAME);
$student = $request->attributes->get('studentProfile');
if (! ($student instanceof StudentProfile)) {
$student = $progressService->resolveProfile(is_string($rawToken) ? $rawToken : null);
}
$stats = $progressService->getProgressStats($student, $totalLessons);
return view('course.index', [
'modules' => $modules,
'stats' => $stats,
'student' => $student,
'lifecyclePhases' => LifecyclePhase::cases(),
]);
}
🎯 Ověření znalostí z lekce
Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.
Co znamená pojem "N+1 query problem" v kontextu databázového ORM?
Jak v Laravelu správně zapíšete Eager Loading pro vnořenou relaci (např. načtení lekcí i jejich otázek u modulu)?
Proč je v databázových seederech klíčové používat metodu updateOrCreate() namísto create()?
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Database\Seeders; |
| 6 | |
| 7 | use Database\Seeders\Curriculum\Module01ModernPhpSeeder; |
| 8 | use Database\Seeders\Curriculum\Module02AnatomyLifecycleSeeder; |
| 9 | use Database\Seeders\Curriculum\Module03RoutingControllersSeeder; |
| 10 | use Database\Seeders\Curriculum\Module04BladeFrontendSeeder; |
| 11 | use Database\Seeders\Curriculum\Module05LivewireReactiveSeeder; |
| 12 | use Database\Seeders\Curriculum\Module06DatabaseEloquentSeeder; |
| 13 | use Database\Seeders\Curriculum\Module07CleanArchitectureSeeder; |
| 14 | use Database\Seeders\Curriculum\Module08SecurityApiSeeder; |
| 15 | use Database\Seeders\Curriculum\Module09TestingArtisanSeeder; |
| 16 | use Database\Seeders\Curriculum\Module10DockerHetznerSeeder; |
| 17 | use Illuminate\Database\Seeder; |
| 18 | |
| 19 | class DatabaseSeeder extends Seeder |
| 20 | { |
| 21 | /** |
| 22 | * Seed the application's database with all 10 course modules. |
| 23 | */ |
| 24 | public function run(): void |
| 25 | { |
| 26 | $seeders = [ |
| 27 | Module01ModernPhpSeeder::class, |
| 28 | Module02AnatomyLifecycleSeeder::class, |
| 29 | Module03RoutingControllersSeeder::class, |
| 30 | Module04BladeFrontendSeeder::class, |
| 31 | Module05LivewireReactiveSeeder::class, |
| 32 | Module06DatabaseEloquentSeeder::class, |
| 33 | Module07CleanArchitectureSeeder::class, |
| 34 | Module08SecurityApiSeeder::class, |
| 35 | Module09TestingArtisanSeeder::class, |
| 36 | Module10DockerHetznerSeeder::class, |
| 37 | ]; |
| 38 | |
| 39 | foreach ($seeders as $seederClass) { |
| 40 | if (class_exists($seederClass)) { |
| 41 | $this->call($seederClass); |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | |