Adatbázis-kezelés 3. rész - Eloquent alkalmazása CRUD műveletekre

Attila | 2022. 03. 04. 22:10 | Olvasási idő: 5 perc

Címkék: #Adatbázis (Database) #Eloquent #Laravel #Laravel 5.7 #Laravel 8 #MySQL #PHP #Tinker

Ebben a blogbejegyzésben megnézzük, hogy hogyan lehet használni az Eloquent-et CRUD (Create, Read, Update, Delete, vagyis létrehozás, kiolvasás, frissítés, törlés) műveletek elvégzésére. Mindezek mellett megismerkedünk a Tinker használatával, aminek a segítségével tudjuk parancssorból programozottan megszólítani az alkalmazásunkat.
Laravel_Eloquent_CRUD1

Már itt az elején kiemelem, hogy még mindig a tábla/mező (és osztály/objektum) kapcsolatok nélküli világban tevékenykedünk. Tehát csak 1-1 táblát / mezőt, osztályt / objektumot fogunk önmagában menedzselni CRUD műveletekkel. A kapcsolatok kialakítása után majd tovább mélyítjük ezt a tudásunkat, hogy abban a szituációban, hogyan lehet majd CRUD műveleteket alkalmazni.


A kezdés: bevezető tevékenységek

Mindenekelőtt szükségünk van egy Model-re, a migrációs fájljára... és most egyelőre elég is lesz ennyi. Ezeket létre tudjuk hozni egyetlen utasítással. Nézzük meg, hogy miket tudunk beállítani a Model-nek és ezen az utasításon keresztül még mit tudunk létrehozni:

php artisan help make:model


Az utasításon keresztül tudunk a Model-hez hozzáadni még Controller-t, migrációs fájlt, factory-t és seed-et is (ez utóbbi kettő a tesztadatok gyártásához hasznos, a későbbiekben majd megismerkedünk velük részletesebben). Dolgozzunk egy repülőjárattal és hozzunk neki létre a Model mellé még egy migrációs fájlt is:

php artisan make:model Flight -m

Ezzel létrejöttek az imént említett fájljaink. A Flight.php Model fájlunkat nem is kell még szerkeszteni, térjünk rá inkább rögtön a migrációs fájljára.

Tipp: ha nem tudnánk, hogy hirtelen valamelyik fájl hol van, akkor VSCode-ban nyomjuk meg a CTRL + p betűt, ami felhoz középen felül egy bemeneti mezőt és ha elkezdjük írni a fájl nevét, akkor rögtön mutatja nekünk a keresésünk alapján szűrt listát és ki tudjuk választani a megnyitandó fájlt. Jelen esetben, ha elkezdjük írni a "create_flights" nevet, akkor hamar megtalálja nekünk az alkalmazás, hogy hol is van ez a fájl és meg is tudjuk már nyitni szerkesztésre.

A migrációs fájlt, ami létrehozza majd a háttérben a flights adattáblát gyakorlásképpen töltsük fel az up() metódusban lévő séma létrehozást jónéhány mezővel, így is ismerkedhetünk a gyakorlati oldalával a migrációknak:

Schema::create('flights', function (Blueprint $table) {
  $table->id();
  $table->string('number');
  $table->string('name')->nullable();
  $table->string('destination')->nullable();
  $table->integer('price')->nullable();
  $table->boolean('active')->default(false);
  $table->boolean('delayed')->default(false);
  $table->boolean('departed')->default(false);
  $table->timestamp('arrived_at')->nullable();
  $table->timestamps();
});

Találkozhatunk itt többféle mezőtípussal (Laravel v9-ben az id(), Laravel v5.7-ben bigIncrements('id') metódus az adatbázisban biginteger-t - nagyméretű egész számot - jelent majd, a string varchar-t - szöveget, az integer int-et - egész számot, a boolean tinyint-et - logikai értéket, timestamp - ugyanezt - időbélyeget, vagy többet, ha timestamps). De láthatunk itt nullázható mezőt, ez azt jelenti, hogy az adatbázisban annak a mezőnek megengedjük, hogy NULL értéket kapjon, ha ezt nem tennénk hozzá, akkor ez az engedély nem lenne meg. A default értéket is be tudjuk itt állítani, vagyis magyarul a mező alapértelmezett értékét. Számos egyéb dolgot is be tudnánk még állítani, akit érdekel a bővebb lista, az nézze meg az előző bejegyzésemet, ami a migrációs fájlokról szólt, mert abban linkeltem a mezőtípusokról és a módosítókról a Laravel dokumentációjában fellelhető listát.

A későbbiekben érdemes megfontolni azt is, hogy miért lett volna hasznos nekünk, ha például a departed mező típusát nem boolean-re, hanem mondjuk timestamp-re állítjuk? Mindenkire rábízom ennek átgondolását, de ha nem tudjuk, akkor a blogbejegyzés végén majd elárulom.

A migrációs fájlt migrálhatjuk is az adatbázisba, hogy egyből érvényre tudjon jutni és tudjunk majd adatokkal dolgozni a továbbiakban:

php artisan migrate


Adattábla manipuláció (INSERT, UPDATE, DELETE) és lekérdezés (SELECT) a gyakorlatban Tinker használatával

Ez az eszköz a továbbiakban többször a segítségünkre lesz majd, itt leírom röviden, hogy mi is ez. A Tinker egy úgynevezett REPL (read-eval-print loop) eszköz. Ez lehetőséget biztosít a programozók számára, hogy kapcsolatot teremtsenek a Laravel alkalmazásukkal működés közben, méghozzá programozottan, PHP utasítások kiadásával. Most nekünk ez tökéletesen kapóra jön, hiszen az Eloquen-t működését ezzel tudjuk tesztelni. Létrehozunk, frissítünk, törlünk egy és több repülőjáratot is, valós időben, programozottan. Ehhez mindössze annyit kell tennünk, hogy a VSCode-ban nyitunk egy terminal-t és kiadjuk a következő parancsot:

php artisan tinker

Ezzel tehát megnyílik egy új világ előttünk és együttműködhetünk a programunkkal, bemenettel szolgálhatunk neki, végrehajthatunk mindenféle utasítást és visszakapjuk az utasítások eredményeit. Például írjuk be, hogy 2+2 és meg is kapjuk rá a választ, ami persze nem a mi Laravel alkalmazásunk "nagyszerűségét" mutatja, hanem ezt a képességet, amire a Tinker képes. Na, de ennyi kis bevezető után térjünk is rá arra, hogy valóban a mi programunkkal működünk együtt. Hozzunk létre egy Flight Model példányt, paraméterezzük fel és mentsük el (megjegyzés: ha Laravel 8 előtti verziót használunk, akkor mindig "App\Flight"-ot használjunk, nem kell a Models mappa megnevezése, hiszen akkor még nem az volt a Laravel keretrendszer logikája, hogy az app mappán belül a models mappába rakja a Model fájlokat, hanem csak simán az app-ba):

$myFirstFlight = new App\Models\Flight;
$myFirstFlight->number = 'ASDF-01';
$myFirstFlight->save();

Minden utasítás sor után üssünk Enter-t és a Tinker rögtön működésbe is lép. Az első sor után megmutatja, hogy létrehozott egy példányt a Flight Model osztályból (melléír egy véletlenszerű azonosítószámot). A number megadásánál konstatálja, hogy ezt adtuk meg number-nek. És mivel úgy alakítottuk ki a Flight migrációs fájlját, hogy a number adattag volt az egyetlen kötelezően megadandó érték, emiatt utána végre is hajthatjuk a mentést (save() metódus meghívása). Visszakapunk egy választ is a Tinker-től: true. Ez annyit tesz, hogy sikerült elmenteni az adatbázisba a $myFirstFlight objektumot, ami az adatbázisban a flights táblában egy sor beszúrásának (SQL INSERT utasításnak) felel meg.

Ha most tovább írjuk a kódot a Tinker-ben, például így:

$myFirstFlight->number = 'QWER-02';
$myFirstFlight->price = 500;
$myFirstFlight->save();

Akkor a $myFirstFlight objektum number adattagjának értékét felülírom, megadom neki továbbá a price paraméterét, majd elmentem a save() metódushívással. Viszont ha újra lekérjük a flights adattábla tartalmát a MySQL Workbenc-ben, akkor láthatjuk, hogy most a save() metódusos utasítás hatására nem egy beszúrás történt meg, hanem ugyanannak a sornak a frissítése. Ez teljesen logikus is, hiszen ugyanannak az objektumnak, kvázi adatsornak a paramétereit módosítottam, majd mentettem el, tehát frissítettem, ami egy SQL UPDATE utasításnak felelt meg.

Hozzunk létre még egy repülőjáratot és paraméterezzük fel, hogy majd tudjunk dolgozni repülőjáratok halmazával:

$mySecondFlight = new App\Models\Flight;
$mySecondFlight->number = 'YXCV-03';
$mySecondFlight->name = 'My favourite flight';
$mySecondFlight->price = 300;
$mySecondFlight->save();

Nagyon hasonló dolgok történtek, mint korábban, csak most egy picit több paramétert adtunk meg már az elején és után mentettük el a repülőjáratot, tehát egy beszúrást hajtottunk végre a flights adattáblába. Így már van értelme annak, hogy "adatbázis lekérdezéseket" hajtsunk végre ugyanúgy a Tinker-ben:

App\Models\Flight::all();

Ez az utasítás visszaad nekünk egy Eloquent Collection-t, egy gyűjteményt, ami tartalmazza a Flight Model osztály összes példányát, objektumát egybefogva, tehát kvázi a flights adattábla összes sorát és mezőjét adja vissza nekünk, mintha beírtunk volna egy SELECT * FROM flights; utasítást a MySQL Workbench-be. Viszont, ha már lekérdezni tudunk, akkor tudunk szűrni is...

App\Models\Flight::first();

Ez visszaadja az imént említett gyűjtemény legelső elemét. De most szűrjünk másképp:

App\Models\Flight::where('price', '<', 400)->get();

Ha így fogalmazzuk meg a where metódus paramétereit, akkor azt a repülőjárat objektumot (adatsort) fogja nekünk visszaadni, amelyiknek az ára kisebb volt mint 400 egység. Tehát a "kedvenc" (favourite) repülőjáratunkat kapjuk vissza, a második adatsort.

De az adatlekéréssel olyan lehetőségünk is van, hogy aggregációs metódusokat hívjuk (SQL-ben ezek például a COUNT, SUM, AVG, MIN, MAX stb.), tehát amik valamilyen összesítést végeznek el az adatsorainkon:

App\Models\Flight::where('active', 0)->max('price');
App\Models\Flight::where('active', 0)->count();

Sajnos, most még mindkét repülőjáratunk "aktivitás szempontjából hamis értéket mutatnak", tehát "inaktívak", de mi meg tudjuk tenni, hogy leszűrünk ezekre az inaktívakra és az iménti első sor segítségével lekérjük a legdrágább repülőjárat árát, visszakapjuk, hogy 500 egységbe kerül. A második sorban ismét az inaktívokra szűrünk (vegyük észre, hogy az egyenlőségjelet nem muszáj beleírni a where metódus paraméterei közé, mert az az alapértelmezett, és így csak két paramétert kell megadni neki), majd utána megszámoljuk (count), hogy hány darab inaktív repülőjáratunk van, visszakapjuk, hogy 2.

Mivel nem akarunk ilyen sok inaktív repülőjáratot, ezért frissítsük fel az összes járatunkat aktívra:

App\Models\Flight::where('active', false)->update(['active' => true]);

Az update() metódus egy tömböt vár el paraméterként, amiben megadjuk, hogy melyik értéket mire szeretnénk állítani. Az iménti PHP kód SQL nyelven így néz ki MySQL-ben:

UPDATE flights
SET active = true
WHERE active = false;

(A sortöréseknek - mindössze 1 utasításról van szó - és a kis/nagybetűknek nincs jelentősége, csak a jobb áttekinthetőség miatt írtam így.)

Itt emlékeztetnék mindenkit arra, hogy igazából még a Flight Model fájlunkba semmit nem írtunk. Mindezeket úgy tudtuk alkalmazni, hogy a Flight Model fájl "csak egy kaput nyitott nekünk" ahhoz, hogy módosítsuk a flights adatbázistáblát. Ebben is van az Eloquent ORM ereje.

Folytassuk a munkát és hozzunk létre még egy repülőjáratot egy kicsit másképp, mint ahogy eddig csináltuk:

App\Models\Flight::create(['number' => 'UIOP-04']);

Itt már nem egy objektumot hozok létre, amit utána felparaméterezek és elmentek a save() metódussal. Itt a create utasítást szeretném használni az iménti felsorolások utasításait egybevéve. Viszont hibát fogok kapni. Azt mondja nekem a Tinker (vagyis igazából a saját Laravel alkalmazásom, amivel interaktív kapcsolatot létesítettem), hogy adjam hozzá a number mezőt azoknak a paramétereknek a listájához, amelyet a Laravel ilyenfajta mentés során ellenőriz, hogy betölthető-e ilyen mező az adatbázistáblába. Mi sem egyszerűbb ennél, orvosoljuk ezt a problémát, nyissuk meg a Flight.php Model fájlunkat és adjunk hozzá az osztályunkhoz egy új adattagot:

protected $fillable = ['number'];

Ez a $fillable tömb tartalmazza azokat az elemeket, amiket mi, programozók megengedünk, hogy az adatbázisba beszúrjon valaki, bárki. Például mi itt most megengedtük, hogy a járatszámot (number) beszúrja a program használója a create() metódus használatával (ami a háttérben rögtön elvégez egy, vagy akár több beszúrást az adattáblába, ha tömegesen hajtanánk végre). De! Nem engedjük meg azt, hogy ilyen módon például a repülőjárat(ok) árát (price) beszúrja valaki. Így ezzel a $fillable tömbbel, mi egyfajta védelmet adunk a nem kívánt adatok feltöltésével szemben. A $fillable adattag ellentettje a $guarded, a funkciója ugyanaz, tehát védelmet biztosít az adattáblánk javára. Ő viszont azt várja el, hogy az ő tömbjébe azokat a mezőket soroljuk fel, amik adatbetöltés-elleni védelmet kapjanak. Így például, ha a $guarded egy üres tömb, akkor semelyik mező nem fog adatbetöltés elleni védelmet kapni, míg ha a $fillable tömb lenne üres, akkor pedig semelyik mezőbe nem engednénk adatot betölteni... Elsőre egy picit talán ez bonyolultnak tűnhet, de majd fogjuk gyakorolni a jövőben. Miután elmentettük a Flight.php hajtsuk végre ugyanazt a create()-es utasítást a Tinker-ben. De még mindig ugyanazt a hibát kapjuk... ez azért van, mert a Tinker nem érzékeli, hogy mi megváltoztattuk a programunk kódját. Ő azzal a programmal teszi lehetővé az interakciót, ami az elindításakor létezett. CTRL + c-vel ki tudunk lépni a Tinker-ből, majd indítsuk újra és hajtsuk végre megint a create()-es utasítást.

Eredményül vissza is kapjuk, hogy létrejött ez az új légijárat, a flights adatbázis táblában a 3 id-t kapta, ezt ellenőrizhetjük a MySQL Workbench segítségével is. Nálam így néz ki a tevékenységek végrehajtása után a flights tábla tartalma:

Csináltunk már beszúrást, frissítést (tehát mentéseket) továbbá lekérdezéseket is. Egy dolog van hátra, hogy a törlést is kipróbáljuk. Töröljük a légijáratokat egyesével, vagy akár tömegével, de persze, ha még gyakorolni szeretnénk inkább az Eloquent működését és a beszúrást, frissítést, lekérdezéseket, akkor csak egy adatsort töröljünk most, például azt, amelyik a legdrágább (először csak kiírom a legdrágábbat, a második sorral törlöm is a flights adatbázistáblából):

App\Models\Flight::orderBy('price', 'desc')->first();
App\Models\Flight::orderBy('price', 'desc')->first()->delete();

Elég sokmindent kipróbáltunk most. Összefoglalásként elmondható, hogy a Tinker-be beírt sorok a programkódjainkban is ugyanígy működnének, de milyen hasznos ez az eszköz, hogy kipróbálhattuk vele az együttműködést az alkalmazásunkkal.

Azt nyilván nem tudom megtenni, hogy az összes létező adatbázis lekérdezésre példát mutatok, de ennyi bevezető után talán mindenki el tud már indulni, és tudja alkalmazni az adatbázisok területéről megszerzett tudását is.


A bejegyzés során feltett kérdésre a válasz: timestamp típus (és nullable módosító hozzáadása) esetén nem csak azt az információt tudhattuk volna meg, hogy elindult-e az adott járat, hanem azt is, hogy mikor. Ha az adott repülőjáratnál ez a mező NULL értékkel szerepelne az adattáblában, az nyilván azt jelenti, hogy nem indult még el. Pillantsunk rá az arrived_at mezőre, ott már ezt az elgondolást sikeresen beépítettem, hogy látható legyen rögtön a különbség.