Címkék: #Adatbázis (Database) #Adatgyár (Factory) #CRUD #Eloquent #Laravel #Laravel 9 #Tinker
A városokhoz tartozó City Model létrehozását így végezzük el:
php artisan make:model City -mf
Egyből a migrációs fájlját és a gyárát is létrehozzuk. A migrációs fájlban adjunk hozzá az ott meglévőkhöz egy új oszlopot (name), és ugye szeretnénk összekapcsolni az Airline és City Model osztályokat, de hogyan is kellene ezt? Ha a cities táblában hoznánk létre egy airline_id idegen kulcsot, akkor minden város csak egy légitársasághoz tartozhatna, ami ugye nem túl valószerű. Ugyanez fordított esetben: ha az airlines táblában hoznánk létre egy city_id oszlopot, akkor az azt jelentené, hogy csak egy városban működne, illetve tevékenykedne a légitársaság, ami ugye megintcsak nem reális... akkor tehát adódik a kérdés, hogy hol és hogyan is valósítsuk meg ezt a kapcsolatot? Egy kapcsolótáblát fogunk ehhez használni. Adatbázis szempontjából ez az az új tábla, amely az összekapcsolni kívánt táblákra hivatkozó külső kulcsokat fog tartalmazni és így valósítható meg a több-többes kapcsolat.
Ezt az új kapcsolótáblát definiáljuk ugyanebben a City migrációs fájlban és annak up() metódusában, természetesen úgy, hogy betartjuk az ilyenkor szokásos névkonvenciókat:
Nézzük meg, hogy mire kell figyelni a létrehozás magjában:
Így néz ki tehát a City migrációs fájljának up() metódusa:
public function up()
{
Schema::create('cities', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
Schema::create('airline_city', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('airline_id');
$table->unsignedBigInteger('city_id');
$table->timestamps();
$table->foreign('airline_id')->references('id')->on('airlines')->onDelete('cascade')->onUpdate('cascade');
$table->foreign('city_id')->references('id')->on('cities')->onDelete('cascade')->onUpdate('cascade');
$table->unique(['airline_id', 'city_id']);
});
}
A migrációs fájl down() metódusában fordított sorrendben "dobjuk el" (töröljük) a táblákat, mint ahogy az up()-ban létrehoztuk őket:
Schema::dropIfExists('airline_city');
Schema::dropIfExists('cities');
Végrehajthatjuk a migrálást:
php artisan migrate
Vegyük észre, hogy csak egy PHP fájl került migrálásra, de abban ugye két táblát is definiáltunk, amelyek létre is jöttek az adatbázisunkban.
Bővítsük az Airline Model osztályunkat:
public function cities()
{
return $this->belongsToMany(City::class);
}
Bővítsük a City Model osztályunkat:
protected $fillable = ['name'];
public function airlines()
{
return $this->belongsToMany(Airline::class);
}
Itt nem csak a kapcsolatot biztosító metódust adtam hozzá, hanem a tömeges adatfeltöltést engedélyező $fillable mező értékét is beállítottam. Úgyhogy most folytathatjuk is a CityFactory osztályunkkal, állítsuk be a következőt a definition gyűjteményben:
return [
'name' => $this->faker->city()
];
Tinker megnyitása után hozzunk is létre néhány várost példaként, mondjuk 5 darabot:
City::factory()->count(5)->create();
Kapcsolatok hozzáadása, lekérdezése, törlése
Megjegyzés: az itt kiadott utasítások eredményét mindig ellenőrizhetjük mondjuk a MySQL Workbench-ben, ha mindig lekérjük az airline_city tábla aktuális sorait.
Nálam van tehát most 12 légitársaság (mivel a repülőjáratok gyárát úgy állítottam be az előző bejegyzésben, hogy minden új repülőjárat létrejöttekor jöjjön létre hozzá egy új légitársaság is) és van 5 városom, amiket az imént hoztam létre. Ezeket kellene összerendelni az airline_city kapcsolótábla segítségével. De hogyan tudunk az Eloquent segítségével új sorokat (kapcsolatokat) hozzáadni a kapcsolótáblánkhoz? Ehhez a Model osztályokban definiált kapcsolat metódusokat (airlines(), cities()) és az attach() metódust fogjuk alkalmazni.
City::find(5)->airlines()->attach(1)
City::find(4)->airlines()->attach([1,2,3])
City::find(5)->airlines()->attach(1)
$airlines = App\Models\Airline::findMany([7,8,9,10])
City::first()->airlines()->attach($airlines)
Az első sor végrehajtásával az 5-ös városhoz hozzárendelem az 1-es légitársaságot. A második sor megmutatja azt, hogy több elemet is tudok egyszerre összekapcsolni, itt például a 4-es városhoz hozzárendelem az 1, 2 és 3 azonosítójú légitársaságot. Végül a harmadik sorban újra megpróbálom az 5-ös városhoz hozzárendelni újra az 1-es légitársaságot, de szerencsére ez nem működik (integritási hiba), hiszen ilyen kapcsolatunk már volt és a rendszer jogosan mondja nekünk azt, hogy ilyet már újra nem hozhatunk (ne is hozzunk) létre. A negyedik sorban rászűrök a 7,8,9,10 légitársaságokra, majd az ötödik sorban hozzárendelem azokat az 1-es városhoz. A kapcsolatok hozzáadása tehát működik. Igény szerint próbálgathatjuk és "bármennyi" egyéb kapcsolatot hozzáadhatunk, jelen esetben ugye maximálisan annyi kapcsolatot tudunk létrehozni, amennyi a [városok száma * légitársaságok száma] = nálam 5 * 12 = 60 darab kapcsolat, jelen esetben és akkor minden városban lesz telephelyen minden légitársaságnak a feladat leírása és értelmezése szerint.
Nézzük meg a lekérdezést, amire ugye "kétirányból" van lehetőségünk.
City::first()->airlines
Airline::first()->cities
Ezek a korábban már megismert módon működnek. Itt például lekértük az 1-es városhoz tartozó légitársaságokat, és meg is kaptuk a 7,8,9,10-es légitársaságokat eredményül. A második sorban az 1-es légitársasághoz tartozó városokat kérdeztem le és eredményül megkaptam a 4,5-ös városokat.
Kapcsolat törléséhez a detach() metódust tudjuk segítségül hívni, ami nagyon hasonlóan működik, mint az imént bemutatott attach, csak fordítva (ez nem létrehozza a kapcsolatokat a kapcsolótáblában, hanem törli őket):
City::first()->airlines()->detach([8,10])
Airline::first()->cities()->detach(4)
Az első sorban az 1-es város légitársaságai közül töröltem a 8-ashoz és a 10-es való kapcsolódást. A második sorban pedig az 1-es légitársaságnál töröltem a kapcsolódást a 4-es városhoz.
Szinkronizálás: sync, syncWithoutDeatching (Fejes Dávid jelzése nyomán) és a toggle segédmetódusok
A kapcsolatokat szinkronizálni is tudjuk. De mit is jelent ez? (Megjegyzés: szükség esetén ürítsük ki az airline_city kapcsolótáblánkat, hogy azokat az eredményeket láthassuk, amiket én is itt jelzek.) Az itteni szekció címében látható segédmetódusokat fogjuk használni. Próbáljuk ki a következő utasításokat:
Airline::find(8)->cities()->sync([1,3,7])
Az utasítás az 8-as légitársasághoz hozzárendelni az 1., 3. és 7. városokat. Ebben eddig semmi meglepő nincs, ezt is vártuk tőle. Eredményben láthatjuk, hogy az attach gyűjteménybe bekerült az 1,3,7 számsor. Viszont ha most ezekután kiadjuk ezt az utasítást:
Airline::find(8)->cities()->sync([2,6,11])
Akkor itt láthatjuk, hogy a 2, 6, 11-es városok hozzáfűzésre kerültek a 8-as légitársasághoz (létrejöttek a kapcsolatok), viszont megszüntette a kapcsolatot az 1, 3, 7-es városokkal. Ezt nem biztos, hogy így akartuk, inkább lehet, hogy mi csak hozzáfűzni szerettünk volna, leválasztás nélkül, ekkor használható ez a parancs:
Airline::find(8)->cities()->syncWithoutDetaching([1,3,7])
Ennek hatására visszakerül a kapcsolatok közé a 8-as légitársasághoz az 1, 3, 7-es városok. Ez ugye azért is hasznos nekünk, mert így elkerülhetjük azt a "hibaüzenetet", amit akkor kaptunk, amikor már egy létező kapcsolatot szerettünk volna újra felvenni. Itt most ezzel nem lesz probléma, a rendszer egyszerűen csak tudomásul veszi, hogy ennek a kapcsolatnak léteznie kell és nem adódik ebből hiba.
Eljátszadozhatunk ezekkel a mi komplex alkalmazásunkban, de mindig figyeljük, hogy milyen eredményt jelez vissza számunkra a tinker, vagy milyen adatok is kerültek be a kapcsolótáblába. Ha már ezeket (sync, syncWithoutDetaching) megnéztük, akkor esetleg a toggle metódust is kipróbálhatjuk, amely megnézi, hogy az újonnan hozzáadandó kapcsolat az már létezett-e, ha létezett, akkor törli, ha nem létezett még korábban, akkor pedig most létrehozza:
Airline::find(8)->cities()->toggle([2,6,9])
Itt láthatjuk, hogy a 2, 6 leválasztásra kerültek a kapcsolatok közül, míg a 9-es új városként hozzáadódott a 8-as repülőjárat telephelyeihez.
Kapcsolatok megjelenítése a weboldalon
Jelen állapotban még nem látszódnak a légitársaságok és a városok a weboldalunkon. Hozzuk létre a hozzájuk kapcsolódó (1) útvonalakat, (2) Controller-eket és metódusokat, (3) nézeteket. Kezdjük az új útvonalakkal a routes/web.php-ban...
Route::get('/airlines/{airline}',[App\Http\Controllers\AirlinesController::class, 'show']);
Route::get('/airlines',[App\Http\Controllers\AirlinesController::class, 'index']);
Route::get('/cities/{city}',[App\Http\Controllers\CitiesController::class, 'show']);
Route::get('/cities',[App\Http\Controllers\CitiesController::class, 'index']);
Itt most már mindkét esetben egyértelműen törekszem arra, hogy a megfelelő Controller kezelje le az útvonal kérések kiszolgálását, aztán pedig majd a nézeteket is egy csoportba fogom szervezni, de előtte hozzuk létre a két itt látható Controller osztályt:
php artisan make:controller AirlinesController
php artisan make:controller CitiesController
Mindkettőhöz jó alap lehet a FlightsController osztályunk. Kezdjük az AirlinesController-rel:
public function show($id)
{
$airline = Airline::findOrFail($id);
return view('airlines.show', [
'airline' => $airline,
'cities' => $airline->cities->pluck('name')
]);
}
public function index()
{
return view('airlines.index', [
'airlines' => Airline::orderBy('name')->get()
]);
}
Folytassuk a CitiesController-rel:
public function show($id)
{
$city = City::findOrFail($id);
return view('cities.show', [
'city' => $city,
'airlines' => $city->airlines->pluck('name')
]);
}
public function index()
{
return view('cities.index', [
'cities' => City::orderBy('name')->get()
]);
}
Ezután következhet a nézet mappák létrehozása a resources/views mappában: airlines és cities névvel. Mindkét új mappába pedig létrehozom az index.blade.php és a show.blade.php nézeteket. Kezdjük az airlines/index.blade.php -val, amelyhez jó alapot biztosít a flights/index.blade.php
Ezt lemásolva, folytathatjuk a cities/index.blade.php -val:
De hogy ezeket ki tudjuk próbálni, a layout.blade.php fájlunkban a menünket bővíteni kell két új menüponttal:
Az eredmény szerencsére tökéletesen megfelelő:
A másik esetében is:
A részletező (show) oldalakra pedig tegyük be az adott légitársaság és város kapcsolatait is. A resources/views/flights mappában lévő show.blade.php megint jó példa lesz a kiindulásra, ehhez hasonlóan készítsük el az airlines/show.blade.php fájl tartalmát:
És a cities/show.blade.php tartalmát:
Az eredmény mindkét esetben megfelelően fog látszódni az oldalakon.
És a "társnézete":
A blogbejegyzésben elvégzett módosítások ezen a Github linken érhetők el.
A mostani blogbejegyzésben megtanultuk, hogy milyen az a több-többes adatbáziskapcsolat és megnéztük, hogy hogyan lehet kialakítani ezt a szerkezetet a Laravel-ben Model osztályok, migrációs fájlok, Controller-ek és nézetek segítségével.