Práce s databází
Práce s databází
Section titled “Práce s databází”Projekt používá MariaDB 11 v Docker prostředí a Laravel migrations pro správu databázového schématu.
Databázová konfigurace
Section titled “Databázová konfigurace”Vývojové prostředí
Section titled “Vývojové prostředí”V .env:
DB_CONNECTION=mysqlDB_HOST=dbDB_PORT=3306DB_DATABASE=kvizDB_USERNAME=kvizDB_PASSWORD=kvizTestovací prostředí
Section titled “Testovací prostředí”V phpunit.xml:
<env name="DB_CONNECTION" value="mysql"/><env name="DB_HOST" value="db"/><env name="DB_DATABASE" value="kviz_testing"/>Migrace
Section titled “Migrace”Spuštění migrací
Section titled “Spuštění migrací”# Spustit všechny pending migracedocker compose exec fpm php artisan migrate
# S seedovánímdocker compose exec fpm php artisan migrate --seed
# Rollback poslední dávkydocker compose exec fpm php artisan migrate:rollback
# Rollback všech migracídocker compose exec fpm php artisan migrate:reset
# Reset a opětovné spuštění všech migracídocker compose exec fpm php artisan migrate:fresh
# Fresh s seedovánímdocker compose exec fpm php artisan migrate:fresh --seedVytvoření nové migrace
Section titled “Vytvoření nové migrace”# Vytvořit novou tabulkudocker compose exec fpm php artisan make:migration create_products_table
# Přidat sloupec do existující tabulkydocker compose exec fpm php artisan make:migration add_status_to_products_table
# S automatickým generováním strukturydocker compose exec fpm php artisan make:migration create_products_table --create=productsdocker compose exec fpm php artisan make:migration add_status_to_products --table=productsStruktura migrace
Section titled “Struktura migrace”<?php
use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;use Illuminate\Support\Facades\Schema;
return new class extends Migration{ public function up(): void { Schema::create('chk_products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->text('description')->nullable(); $table->decimal('price', 10, 2); $table->foreignId('category_id') ->constrained('chk_categories') ->onDelete('cascade'); $table->timestamps(); }); }
public function down(): void { Schema::dropIfExists('chk_products'); }};Best Practices pro migrace
Section titled “Best Practices pro migrace”1. Vždy používejte prefix chk_
Section titled “1. Vždy používejte prefix chk_”// ✅ SprávněSchema::create('chk_products', function (Blueprint $table) { ... });
// ❌ ŠpatněSchema::create('products', function (Blueprint $table) { ... });2. Definujte foreign key constraints
Section titled “2. Definujte foreign key constraints”$table->foreignId('region_id') ->constrained('chk_regions') ->onDelete('set null') ->onUpdate('cascade');3. Používejte nullable() pro volitelná pole
Section titled “3. Používejte nullable() pro volitelná pole”$table->string('phone')->nullable();$table->text('description')->nullable();4. Indexujte časté vyhledávací sloupce
Section titled “4. Indexujte časté vyhledávací sloupce”$table->string('email')->unique();$table->index('status');$table->index(['user_id', 'created_at']);Seeders
Section titled “Seeders”Seedery slouží k naplnění databáze testovacími daty.
Struktura seederů
Section titled “Struktura seederů”Sada je rozdělená na dvě vrstvy — referenční data a demo provoz:
database/seeders/├── DatabaseSeeder.php # Jen produkční guard + volání dvou vrstev├── ReferenceSeeder.php # Referenční data: Country, Region, City, formule,│ # uživatelé/role, LeagueType, QuizzConfig, obsah stránek├── DemoSeeder.php # Demo provoz (seedlý faker): Topic, Pub, blueprinty,│ # speciály, výsledky, rezervace, platby, ligy, novinky├── DemoAccountSeeder.php # MANUAL TOOL — prezentační účet (prod ID), mimo sadu└── AppVideoSeeder.php # MANUAL TOOL — video fixtures, mimo saduDemo vrstva odpovídá rozsahem produkci (~90 %: všechny regiony s produkčními
slugy, ~24 podniků, 1–2 blueprinty na podnik, 4+4 týdny eventů), ale drží
mělkou historii. Rozsah řídí config('kviz.seed_scale') — full na stagingu,
ci v testech (KVIZ_SEED_SCALE=ci v phpunit.xml).
Ligová soustava
Section titled “Ligová soustava”NewLeagueSeeder kopíruje tvar produkce, ne jen počty. Produkce nemá jednu
extraligu, ale sezóny: každá má celkovou ligu a pod ní denní varianty
(pondělí až čtvrtek, víkend, čtrnáctidenní). Vedle nich běží regionální série,
srovnávačky a stovky místních lig po jednom podniku.
Klíčová vlastnost je překryv. V produkci sedí typický blueprint v šesti až sedmi ligách naráz (rozsah 1–13), protože ho zároveň sbírá denní varianta, celková liga sezóny, regionální série i místní liga podniku. Bez toho nemají žebříčky ani filtry na čem ukázat, že se soustavy prolínají.
| Scale | Ligy | Z toho | Lig na blueprint | Čas |
|---|---|---|---|---|
full |
35 | 2 sezóny × 7 lig, 3 regionální, 1 srovnávačka, 18 místních | 3–8 | ~25 s |
ci |
9 | 1 sezóna × 5 lig, 1 regionální, 3 místní | 2–3 | ~3 s |
Přepočet žebříčků (LeagueResultsService) je nejdražší část celého seedu, proto
je bohatší soustava jen ve full scale. Na CI by za ni sada platila desítkami
sekund a nic by tam netestovala.
Všichni produkční admini mají v seednutém prostředí účet se svým e-mailem a
heslem password (UserRoleCompanySeeder::PROD_ADMINS).
Podniky
Section titled “Podniky”Každý podnik z PubSeeder::PUBS má zeměpisnou šířku a délku a většina má odkaz
na Facebook. Bez souřadnic se v detailu podniku nevykreslí mapa a předpověď
počasí nemá pro co počítat, takže by tyhle části webu na seednutém prostředí
nešly vyzkoušet.
Souřadnice jsou přibližné, na úrovni centra města, ne přesná poloha adresy. Facebook nemá každý šestý podnik, aby šla otestovat i varianta detailu bez odkazu.
Stoly a online rezervace
Section titled “Stoly a online rezervace”PubTableSeeder běží po ReservationSeeder a dělí podniky do tří stálých
skupin, aby šly vyzkoušet všechny podoby rezervační stránky:
- se stoly (zhruba polovina): 3, 6, 8 nebo 9 stolů s různou kapacitou, dva z nich pojmenované. Podniky se třemi stoly jsou malé schválně, aby se naseedovanými rezervacemi zaplnily. Rezervační stránka ukazuje rozmístění stolů a počet volných.
- jen online rezervace (zhruba třetina): bez stolů, stránka ukazuje prostý seznam přihlášených týmů.
- bez online rezervací: stránka rezervace se neotevře, stejně jako v produkci u podniků, které rezervace neberou.
Rezervace, které vytvořil ReservationSeeder, dostanou každá svůj stůl. Stálý
tým sedí na všech termínech svého kvízu u stejného stolu. Když je rezervací víc
než stolů, zbylé zůstanou bez stolu a termín je plně obsazený, takže jde
vyzkoušet i čekací listina.
Do které skupiny podnik patří, se odvozuje z jeho názvu
(PubTableSeeder::hasTables(), hasOnlineReservations()), takže je rozdělení
při každém seedu stejné.
Podniky, které vznikly dřív, než katalog tahle pole obsahoval, si je doplní při dalším běhu seederu. Hodnoty vyplněné ručně zůstávají.
E-shop
Section titled “E-shop”EshopStorefrontSeeder (referenční vrstva) zakládá oba obchody a jejich metody
dopravy a platby. Na ID obchodu visí konstanty App\Enums\Shop, které kód
používá napevno, a bez dopravní a platební metody nejde objednávku dokončit.
EshopSeeder (demo vrstva) plní sortiment a objednávky:
- osm produktů s popisem, parametry, sklademi a obrázkem, z toho dva s velikostmi a jeden s běžící slevou, aby šla vyzkoušet i přeškrtnutá cena,
- štítky produktů jsou kategorie (Oblečení, Doplňky, Hry, Dárky), protože výpis
obchodu z nich staví filtrovací pilulky a tiskne je tak, jak jsou uložené.
Filtr kombinuje vybrané kategorie přes AND, proto část produktů patří do dvou,
Objednávky má na starost
EshopOrderSeeder. Jedenáct objednávek pokrývá kombinace, na kterých se chování liší:
| Co | Kde to jde vyzkoušet |
|---|---|
| Všechny stavy | nová, čeká na platbu, zpracovává se, odeslaná, dokončená, stornovaná, vrácená |
| Nákup na účet | player1@, support@ a [email protected], heslo password — projde i „Moje objednávky” |
| Nákup bez účtu | tři objednávky hosta s vlastním kontaktem |
| Košík z víc položek | dvě a tři různé produkty v jedné objednávce |
| Velikost | tričko L, XXL, mikina S a M |
| Sleva | objednávky s deskovou hrou, sleva se propíše do položky |
| Doprava | osobní odběr i kurýr |
| Poznámka zákazníka | pět objednávek ji má |
| Historie | od dnešního dne po dva měsíce zpět |
Objednávky visí na testovacích účtech z UserRoleCompanySeeder; chybějící účet
seeder nespolkne, ale spadne s hláškou, který seeder spustit. Slovenský obchod
objednávky nemá, nemá vlastní sortiment a korunová cena v eurech by byla nesmysl.
Ceny jsou v databázi jako základ bez DPH, ale odvozené od koncové částky, takže zákazník vidí kulaté číslo (404,96 + 21 % = 490 Kč). Součet objednávky se počítá od koncové ceny a daň se dopočítá jako zbytek, jinak by sečítání zaokrouhlených základů vyrobilo objednávku za 648,99 Kč místo 649 Kč.
Objednávky se zapisují přes Order::withoutEvents(). OrderObserver totiž na
každou změnu stavu posílá zákazníkovi e-mail a seed by jinak rozeslal celou
sadu zpráv o objednávkách.
Obrázky v demo datech
Section titled “Obrázky v demo datech”Ligy, podniky i novinky mají v produkci obrázek skoro vždy. Seedery proto cesty vyplňují, ale soubory na disk nekopírují — to dělá samostatný příkaz:
docker compose exec fpm php artisan demo:sync-assetsPříkaz nakopíruje obrázky, které už v repu jsou (public/images/web2/others/),
na úložný disk pod demo/ a blog/. Kvůli demo datům se do repa nepřidávají
žádné nové binárky. Bez toho kroku dataset funguje, jen se obrázky nenačtou.
Spuštění seederů
Section titled “Spuštění seederů”# Všechny seedery (DatabaseSeeder)docker compose exec fpm php artisan db:seed
# Konkrétní seederdocker compose exec fpm php artisan db:seed --class=CountrySeeder
# Fresh migrate + seeddocker compose exec fpm php artisan migrate:fresh --seedVytvoření nového seederu
Section titled “Vytvoření nového seederu”docker compose exec fpm php artisan make:seeder ProductSeederStruktura seederu (idempotentní)
Section titled “Struktura seederu (idempotentní)”<?php
namespace Database\Seeders;
use App\Models\Product;use Illuminate\Database\Seeder;
class ProductSeeder extends Seeder{ public function run(): void { $products = [ ['name' => 'Product 1', 'price' => 100], ['name' => 'Product 2', 'price' => 200], ];
foreach ($products as $product) { Product::updateOrCreate( ['name' => $product['name']], // Unique identifikátor $product // Data k aktualizaci ); }
$this->command?->info("✅ ProductSeeder dokončen. Vytvořeno/aktualizováno " . count($products) . " produktů."); }}Pořadí závislostí v DatabaseSeeder
Section titled “Pořadí závislostí v DatabaseSeeder”public function run(){ if (app()->environment('production')) { $this->command?->warn('⚠️ Seeding je zakázáno v produkčním prostředí.'); return; }
// Základní data v pořadí závislostí $this->call([ CountrySeeder::class, // 1. Země (bez závislostí) RegionSeeder::class, // 2. Regiony (vyžadují Country) CitySeeder::class, // 3. Města (vyžadují Region) ]);
// Testovací data $this->call([ UserRoleCompanySeeder::class, PubSeeder::class, // 4. Puby (vyžadují City) ]);}Datové operace (produkční data)
Section titled “Datové operace (produkční data)”Seedery se v produkci nespouští, DatabaseSeeder je tam odmítne. Data, která
musí vzniknout i na produkci (referenční číselníky, přenos nastavení z configu
do databáze, jednorázové opravy dat), proto jezdí jako jednorázové operace
balíku timokoerber/laravel-one-time-operations.
Chovají se jako migrace: mají pořadí, evidenci v tabulce chk_operations
a spustí se právě jednou. Oproti seederu tak chyba neproběhne při každém
nasazení, ale jednou a je po ní stopa.
# Nová operacedocker-compose exec fpm php artisan operations:make nazev_operace
# Spuštění čekajících operací (deploy to dělá sám)docker-compose exec fpm php artisan operations:process --sync
# Přehled, co už proběhlodocker-compose exec fpm php artisan operations:showSoubory bydlí v database/operations/, deploy je pouští za migrate --force
na produkci, dev, novém webu i experimentálu.
Kam co patří
Section titled “Kam co patří”| Druh dat | Kam |
|---|---|
| Struktura tabulek | migrace v database/migrations/ |
| Referenční data pro produkci | operace v database/operations/ |
| Testovací a demo data | seeder v database/seeders/ |
| Data pro jeden test | factory |
Datovou migraci už nepiš. Dřív byl jediný způsob, jak dostat řádky na produkci,
insertOrIgnore uvnitř migrace — proto v database/migrations/ pár takových
zůstalo (reinsert_shops_data, seed_form_integrations_from_config). Nové
věci patří do operací.
Jeden zdroj pravdy
Section titled “Jeden zdroj pravdy”Když stejná data potřebuje staging (seeder) i produkce (operace), nesmí být
zapsaná dvakrát. Definice žije v katalogové třídě v app/Support/ReferenceData/
a obě cesty ji jen zavolají:
FormIntegrationData::apply(bool $overwrite = false);Parametr $overwrite odděluje obě prostředí. Seeder na stagingu přepisuje, ať
je po každém seedu stejný výchozí stav. Operace v produkci jen doplňuje, co
chybí — co si klient nastavil v adminu, zůstává.
Factories
Section titled “Factories”Factories generují testovací data pro modely.
Vytvoření factory
Section titled “Vytvoření factory”docker compose exec fpm php artisan make:factory ProductFactory --model=ProductStruktura factory
Section titled “Struktura factory”<?php
namespace Database\Factories;
use App\Models\Product;use Illuminate\Database\Eloquent\Factories\Factory;
class ProductFactory extends Factory{ protected $model = Product::class;
public function definition(): array { return [ 'name' => $this->faker->words(3, true), 'description' => $this->faker->sentence(), 'price' => $this->faker->randomFloat(2, 10, 1000), 'category_id' => 1, // Nebo Category::factory() ]; }
/** * State pro drahé produkty */ public function expensive(): static { return $this->state(fn (array $attributes) => [ 'price' => $this->faker->randomFloat(2, 1000, 10000), ]); }
/** * State pro vyprodané produkty */ public function soldOut(): static { return $this->state(fn (array $attributes) => [ 'stock' => 0, ]); }}Použití factory v modelu
Section titled “Použití factory v modelu”<?php
namespace App\Models;
use Database\Factories\ProductFactory;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;
class Product extends Model{ use HasFactory;
protected static function newFactory() { return ProductFactory::new(); }
// ... rest of model}Použití factory
Section titled “Použití factory”// Vytvořit jeden produkt (bez uložení)$product = Product::factory()->make();
// Vytvořit a uložit jeden produkt$product = Product::factory()->create();
// Vytvořit 10 produktů$products = Product::factory()->count(10)->create();
// S vlastními atributy$product = Product::factory()->create([ 'name' => 'Custom Product', 'price' => 99.99,]);
// Použití states$expensiveProduct = Product::factory()->expensive()->create();$soldOutProduct = Product::factory()->soldOut()->create();
// S relacemi$product = Product::factory() ->for(Category::factory()) ->create();Database Schema
Section titled “Database Schema”Export aktuálního schématu
Section titled “Export aktuálního schématu”# Export do SQL souborudocker compose exec db mysqldump -u kviz -pkviz kviz > database/schema/mysql-schema.sql
# Export pouze struktury (bez dat)docker compose exec db mysqldump -u kviz -pkviz --no-data kviz > database/schema/mysql-schema.sqlImport schématu
Section titled “Import schématu”# Import do existující databázedocker compose exec -T db mysql -u kviz -pkviz kviz < database/schema/mysql-schema.sql
# Import do testovací databázedocker compose exec -T db mysql -u kviz -pkviz kviz_testing < database/schema/mysql-schema.sqlSquash Migrations
Section titled “Squash Migrations”Pokud máte příliš mnoho migrací, můžete je “squashnout” do jednoho SQL souboru.
1. Export aktuálního schématu
Section titled “1. Export aktuálního schématu”docker compose exec db mysqldump -u kviz -pkviz --no-data kviz > database/schema/mysql-schema-YYYY-MM-DD.sql2. Archivace starých migrací
Section titled “2. Archivace starých migrací”mkdir database/migrations/archivedmv database/migrations/2024_*.php database/migrations/archived/3. Vytvoření nové base migrace
Section titled “3. Vytvoření nové base migrace”<?php
use Illuminate\Database\Migrations\Migration;use Illuminate\Support\Facades\DB;
return new class extends Migration{ public function up(): void { $sql = file_get_contents(database_path('schema/mysql-schema-2024-12-01.sql')); DB::unprepared($sql); }
public function down(): void { // Rollback není možný pro squash migraci throw new Exception('Cannot rollback squashed migrations'); }};Časté operace
Section titled “Časté operace”Resetování databáze
Section titled “Resetování databáze”# Kompletní resetdocker compose exec fpm php artisan migrate:fresh --seed
# Pouze migrace (bez seedování)docker compose exec fpm php artisan migrate:freshKontrola stavu migrací
Section titled “Kontrola stavu migrací”docker compose exec fpm php artisan migrate:statusPřímý přístup k databázi
Section titled “Přímý přístup k databázi”# MySQL CLIdocker compose exec db mysql -u kviz -pkviz kviz
# Nebo přes fpm kontejnerdocker compose exec fpm php artisan db
# Tinker (Laravel REPL)docker compose exec fpm php artisan tinkerBackup databáze
Section titled “Backup databáze”# Automatický backup script./backup-database.sh
# Manuální backupdocker compose exec db mysqldump -u kviz -pkviz kviz > backup-$(date +%Y%m%d-%H%M%S).sqlRestore databáze
Section titled “Restore databáze”docker compose exec -T db mysql -u kviz -pkviz kviz < backup-20240101-120000.sqlDatabázové konvence
Section titled “Databázové konvence”Naming conventions
Section titled “Naming conventions”-
Tabulky: Plural, snake_case s prefixem
chk_chk_users,chk_regions,chk_quiz_events
-
Sloupce: snake_case
user_id,created_at,display_name
-
Foreign keys:
{model}_idcountry_id,region_id,user_id
-
Pivot tabulky: Alphabetically ordered, singular, s prefixem
chk_pub_user,chk_quiz_team
-
Indexy:
{table}_{column}_indexusers_email_index,regions_country_id_index
Datové typy
Section titled “Datové typy”$table->id(); // BIGINT UNSIGNED AUTO_INCREMENT$table->string('name', 255); // VARCHAR(255)$table->text('description'); // TEXT$table->integer('count'); // INT$table->decimal('price', 10, 2); // DECIMAL(10,2)$table->boolean('active'); // TINYINT(1)$table->timestamp('published_at'); // TIMESTAMP$table->timestamps(); // created_at, updated_at$table->softDeletes(); // deleted_atTroubleshooting
Section titled “Troubleshooting”“Table doesn’t exist” chyby
Section titled ““Table doesn’t exist” chyby”# Spusťte migracedocker compose exec fpm php artisan migrate“Access denied” chyby
Section titled ““Access denied” chyby”# Zkontrolujte credentials v .env# Zkontrolujte, že databázový kontejner běžídocker compose ps dbMigrace selhávají
Section titled “Migrace selhávají”# Zkontrolujte syntax v migraci# Zkontrolujte existenci foreign key tabulek# Rollback a zkuste znovudocker compose exec fpm php artisan migrate:rollbackdocker compose exec fpm php artisan migrate