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\mongodatabase
Ezt 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
datadriven
legyen. 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
Entities
mappában találhatók az entitás osztályok, a megoldást pedig aProgram.cs
fájlba írjuk. - Nézzük meg a
Program.cs
tartalmát. Itt már megtalálható a MongoDB kommunikációhoz szükséges inicializáció.- Az
IMongoClient
interfész tartalmazza az adatbázissal való kommunikációhoz szükséges metódusokat. Ezeket nem fogjuk közvetlenül használni. - Az
IMongoDatabase
interfész reprezentálja adatadriven
adatbá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
Entities
mappá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!
-
Í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 aVAT
entitásosztályokat. Miért van aProduct
entitásban[BsonId]
-val ellátott mező, és miért nincs azVAT
osztályban? -
Hozz létre entitásosztályt a
Category
entitásnak, és vedd fel hozzá a megfelelőIMongoCollection<Category>
mezőt.
Megoldás
-
A
Product
osztály aproducts
gyűjteményt reprezentálja az adatbázisban, ezért tartozik hozzá egyediObjectID
ami alapján hivatkozni tudunk rá az adatbázis felé. Ezzel szemben azVAT
osztály aProduct
egy 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
Category
néven.Nézzük meg először VSCode-ban, hogy milyen adattagok találhatók a
categories
kollekcióban lévő dokumentumokban.Ez alapján létre tudjuk hozni a
Category
osztályt anEntities
mappá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.cs
fájlban vegyül fel az új kollekció interfészt.private static IMongoCollection<Category> categoriesCollection;
Az
initialize
metódusban pedig inicializáljuk is ezt a kollekciót.categoriesCollection = database.GetCollection<Category>("categories");
Feladat 3: Adatmódosítások¶
Az IMongoColection<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 sorod á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 toys
kategóriát. Amennyiben nem létezik, hozd létre." Ehhez aFindOneAndUpdate
parancs 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.