ORM-alapú adatbáziskezelés alapjainak elsajátítása CRUD-műveletek írásán keresztül. LINQ to Entities-lekérdezések írásának gyakorlása.
A labor elvégzéséhez szükséges eszközök:
Amit érdemes átnézned:
Felkészülés ellenőrzése:
Az utolsó feladatot leszámítva a feladatokat a gyakorlatvezetővel együtt oldjuk meg. Az utolsó feladat önálló munka, amennyiben marad rá idő. A közös feladatok megoldásai megtalálhatóak az útmutatóban is. Előbb azonban próbáljuk magunk megoldani a feladatot!
Az adatbázis az adott géphez kötött, ezért nem biztos, hogy a korábban létrehozott adatbázis most is létezik. Ezért először ellenőrizzük, és ha nem találjuk, akkor hozzuk létre újra az adatbázist. (Ennek mikéntjét lásd a Tranzakciókezelés gyakorlat anyagában.)
Az EF, mint ORM-eszköz használatához az alábbi összetevőkre van szükség:
Az objektummodellt és a leképezést generáltatni fogjuk az adatbázis alapján - ez az ún. Reverse Engineering modellezési módszer.
Main
függvényt írni, a belépési pontnak szánt kódfájlban (jelenleg a Program.cs-ben) írhatunk egyből utasításokat, nem kell se függvényt, se osztályt létrehoznunk.Install-Package Microsoft.EntityFrameworkCore.Tools -Version 8.0.13
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 8.0.13
Ezek a csomagok függőségként magát az Entity Framework Core-t is telepítik.
kulcs=érték
-párok pontosvesszővel elválasztva. Nekünk most az alábbi adatok kellenek:
A connection string szerkezete gyártónként eltér és elég sok paramétere lehet. Bővebben itt.
Szintén PMC-ben:
Scaffold-DbContext -Connection "Server=(localdb)\mssqllocaldb;Database=<neptun>;Trusted_Connection=True;" -Provider Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Context ACMEShop -NoPluralize
A generált kódban figyeljük meg az alábbiakat:
Termek.Afa
). Ezek alkotják az objektummodellt.DbSet<RekordTipus>
propertyket, ezek a tábláknak felelenek megOnConfiguring
függvény), pl. kapcsolódási adatokat, bár azt inkább külön konfig fájlban szoktuk tárolni, nem a kódban (erre van is figyelmeztetés a generált kódban)OnModelCreating
függvényben. Minden rekordtípus minden property-jére megadja, hogy melyik tábla melyik oszlopára lépződik. Navigációs property-knél megadja a kapcsolat egyes résztvevőinek számosságát (1-N-es, N-N-es), valamint ha van, akkor az idegenkulcs-kényszert is.Mindezzel minden összetevőt létrehoztunk az EF-alapinfrastruktúrához.
Írjuk meg az első EF Core-lekérdezésünket. Az SQL-mérés alapján itt is kérdezzük le az összes vevőt, majd írjuk ki azonosítójukat, nevüket és felhasználói nevüket is.
A Program.cs-be:
using (var ctx = new ACMEShop()) // Context létrehozása. Using blokk, mert használat után illik az adatbáziskapcsolatot lezárni.
{
foreach (var vevo in ctx.Vevo) // A ctx.Vevo a lekérdezésünk, ami a Vevo táblát reprezentálja.
{
Console.WriteLine($"{vevo.Nev} ({vevo.Id}, {vevo.Login})");
}
}
Próbáljuk ki, és örvendezzünk, hogy milyen egyszerű és elegáns a kód. Semmilyen SQL stringet / SQL-kódot nem kellett írnunk.
Minden olyan ORM használatakor, ahol az ORM állítja elő az SQL-t, rendkívül fontos, hogy lássuk, milyen SQL fut le az adatbázisszerveren. Nyomkövetni lehet az adatbázis oldalán (adatbáziseszközzel), illetve a programunk oldalán (nyomkövető komponenssel) is. Előbbire példa SQL Server esetén az SQL Server Profiler Trace eszköze. Mi most az utóbbi irányt követjük. A context OnConfiguring
függvényébe:
optionsBuilder.UseSqlServer("connection string") //ez a rész maradjon változatlan
.LogTo(message => System.Diagnostics.Debug.WriteLine(message),LogLevel.Information); //ez a rész ékelődjön be a ; elé
A debug kimenet az Output ablakra van kötve - viszont csak akkor, ha a Visual Studio debugger csatlakoztatva van. Ezért fontos, hogy ha látni akarjuk az EF naplókat, akkor Debug módban (zöld nyíl, F5
billentyű) futtassuk az alkalmazást.
Próbáljuk ki, az Output ablak alján meg kell jelennie egy hasonló SQL-nek:
info: [időbélyeg] RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (26ms) [Parameters=[], CommandType=’Text’, CommandTimeout=’30’]
SELECT [v].[ID], [v].[Email], [v].[Jelszo], [v].[KozpontiTelephely], [v].[Login], [v].[Nev], [v].[Szamlaszam]
FROM [Vevo] AS [v]
Ha hibakeresés miatt részletesebb naplóra van szükség, akkor a LogLevel
típusú paramétert (ideiglenesen) állítsuk Loglevel.Debug
-ra.
Minden részfeladatot a using
blokkon belül írjunk. Ha zavar a többi részfeladat kódja, kommentezzük ki őket. Minden részfeladatnál ellenőrizzük a kiírt eredményt az adatbázisadatok alapján is (pl. Management Studio-ban) és figyeljük meg a generált SQL-t is az Output ablak alján.
Kérdezze le az összes vevőt, írja ki a nevüket, azonosítójukat és felhasználónevüket! - Ezt már megoldottuk!
foreach (Vevo vevo in ctx.Vevo)
{
Console.WriteLine($"{vevo.Nev} ({vevo.Id}, {vevo.Login})");
}
Listázza ki, hogy eddig milyen nevű termékeket rendeltek!
//bármelyik jó a két alábbi lekérdezés közül
var termeknevek = ctx.MegrendelesTetel.Select(mt => mt.Termek.Nev).Distinct(); //MegrendelesTetel -> Termek
//var termeknevek = ctx.Termek.Where(mt => mt.MegrendelesTetel.Any()).Select(t => t.Nev); //Termek -> MegrendelesTetel
foreach (string? tn in termeknevek)
{
Console.WriteLine(tn);
}
Használjuk a navigációs property-ket és a System.Linq
névtérben található LINQ operátorokat. Kiindulhatunk a termék, illetve a megrendeléstétel táblából (DbSet
-ből) is. Érdemes kipróbálni mind a két változatot - nem ugyanolyan SQL generálódik.
A string
utáni kérdőjel nem kötelező, azzal jelezzük, hogy az érték lehet null
is. Hiszen a terméknév nem kötelező adat az adatbázisban.
Hány nem teljesített megrendelésünk van (a státusz alapján)?
Console.WriteLine(ctx.Megrendeles.Count(mr => mr.Statusz.Nev != "Kiszállítva"));
Ez ujjgyakorlat navigációs propertyt használva.
Melyek azok a fizetési módok, amiket soha nem választottak a megrendelőink?
var fmList=ctx.FizetesMod.Where(fm => !fm.Megrendeles.Any()).Select(fm=>fm.Mod);
foreach (string? fm in fmList)
{
Console.WriteLine(fm);
}
Rögzítsünk egy új vevőt! Írjuk ki az újonnan létrejött rekord kulcsát!
var ujvevo = new Vevo
{
Nev = "Teszt Elek",
Login = "t.elek",
Jelszo = "******",
Email = "t.elek@email.com"
};
ctx.Add(ujvevo);
ctx.SaveChanges();
Console.WriteLine(ujvevo.Id);
Az adatváltozásokat (pl. beszúrások) a kontext gyűjti. Az Add
sor után a kontext nyilvántartja az új példányunkat új rekordként, de még nem csinál az adatbázison semmit. A SaveChanges
a felgyűlt módosításokat egy füst alatt érvényesíti az adatbázison és vissza is frissíti az érintett C# oldali objektumokat - így a SaveChanges
után az új vevő azonosítója ki lesz töltve. Debuggerrel láthatjuk is, ahogy a SaveChanges
soron átjutva kitöltődik az azonosító. Ellenőrizzük a változást az adatbázisban.
A megoldás után ezt a részt kommentezzük ki, ne szúrjunk be minden kipróbálásnál új sort.
A kategóriák között hibásan szerepel a Fajáték kategórianév. Javítsuk át a kategória nevét Fakockákra!
var fakocka = ctx.Kategoria.Single(k => k.Nev == "Fajáték");
fakocka.Nev = "Fakockák";
ctx.SaveChanges();
Első lépésként le kell kérdeznünk a módosítandó entitást, elvégezni a módosítást, amit a kontext megint csak nyilvántart, végül érvényesíteni a módosításokat. A Watch ablakban figyeljük meg a kontext által nyilvántartott állapot változását a ctx.Entry(fakocka).State
kifejezés figyelésével. Ellenőrizzük a változást az adatbázisban.
A megoldás után ezt a részt kommentezzük ki, ha nincs az adatbázisban fajáték, a Single
kivételt fog dobni, mivel pontosan egy darab rekordot vár.
Sajnos ez a művelet így két SQL parancs (egy SELECT
és egy UPDATE
), jelenleg az EF Core nem tudja ezt egy utasításként megoldani, bár vannak hozzá kiterjesztések erre a célra.
Mely kategóriákban van a legtöbb termék? Írjuk ki ezen kategóriák nevét.
//naív megoldás, csak egy eredmény
Console.WriteLine(ctx.Kategoria.OrderByDescending(k => k.Termek.Count())
.Select(k=>k.Nev).First());
//navigációs propertyvel - kivételt dob!
var maxkat=ctx.Kategoria
.Where(k => k.Termek.Count == ctx.Kategoria.Max(kk=>kk.Termek.Count))
.Select(k=>k.Nev);
foreach (var k in maxkat)
{
Console.WriteLine(k);
}
//navigációs propertyvel - működik, de két lekérdezés
var maxkat = ctx.Kategoria.Select(kk => kk.Termek.Count)
.AsEnumerable() // ez elsüti a lekérdezést, minden kategória termékszámát lekérdezzük
.Max();
var ktg = ctx.Kategoria.Where(k => k.Termek.Count == maxkat).Select(k=>k.Nev);
foreach (var k in ktg)
{
Console.WriteLine(k);
}
//másik megközelítés: azokat a kategóriákat keressük, amiknél nincs több termékes kategória
//egy lekérdezés és működik is
var maxkat= ctx.Kategoria
.Where(k1=>!ctx.Kategoria.Any(k2=> k2.Termek.Count > k1.Termek.Count))
.Select(k=>k.Nev);
foreach (var k in maxkat)
{
Console.WriteLine(k);
}
Sajnos elég nehéz olyan lekérdezést összerakni C#-ban, ami az SQL összerakásánál ne dobna kivételt vagy ne több lekérdezésből állna . Nem minden logikailag helyes, forduló LINQ kód alakítható SQL-lé, ráadásul ez csak futási időben derül ki. Érdemes ilyenkor megírni a sima SQL-t és ahhoz hasonlóan összerakni a C# kódot, plusz minden lekérdezést legalább egyszer erősen ajánlott kipróbálni futás közben is.
Az egyes telephelyekre hány rendelés volt eddig? Írjuk ki a telephelyek azonosítóját és a rendelések számát!
var tmegrList = ctx.Telephely.Select(t => new { t.Id, MegrendelesSzam = t.Megrendeles.Count });
foreach (var tmegr in tmegrList)
{
Console.WriteLine($"{tmegr.Id}-{tmegr.MegrendelesSzam}");
}
Gyakran alkalmazzuk a Select
operátorban az anonim típusokat. Ezekben a típusokban mi mondjuk meg, hogy milyen nevű property-k legyenek és azok hogyan töltődjelenek fel. Bár a típus nevét nem ismerjük (anonim), de tudunk hivatkozni a property-jeire a megadott névvel.
Lásd a közös feladatokból a hasonlót. Egy lehetséges megoldás, ha először a megrendeléseket csoportosítjuk a kapcsolódó város neve szerint a GroupBy
operátorral, majd vesszük minden csoport kulcsát és a csoport számosságát. Ezután ezen allekérdezésből kiindulva kell kikeresni, hogy melyek azok az allekérdezés elemek, amelyeknél nincsenek nagyobb számosságú más allekérdezés elem.
Így használhatjuk a SelectMany
LINQ operátort egy v
vevő összes rendelésének összeszedésére: v.Telephely.SelectMany(t => t.Megrendeles)
. A SelectMany
-t akkor használjuk, ha egy kollekció minden elemhez egy kollekciót rendelünk, de az eredményt nem listák listájaként szeretnénk megkapni, hanem csak sima kilapított listaként
EF.Functions.DateDiffDay(<dátum>,<határidő>)
megadja a dátum
és határidő
között eltelt napok számát)Lásd a közös feladatokból a hasonlót. Egy lehetséges megoldás, ha először minden megrendeléshez kiszámoljuk a kapcsolódó vevő nevét és a megrendelés tételeinek számát. Ezután ezen allekérdezésből kiindulva kell kikeresni, hogy melyek azok az allekérdezés elemek, emelyeknél nincsenek nagyobb tételszámú más allekérdezés elem.
Az itt található oktatási segédanyagok a BMEVIAUBB04 tárgy hallgatóinak készültek. Az anyagok oly módú felhasználása, amely a tárgy oktatásához nem szorosan kapcsolódik, csak a szerző(k) és a forrás megjelölésével történhet.
Az anyagok a tárgy keretében oktatott kontextusban értelmezhetőek. Az anyagokért egyéb felhasználás esetén a szerző(k) felelősséget nem vállalnak.