MongoDB¶
A gyakorlat célja, hogy a hallgatók megismerjék a MongoDB általános célú dokumentumkezelő adatbázis alapvető működését, valamint a MongoDB C#/.NET Driver használatát.
Entity Framework Core provider
A MongoDB-hoz is létezik már Entity Framework Core provider, azonban a gyakorlaton a MongoDB C#/.NET Driver használatát fogjuk gyakorolni, hogy a hallgatók megismerjék a MongoDB alacsonyabb szintű sajátosságait.
Előfeltételek¶
A labor elvégzéséhez szükséges eszközök:
- MongoDB Community Edition
- Microsoft Visual Studio 2022
- VSCode
- MongoDB for VSCode kiegészítő
- Adatbázis létrehozó script: mongo.js
- Kiinduló alkalmazás kódja: https://github.com/bmeviauac01/gyakorlat-mongo-kiindulo
Amit érdemes átnézned:
- C# nyelv és Linq kifejezések
- MongoDB előadás
- MongoDB használata segédlet
Gyakorlat menete¶
A gyakorlat végig vezetett, a gyakorlatvezető utasításai szerint haladjunk. Egy-egy részfeladatot próbáljunk meg először önállóan megoldani, utána beszéljük meg a megoldást közösen.
Emlékeztetőként a megoldások is megtalálhatóak az útmutatóban is. Előbb azonban próbáljuk magunk megoldani a feladatot!
Feladat 0: Adatbázis létrehozása, projekt megnyitása¶
-
Nyiss egy PowerShell konzolt (a Start menüben keress rá a PowerShell-re és indítsd el, de ne az "ISE" végűt, az nem a konzol).
-
Másold be az alábbi sorokat és futtasd le enterrel. Az utolsó utasításban az elérési út lehet, hogy kisebb javítást igényel, pl. más verziószám miatt.
Remove-Item c:\work\mongodatabase -Recurse -ErrorAction Ignore New-Item -Type Directory c:\work\mongodatabase c:\tools\mongodb\bin\mongod.exe --dbpath c:\work\mongodatabaseEzt az ablakot hagyjuk nyitva, mert ebben fut a szerver. Leállítani Ctrl+C billentyűkombinációval lehet majd a végén.
-
Indítsuk el a VSCode-ot és csatlakozzunk a MongoDB szerverhez.
-
Hozzuk létre az adatbázist a kapcsolat nevén (localhost) jobb egérrel kattintva. Ez egy playground script ablakot nyit, ahova másoljuk be az adatbázis létrehozó scriptünket innen, és futtassuk le az fejlécben található fekete "lejátszás" gombbal Az adatbázis neve
datadrivenlegyen. Ennek hatására létre kell jönnie a collection-öknek - nyissuk le az adatbázis elemeit ennek ellenőrzéséhez. -
Töltsük le a kiinduló projekt vázat!
- Nyissunk egy új command prompt-ot vagy PowerShell konzolt (ne azt használjuk, amelyikben a szerver fut)
- Navigáljunk el egy tetszőleges mappába, például
c:\work\NEPTUN -
Adjuk ki a következő parancsot:
git clone https://github.com/bmeviauac01/gyakorlat-mongo-kiindulo.git
-
Nyissuk meg a forrásban az sln fájlt Visual Studio-val. Vizsgáljuk meg a projektet.
- Ez egy .NET konzol alkalmazás. Felépítésében hasonlít az Entity Framework gyakorlaton látotthoz: az
Entitiesmappában találhatók az entitás osztályok, a megoldást pedig aProgram.csfájlba írjuk. - Nézzük meg a
Program.cstartalmát. Itt már megtalálható a MongoDB kommunikációhoz szükséges inicializáció.- Az
IMongoClientinterfész tartalmazza az adatbázissal való kommunikációhoz szükséges metódusokat. Ezeket nem fogjuk közvetlenül használni. - Az
IMongoDatabaseinterfész reprezentálja adatadrivenadatbázist a MongoDB-n belül. - A különböző
IMongoCollection<TEntity>interfészek pedig a különböző kollekciókat reprezentálják. Ezeket használva tudunk lekérdezéseket és módosító utasításokat kiadni.
- Az
- Az adatbázisunk entitásainak C# osztályra való leképezése az
Entitiesmappában található. Különbség itt az Entity Frameworkhöz képest, hogy itt ezt nekünk kézzel kell elkészítenünk.- Az entitások egy részének a leképezése már megtalálható itt.
- A labor során még visszatérünk ide, és fogunk magunk is készíteni entitás osztályt.
- Ez egy .NET konzol alkalmazás. Felépítésében hasonlít az Entity Framework gyakorlaton látotthoz: az
Feladat 1: Lekérdezések¶
A leképzett adatmodellen fogalmazd meg az alábbi lekérdezéseket a MongoDB C#/.NET Driver használatával. Írd ki konzolra az eredményeket.
-
Listázd azon termékek nevét és raktárkészletét, melyből több mint 30 darab van raktáron!
-
Írj olyan lekérdezést, mely kilistázza azon megrendeléseket, melyekhez legalább két megrendeléstétel tartozik!
-
Készíts olyan lekérdezést, mely kilistázza azokat a megrendeléseket, melyek összértéke több mint 30000 Ft! Az eredményhalmaz kiírásakor a vevő ID-t követően soronként szerepeljenek az egyes tételek (Termék ID, mennyiség, nettó ár).
-
Listázd ki a legdrágább termék adatait! Ha több ilyen termék is van (több terméknek is lehet ugyanaz az ára), mindegyik adatát ki kell listázni.
-
Írj olyan lekérdezést, mely kilistázza azon termékeket, melyből legalább kétszer rendeltek!
Megoldás
-
Ehhez a feladathoz csupán a termékeket reprezentáló gyűjteményben kell egy egyszerű lekérdezést kiadnunk. A szűrési feltételt kétféleképpen is megfogalmazhatjuk: lambda kifejezés segítségével, és kézzel összerakva is.
Console.WriteLine("***** Első feladat *****"); // 1.1 első megoldás Console.WriteLine("\t1.1 1. megoldás:"); var qProductAndStock1 = productsCollection .Find(p => p.Stock > 30) .ToList(); foreach (var p in qProductAndStock1) Console.WriteLine($"\t\tName={p.Name}\tStock={p.Stock}"); // 1.1 második megoldás Console.WriteLine("\t1.1 2. megoldás:"); var qProductAndStock2 = productsCollection .Find(Builders<Product>.Filter.Gt(p => p.Stock, 30)) .ToList(); foreach (var p in qProductAndStock2) Console.WriteLine($"\t\tName={p.Name}\tStock={p.Stock}"); -
Ez a feladat nagyon hasonló ez előzőhöz. Figyeljük meg, hogy az SQL-es adatbázis séma esetén ehhez már
JOIN-t (Navigation Property) kellett alkalmazni. Ezzel szemben itt minden szükséges adat a megrendelés kollekcióban található.// 1.2 első megoldás Console.WriteLine("\t1.2 1. megoldás:"); var qOrderItems1 = ordersCollection .Find(o => o.OrderItems.Length >= 2) .ToList(); foreach (var o in qOrderItems1) Console.WriteLine($"\t\tCustomerID={o.CustomerID}\tOrderID={o.ID}\tItems={o.OrderItems.Length}"); // 1.2 második megoldás Console.WriteLine("\t1.2 2. megoldás:"); var qOrderItems2 = ordersCollection .Find(Builders<Order>.Filter.SizeGte(o => o.OrderItems, 2)) .ToList(); foreach (var o in qOrderItems2) Console.WriteLine($"\t\tCustomerID={o.CustomerID}\tOrderID={o.ID}\tItems={o.OrderItems.Length}"); -
Ehhez a feladathoz már nem elegendő számunkra a sima lekérdezés kifejezőereje, így az aggregációs pipeline-t kell alkalmaznunk. Figyeljük meg azonban, hogy a séma felépítése miatt továbbra is minden szükséges adat rendelkezésre áll egyetlen gyűjteményben.
// 1.3 Console.WriteLine("\t1.3:"); var qOrderTotal = ordersCollection .Aggregate() .Project(order => new { CustomerID = order.CustomerID, OrderItems = order.OrderItems, Total = order.OrderItems.Sum(oi => oi.Amount * oi.Price) }) .Match(order => order.Total > 30000) .ToList(); foreach (var o in qOrderTotal) { Console.WriteLine($"\t\tCustomerID={o.CustomerID}"); foreach (var oi in o.OrderItems) Console.WriteLine($"\t\t\tProductID={oi.ProductID}\tPrice={oi.Price}\tAmount={oi.Amount}"); } -
A legdrágább termékek lekérdezéséhez két lekérdezést kell kiadnunk: először lekérdezzük a legmagasabb árat, utána pedig lekérdezzük azokat a termékeket, melyeknek a nettóára megegyezik ezzel az értékkel.
// 1.4 Console.WriteLine("\t1.4:"); var maxPrice = productsCollection .Find(_ => true) .SortByDescending(p => p.Price) .Limit(1) .Project(p => p.Price) .Single(); var qProductMax = productsCollection .Find(p => p.Price == maxPrice) .ToList(); foreach (var t in qProductMax) Console.WriteLine($"\t\tName={t.Name}\tPrice={t.Price}"); -
Ez a feladat azért nehéz a jelenlegi adatbázissémánk mellett, mert itt már nem igaz az, hogy egyetlen kollekcióban rendelkezésre áll minden adat. Szükségünk van ugyanis a termék kollekcióból a termék nevére és raktárkészletére, a megrendelések kollekcióból pedig a termékhez tartozó megrendelések számára.
Ilyen helyzetben MongoDB esetén kénytelenek vagyunk kliensoldalon (értsd: C# kódból) "joinolni". A megoldás itt tehát hogy lekérdezzük az összes megrendelést, majd pedig C#-ból, LINQ segítségével összegyűjtjük az adott termékhez tartozó megrendeléstételeket. Ezután lekérdezzük az adatbázisból a termékeket is, hogy azok adatai is rendelkezésünkre álljanak.
// 1.5 Console.WriteLine("\t1.5:"); var qOrders = ordersCollection .Find(_ => true) .ToList(); var productOrders = qOrders .SelectMany(o => o.OrderItems) // Egyetlen listába gyűjti a tételeket .GroupBy(oi => oi.ProductID) .Where(p => p.Count() >= 2); var qProducts = productsCollection .Find(_ => true) .ToList(); var productLookup = qProducts.ToDictionary(p => p.ID); foreach (var p in productOrders) { var product = productLookup.GetValueOrDefault(p.Key); Console.WriteLine($"\t\tName={product?.Name}\tStock={product?.Stock}\tOrders={p.Count()}"); }A fenti nem túl elegáns megoldás, és csak kis adatbázisok esetén működik. Ha valódi körülmények között szembesülünk ezzel a feladattal, két lehetőségünk van: átdolgozni az adatbázis sémát (pl. a megrendelésbe belementeni a termék adatait - denormalizáció), avagy a MongoDB aggregációs pipeline-jának használatával a fenti módszerhez hasonlóra "rávenni" a MongoDB szervert (amire képes ugyan, de le fogja terhelni)
Feladat 2: Entitásosztály létrehozása¶
-
Vizsgáld meg a
Productés aVATentitásosztályokat. Miért van aProductentitásban[BsonId]-val ellátott mező, és miért nincs azVATosztályban? -
Hozz létre entitásosztályt a
Categoryentitásnak, és vedd fel hozzá a megfelelőIMongoCollection<Category>mezőt.
Megoldás
-
A
Productosztály aproductsgyűjteményt reprezentálja az adatbázisban, ezért tartozik hozzá egyediObjectIDami alapján hivatkozni tudunk rá az adatbázis felé. Ezzel szemben azVATosztály aProductegy beágyazott objektuma, önmagában nem jelenik meg gyűjteményként. Ezért nem tartozik hozzáObjectIDérték. -
Hozzunk létre új POCO osztályt
Categorynéven.Nézzük meg először VSCode-ban, hogy milyen adattagok találhatók a
categorieskollekcióban lévő dokumentumokban.Ez alapján létre tudjuk hozni a
Categoryosztályt anEntitiesmappában.using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace BME.DataDriven.Mongo.Entitites { public class Category { [BsonId] public ObjectId ID { get; set; } public string Name { get; set; } public ObjectId? ParentCategoryID { get; set; } } }A
Program.csfájlban vegyül fel az új kollekció interfészt.private static IMongoCollection<Category> categoriesCollection;Az
initializemetódusban pedig inicializáljuk is ezt a kollekciót.categoriesCollection = database.GetCollection<Category>("categories");
Feladat 3: Adatmódosítások¶
Az IMongoCollection<TEntity> interfész nem csak lekérdezéshez használható, hanem rajta keresztül módosítások is végrehajthatóak.
-
Írj olyan MongoDB C#/.NET Driverre épülő C# kódot, mely a "LEGO" kategóriájú termékek árát megemeli 10 százalékkal!
-
Hozz létre egy új kategóriát a Expensive toys néven, és sorold át ide az összes olyan terméket, melynek ára, nagyobb, mint 8000 Ft!
-
Töröld ki az összes olyan kategóriát, amelyhez nem tartozik termék.
Megoldás
-
Először lekérdezzük a megfelelő kategória ID-ját, majd az ehhez tartozó termékekre adunk ki módosító utasítást.
Console.WriteLine("***** Harmadik feladat *****"); //3.1 Console.WriteLine("\t3.1:"); var categoryLegoId = categoriesCollection .Find(c => c.Name == "LEGO") .Project(c => c.ID) .Single(); var qProductLego = productsCollection .Find(p => p.CategoryID == categoryLegoId) .ToList(); Console.WriteLine("\t\tMódosítás előtt:"); foreach (var p in qProductLego) Console.WriteLine($"\t\t\tName={p.Name}\tStock={p.Stock}\tÁr={p.Price}"); productsCollection.UpdateMany( filter: p => p.CategoryID == categoryLegoId, update: Builders<Product>.Update.Mul(p => p.Price, 1.1)); qProductLego = productsCollection .Find(p => p.CategoryID == categoryLegoId) .ToList(); Console.WriteLine("\t\tMódosítás után:"); foreach (var p in qProductLego) Console.WriteLine($"\t\t\tName={p.Name}\tStock={p.Stock}\tÁr={p.Price}"); -
MongoDB segítségével tranzakció nélkül atomikusan el tudjuk végezni a következő feladatot: "Kérem a
Expensive toyskategóriát. Amennyiben nem létezik, hozd létre." Ehhez aFindOneAndUpdateparancs használatára van szükségünk.//3.2 Console.WriteLine("\t3.2:"); var catExpensiveToys = categoriesCollection.FindOneAndUpdate<Category>( filter: c => c.Name == "Expensive toys", update: Builders<Category>.Update.SetOnInsert(c => c.Name, "Expensive toys"), options: new FindOneAndUpdateOptions<Category, Category> { IsUpsert = true, ReturnDocument = ReturnDocument.After }); productsCollection.UpdateMany( filter: p => p.Price > 8000, update: Builders<Product>.Update.Set(p => p.CategoryID, catExpensiveToys.ID)); var qProdExpensive = productsCollection .Find(p => p.CategoryID == catExpensiveToys.ID) .ToList(); foreach (var p in qProdExpensive) Console.WriteLine($"\t\tName={p.Name}\tPrice={p.Price}"); -
Lekérdezzük azokat a kategóriákat amelyekhez tartozik termék, majd pedig töröljük azokat, amelyek nem tartoznak ezek közé.
//3.3 Console.WriteLine("\t3.3:"); Console.WriteLine($"\t\tMódosítás előtt {categoriesCollection.CountDocuments(_ => true)} db kategória"); var qProductCategory = new HashSet<ObjectId>( productsCollection .Find(_ => true) .Project(p => p.CategoryID) .ToList()); categoriesCollection.DeleteMany(c => !qProductCategory.Contains(c.ID)); Console.WriteLine($"\t\tMódosítás után {categoriesCollection.CountDocuments(_ => true)} db kategória");Vegyük észre, hogy ez az utasítás nem atomikus. Ha közben vettek fel új terméket, akkor lehet, hogy olyan kategóriát törlünk amihez azóta tartozik termék. Nem vettük figyelembe továbbá a kategóriák hierarchiáját sem.



