Databázové migrace v PostgreSQL: Tabulky, cizí klíče a JSONB sloupce
Jak Laravel migrace definují databázové schéma pro PostgreSQL 18. Od cizích klíčů s kaskádovým mazáním přes optimalizované datové typy až po moderní JSONB sloupce.
Databázové migrace jako verzovací systém pro PostgreSQL
V moderním vývoji softwaru je nemyslitelné spravovat databázové tabulky ručními zásahy přes grafické rozhraní. Databázové schéma musí být verzováno v repozitáři stejně jako aplikační kód.
V Laravelu tuto úlohu plní Migrace (uložené v adresáři database/migrations/). Při nasazení na server stačí spustit příkaz php artisan migrate, který automaticky aplikuje všechny nové migrace.
V našem projektu běží nejnovější verze PostgreSQL 18 (v Docker kontejneru). Podívejme se, jak je schéma naší vzdělávací platformy definováno v souboru database/migrations/0001_01_01_000010_create_course_tables.php.
1. Integrita dat a cizí klíče s kaskádovým mazáním
Základem relační databáze je integrita vztahů mezi tabulkami. V naší migraci vytváříme hierarchii course_modules → course_lessons → quiz_questions:
Schema::create('course_lessons', function (Blueprint $table): void {
$table->id();
$table->foreignId('course_module_id')->constrained('course_modules')->cascadeOnDelete();
$table->unsignedSmallInteger('number');
$table->string('slug')->unique();
// ...
});
Co se zde odehrává uvnitř PostgreSQL?
$table->foreignId('course_module_id')vytvoří sloupec typuBIGINTodpovídající primárnímu klíčiidrodiče.->constrained('course_modules')vytvoří skutečné databázové omezení cizího klíče (FOREIGN KEY CONSTRAINT). PostgreSQL odmítne vložit lekci, pokud zadaný modul neexistuje.->cascadeOnDelete()přidá pravidloON DELETE CASCADE. Pokud je smazán modul, PostgreSQL na úrovni své databázové enginy automaticky a atomicky smaže všechny jeho lekce (a díky stejnému pravidlu vquiz_questionsi všechny navazující kvízové otázky). V databázi tak nikdy nezůstanou sirotčí záznamy.
2. Efektivní datové typy a indexy
Laravel Blueprint nabízí širokou škálu typů optimalizovaných pro PostgreSQL:
$table->unsignedSmallInteger('number'): Pro čísla modulů a lekcí (1 až 10) by byl klasickýinteger(4 bajty) nebobigInteger(8 bajtů) zbytečným plýtváním.unsignedSmallInteger(2 bajty,SMALLINT) šetří místo na disku i v mezipaměti RAM.$table->string('slug')->unique(): Vytváří unikátní B-Tree index na sloupcislug. Tento index je nezbytný pro bleskurychlé vyhledávání lekcí podle URL v Route Model Bindingu (O(log n)namísto pomalého full-table scanuO(n)).
3. Využití nativního typu JSON v PostgreSQL
V tabulkách course_lessons a student_profiles najdete sloupce:
$table->json('anatomy_files');
$table->json('completed_lessons');
$table->json('quiz_scores');
V PostgreSQL je JSON prvotřídním občanem. Oproti některým starším databázím, kde byl JSON pouze obyčejným textem, PostgreSQL validuje syntaktickou správnost JSON dokumentu a umožňuje v něm vyhledávat pomocí specializovaných JSON operátorů (->, ->>, @>).
4. Konfigurace spojení v config/database.php
V konfiguračním souboru config/database.php najdete nastavení ovladače 'pgsql':
'search_path' => 'public': Určuje výchozí PostgreSQL schéma.'sslmode' => env('DB_SSLMODE', 'prefer'): Umožňuje bezpečné šifrované spojení s databází v produkčním prostředí Hetzneru.
// database/migrations/0001_01_01_000010_create_course_tables.php
Schema::create('course_lessons', function (Blueprint $table): void {
$table->id();
// Vytvoří cizí klíč s automatickým kaskádovým mazáním v PostgreSQL
$table->foreignId('course_module_id')->constrained('course_modules')->cascadeOnDelete();
$table->unsignedSmallInteger('number');
$table->string('slug')->unique(); // B-Tree unikátní index pro Route Model Binding
$table->string('title');
$table->text('summary');
$table->longText('content_markdown');
$table->longText('code_snippet')->nullable();
$table->json('anatomy_files'); // PostgreSQL JSON datový typ
$table->text('anatomy_note')->nullable();
$table->string('lifecycle_phase')->default('controller');
$table->timestamps();
});
Schema::create('quiz_questions', function (Blueprint $table): void {
$table->id();
$table->foreignId('course_lesson_id')->constrained('course_lessons')->cascadeOnDelete();
$table->unsignedSmallInteger('position')->default(1);
$table->text('question');
$table->json('options'); // Pole 4 možností v JSON formátu
$table->unsignedSmallInteger('correct_option');
$table->text('explanation');
$table->timestamps();
});
🎯 Ověření znalostí z lekce
Kvíz otestuje pochopení architektury a reálného kódu probíraného v této lekci.
Co přesně zajistí deklarace ->cascadeOnDelete() u cizího klíče v migraci pro PostgreSQL?
Proč je u sloupce slug v tabulce course_lessons použita metoda ->unique()?
Kde Laravel eviduje, které migrační soubory již byly v databázi spuštěny?
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | use Illuminate\Database\Migrations\Migration; |
| 6 | use Illuminate\Database\Schema\Blueprint; |
| 7 | use Illuminate\Support\Facades\Schema; |
| 8 | |
| 9 | return new class extends Migration |
| 10 | { |
| 11 | /** |
| 12 | * Run the migrations. |
| 13 | */ |
| 14 | public function up(): void |
| 15 | { |
| 16 | Schema::create('course_modules', function (Blueprint $table): void { |
| 17 | $table->id(); |
| 18 | $table->unsignedSmallInteger('number')->unique(); |
| 19 | $table->string('slug')->unique(); |
| 20 | $table->string('title'); |
| 21 | $table->string('difficulty')->default('beginner'); |
| 22 | $table->unsignedSmallInteger('estimated_minutes')->default(45); |
| 23 | $table->text('summary'); |
| 24 | $table->timestamps(); |
| 25 | }); |
| 26 | |
| 27 | Schema::create('course_lessons', function (Blueprint $table): void { |
| 28 | $table->id(); |
| 29 | $table->foreignId('course_module_id')->constrained('course_modules')->cascadeOnDelete(); |
| 30 | $table->unsignedSmallInteger('number'); |
| 31 | $table->string('slug')->unique(); |
| 32 | $table->string('title'); |
| 33 | $table->text('summary'); |
| 34 | $table->longText('content_markdown'); |
| 35 | $table->longText('code_snippet')->nullable(); |
| 36 | $table->json('anatomy_files'); |
| 37 | $table->text('anatomy_note')->nullable(); |
| 38 | $table->string('lifecycle_phase')->default('controller'); |
| 39 | $table->timestamps(); |
| 40 | }); |
| 41 | |
| 42 | Schema::create('quiz_questions', function (Blueprint $table): void { |
| 43 | $table->id(); |
| 44 | $table->foreignId('course_lesson_id')->constrained('course_lessons')->cascadeOnDelete(); |
| 45 | $table->unsignedSmallInteger('position')->default(1); |
| 46 | $table->text('question'); |
| 47 | $table->json('options'); |
| 48 | $table->unsignedSmallInteger('correct_option'); |
| 49 | $table->text('explanation'); |
| 50 | $table->timestamps(); |
| 51 | }); |
| 52 | |
| 53 | Schema::create('student_profiles', function (Blueprint $table): void { |
| 54 | $table->id(); |
| 55 | $table->uuid('token')->unique(); |
| 56 | $table->string('transfer_code', 24)->unique(); |
| 57 | $table->json('completed_lessons'); |
| 58 | $table->json('quiz_scores'); |
| 59 | $table->timestamp('last_active_at')->nullable(); |
| 60 | $table->timestamps(); |
| 61 | }); |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Reverse the migrations. |
| 66 | */ |
| 67 | public function down(): void |
| 68 | { |
| 69 | Schema::dropIfExists('quiz_questions'); |
| 70 | Schema::dropIfExists('course_lessons'); |
| 71 | Schema::dropIfExists('course_modules'); |
| 72 | Schema::dropIfExists('student_profiles'); |
| 73 | } |
| 74 | }; |
| 75 | |