LINQ, azaz Language Integrated Queries (nyelvbe épített lekérdezések) a C# 3.0 nyelv és keretrendszer újdonsága, mellyel struktúrált típusbiztos lekérdézeseket készíthetünk lokális objektum gyűjteményeken és távoli data source-okon.
LINQ lekérdezéseket bármelyik IEnumerable<> interfészt implementáló gyűteményen végre lehet hajtani, legyen az tömb, lista, XML DOM vagy esetleg egy SQL Server táblája. A LINQ előnyei még a forditás idei típusellenőrzés és a dinamikus lekérdezés kompozíció.
A LINQ alaptípusok a System.Linq illetve System.Linq.Expressions névterekben definiáltak.
A lekérdezések alap egységei a sorozatok és az elemek. Sorozatok lehetnek az előbb említett IEnumerable<> generikus interfész által implementált gyűjtemények, és az elem ennek a sorozatnak bármely tagja. Pl. az alábbi példában a names a sorozat és Tom, Dick, Harry az elemek (a továbbiakban ezt a példát fogjuk használni):
Ezt lokális sorozatnak hívjuk, mivel egy lokális objektum-gyűjteményt reprezentál a memóriában.
A lekérdező operátor egy olyan metódus, ami transzformálja sorozatunkat. Egy lekérdező operátor egy bemenő sorozatot kap, és egy transzformált kimenő sorozatot ad vissza. A System.Linq Enumerable osztályában közel 40 lekérdező operátor van (mindegyik statikus kiterjesztett metódusként van implementálva). Ezek a standard lekérdező operátorok.
A legegyszerűbb lekérdezés egy sorozatból és egy operátorból áll. Például alkalmazhatjuk a Where operátort egy sima tömbön, azokat lekérdezve, melyeknek a hossza legalább 4:
Mivel a standard query operátorok kiterjesztesztett metódusok, a Where-t közvetlenül a names-en is meghívhatjuk:
A legtöbb lekérdező operátor argumentumként elfogad lambda kifejezést is. A lambda kifejezések segítenek irányítani és formában tartani a lekérdezést. Így néz ki egy lamdba lekérdezés pl.:
A bemenő argumentum egy bemenő elemnek felel meg. Esetünkben az n bemenő argumentum reprezentálja az összes nevet a tömbben, melyek string típusúak. A Where operátor egy bool típusú értéket vár el a lamdba kifejezéstől, amely, ha igazat ad vissza, akkor a kimenő sorozatba bele kell rakni. A szignatúra:
Pl. a következő metódus visszaadja az összes olyan nevet, amiben van a betű:
Eddig a lekérdezéseket kiterjesztett metódusok és lambda kifejezések segítségével végeztük el. De a C#-ban egy speciális szintaxis is definiálva van lekérdezések írásához, az úgynevezett query comprehension szintaxis. Itt van az előbbi lekérdezésünk ebben a szintaxisban:
A lambda szintaxist és a comprehension szintaxist részletezzük a továbbiakban.
A lambda lekérdezések a legrugalmasabbak és legalapvetőbbek. Megmutatjuk, hogyan lehet az operátorokat láncolni, hogy összetettebb lekérdezéseket tudjunk csinálni, és megmutatjuk, hogy a kiterjesztett metódus szintaxis miért fontos a folyamatban. Még azt is tárgyaljuk, hogyan kell lambda kifejezéseket megírni egy lekérdező operátornak, és bemutatunk néhány új operátort.
Összetettebb lekérdezések építéséhez több lekérdező operátort használunk, ezzel láncot alkotva. Az alábbi példa kiszűri az ’a’-t tartalmazó stringeket, hossz szerint rendezi őket, és az eredményt nagybetűssé alakítja:
Már bemutattuk a Where operátort, ami a bemenő sorozat szűrt változatát adja vissza. Az OrderBy operátor egy rendezett változatát adja vissza a bemenő sorozatnak, míg a Select metódus az, ahol a bemenő sorozat összes elemét transzformáljuk vagy leképezzük egy adott lambda kifejezéssel (esetünkben mindent nagybetűssé tettünk). A láncolásban az adatok jobbról balra haladnak, először szűrjük, rendezzük, utána képezzük le őket.
Megadhatjuk a lekérdezéseket progresszíven is:
Az utolsó változóban tárolt érték megegyezik a láncolás után keletkező változóban levő operátorral.
Miért fontosak a kiterjesztett metódusok?
A kiteresztett metódus szintaxisa helyett használhatjuk a hagyományos statikus metódus szintaxist is (előző példa). A fordító erre az alakra hozza a kiterjesztett metódus hívásokat.
A lambda kifejezés célja az egyes lekérdező operátoroktól függ. pl. A Where operátor azt mondja meg egy elemről, hogy az benne legyen vagy ne a kimenetben, az OrderBy-ban a lambda kifejezés átcsoportosítja azt a kulcsnak megfelelően, míg a Select operátor meghatározza, hogy egyes elemeken mit kell végrehajtani a kimenetre küldés előtt. A lamdba kifejezésre lehet úgy gondolni, mint egy visszacsatolásra. A lekérdező operátor a lamdba kifejezést igény esetén értékeli ki (tipikusan egyszer elemenként a bemenetben). A lambda kifejezések megengedik, hogy a saját logikádat beépítsd a lekérdező operátorokba. Ez teszi a lekérdező operátorokat sokoldalúvá (és emellett még egyszerűvé is). Itt van példának a Where metódus teljes implementációja (hibakezelés nélkül):
Az elemek természetes sorrendje lényeges a LINQ-ben. Pár lekérdező operátor erre támaszkodik, mint pl. Take, Skip és Reverse.
A Take az első x elemet veszi ki, a Skip elhagyja, míg a Reverse megfordítja az összes sorrendjét:
A Where és Select megőrzi a sorozatunk eredeti sorrendjét. A LINQ ahol tudja, megtartja az kezdősorozat sorrendjét.
Nem minden operátor ad vissza egy sorozatot. Az egyelemes operátorok egy elemet vesznek ki a bemenő sorozatból. Pl. First, Last, Single, ElementAt :
Az aggregációs operátorok skaláris értéket adnak vissza (általában numerikusat):
A mennyiségi operátorok egy bool értéket:
Mivel nem egy sorozatot ad vissza, több operátort már nem hívhatunk meg rajta, ezért általában a lánc végén szoktuk használni.
Néhány lekérdező operátornak két bemenő sorozatot kell megadnunk. Pl a Concat, ami az egyik sorozat után fűzi a másikat. Másik példa az Union , amely ugyanazt csinálja, de az azonos elemekből csak egy szerepel:
A C# szintaktikus gyorslehetőséget ad LINQ lekérdezések írásához a query comprehension szintaxis révén. Az előző fejezetben felhozott példa comprehension szintaxisban:
A comprehension lekérdezések egy frommal kezdődnek és egy select vagy group típusú lekérdezéssel zárul. A from egy iterációs változó, esetünkben n, amit úgy lehet felfogni, mint a bemenő gyűjtemény végigfutása – úgymint foreach.
Az comprehension lekérdezéseket a fordító lamdba szintaxisra hozza. Eléggé mechanikus módon csinálja – hasonlóan, mint a foreach kifejezéseket GetEnumerator és MoveNext hívásokra alakítja. Ez azt jelenti, hogy amit megírunk comprehension szintaxisban, megírható lamdba szintaxisban is. A forditó az alábbi formára hozza a példát:
A from kulcsszót követő azonosítót hívjuk így (példánkban n). Esetünkben a változó más sorozaton megy végig minden egyes hívásban:
Ez akkor lesz tiszta, ha megvizsgáljuk a fordító mechanikus fordítását lambda szintaxisra:
Így az iterációs változó mindig a megelőző kifejezés eredményein fut végig. Az egyetlen kivétel a szabály alól, ha egy kifejezés egy új változót vezet be. Három eset lehetséges:
A LINQ comprehension szintaxis látszólag úgy néz ki, mint az SQL szintaxisa, de a kettő nagyon is különbözik. LINQ lekérdezés C# kifejezés besűrítése, azaz a C# szabályait követi. Például a LINQ-ben nem használható változó a deklarációja előtt. SQL-ben a SELECT kifejezésnél egy tábla alias-ra hivatkozunk, mielőtt definiálnánk egy from kifejezésben.
Egy allekérdezés a LINQ-ben csupán még egy C# kifejezés és nincs szüksége speciális C# szintaxisra. SQL-ben az allekérdezésekre specális szabályok érvényesek.
A LINQ-kel az adatok balról jobbra haladnak a lekérdezésben. SQL-ben a sorrend többnyire véletlen.
Mind a lambda mind a comprehension szintaxisnak megvan a maga előnye.
A comprehension szintaxis sokkal egyszerűbb, ami az alábbiakat foglalja magába:
Egy operátort használó lekérdezéshez a lambda szintaxis rövidebb és kevésbé zsúfolt.
Ha egy lekérdező operátor nem támogatja a comprehension szintaxist, akkor keverhetjük a két szintaxist. Az egyetlen megkötés a comprehension szintaxis teljessége (emlékeztetőül: from-mal kezdődik és select-re vagy group-ra végződik)
Feltételezzük az alábbi tömböt:
Az alábbi példa megszámolja azoknak a neveknek a számát, melyek tartalmaznak ’a’ betűt:
A következő lekérdezés pedig ábécé sorrendben visszaadja az első nevet:
A kevert szinaxisú megközelítés néhány esetben hasznosabb az összetettebb lekérdezésekben. Az alábbi egyszerű példában lambda szintaxishoz kapcsolhatjuk bármilyen hátrányos következmény nélkül:
Az egyik legfontosabb tulajdonság, hogy a lekérdező operátorok nagy része nem akkor kerül végrehajtásra, amikor elkészítjük, hanem akkor, amikor bejárjuk (vagyis amikor meghívjuk rajta a MoveNext-et). Nézzük az alábbi lekérdezést:
A plusz szám, amit belecsempésztünk a listába a lekérdezés építése után, benne van az eredményben, mivel a foreach lekérdezés vagy bármilyen rendező illetve szűrő parancs előtt van. Ezt hívjuk késleltetett vagy lusta kiértékelésnek. Majdnem mindegyik standard lekérdező operátornak van késleltetett végrehajtása, néhány kivétellel:
A késleletett végrehajtás azért fontos, mert kettéválik a lekérdezés építés és végrehajtás. Ezzel lehetővé válik lekérdezés több lépésben történő építése és LINQ 2 SQL lekérdezések építése.
A késleltetett végrehajtásnak van másik következménye is: a késleltetett lekérdezés újra kiértékelődik, ha megint bejárjuk:
Van pár ok, ami miatt az újrakiértékelés nem túl előnyös:
A késleltetett végrehajtásnak van egy mellékhatása. Ha a lekérdezés lambda kifejezése lokális változóra hivatkozik, akkor külső változó szemantika vonatkozik rá. Azaz ha később megváltozik az értéke, megváltozik a lekérdezés is:
A lekérdező operátorokat késleltetett végrehatást dekorátor sorozatok visszaadásával működtetik. A hagyományos gyűjteményosztállyal ellentétben (mint pl. egy tömb vagy egy láncolt lista), a dekorátor sorozatnak nincs támogatott struktúrája önmagában az elemek tárolására. Ehelyett becsomagol egy sorozatot, ami futási időben adtunk meg, amihez permanens hozzáköti. Amikor a dekorátortól adatot kérünk, neki cserében a csomagolt bemenő sorozattól kell adatot kérnie.
A Where meghívása csupán a dekorátor csomagoló sorozatot hoz létre a bemenő sorozatra referenciával, a lambda kifejezést, és pár egyéb argumentumot. A bemenő sorozat akkor lesz bejárva, amikor a dekorátor sorozat.
Amikor bejárjuk a lessThanTen-t, valójában a tömböt a Where dekorátorán keresztül tesszük.
A lekérdező operátorok láncolása dekorátorok rétegződését hozza létre. Nézzük az alábbi lekérdezést:
Minden lekérdező operátor egy új dekorátort hoz létre, ami az előző sorozatot csomagolja be (olyan, mint egy Matrioska baba). Amikor bejárjuk a query -t, az eredeti tömbön fut a lekérdezés, és a dekorátorok rétegein vagy -láncán transzformálodik. A korábban említett példánkban a Select dekorátora az OrderBy dekorátorára hivatkozik, ami a Where dekorátorára hivatkozik, ami pedig az eredeti tömbre hivatkozik.
Egy allekérdezés egy lekérdezés egy másik lekérdezés lambda kifejezésében. Az alábbi példa egy allekérdezést használ a zenészek vezetékneveivel való rendezéshez:
m.Split mindegyik stringet szavak gyűjteményévé konvertálja, amin meghívuk a Last operátort. A Last egy allekérdezés, a query a külső lekérdezésre hivatkozik.
Az allekérdezések engedélyezettek, mert bármilyen érvényes C# kifejezést használhatunk a lambda kifejezés jobb oldalán. Az allekérdezés csak egy újabb C# kifejezés. Ez azt jelenti, hogy az allekérdezések szabályai a lamdba kifejezések szabályainak következményei (és a lekérdező operátorok viselkedésének).
Egy allekérdezés az őt bezáró kifejezés saját hatáskörében van és a külső lambda argumentumra hivatkozhat (vagy iterációs változóra a comprehension szintaxisban).
Last egy nagyon egyszerű allekérdezés. A következő lekérdezés az összes stringet visszaadja, melyek hossza megegyezik a legrövidebb string hosszával:
Ugyanez comprehension szintaxisban:
Egy elem vagy aggregációs operátor, mint pl. a First vagy Count egy allekérdezésben nem kényszeríti rá a külső lekérdezést azonnali végrehajtásra (a késleltetett végrehajtást továbbra is a külső lekérdezésnek tartja meg). Ez azért van, mert az allekérdezések indirekt hívottak (lokális lekérdezésnél egy delegálton keresztül vagy egy kifejezésfán keresztül egy interpretált lekérdezésben).
Ebben a fejezetben 3 stratégiát tárgyalunk az összetettebb lekérdezésekhez:
A fejezet elején található példa:
Több lehetséges előnye is van a progresszív lekérdezés-építésnek:
Ezt comprehension szintaxisba átrakni problémás, mivel ez a szintaxis a where-orderby-select rendet követi annak érdekében, hogy a fordító felismerje. Ha megpróbáljuk átírni hasonlóan, az eredmény különböző lesz:
Szerencsére, több mód is van rá, hogy visszakapjuk az eredeti eredményt comprehension szintaxisban. Az első a progresszív lekérdezés:
Az into kulcsszó lehetőséget nyújt a lekérdezés „folytatására”, egy leképezés és egy rövidítés a progresszív lekérdezésben. Az előző lekérdezés újraírható into-val:
Csak select vagy group után használható. Az into „újraindítja” a lekérdezést, lehetőséget adva egy új where, orderby és select kifejezésnek.
Lambda szintaxisban az into megfelelője egyszerűen az operátorok egy hosszabb lánca.
Hatóköri szabályok:
Az összes lekérdező változó az into után a hatókörön kívűlre kerül. A következő nem fog fordulni:
Hogy lássuk miért, nézzük meg, hogy lambda szintaxisban hogy néz ki:
Az eredeti név (n1) már nincs , amikor a Where fut. A Where bemenete csak a nagybetűs változatot tartalmazza, szóval nem tud az n1 alapján szűrni.
A progresszíven épített lekérdezést egy kifejezésbe is besűríthetjük az egyik lekérdezés másikba csomagolásával. Általában véve:
átírható:
A burkolás szemantikusan ugyanaz, mint a progresszív lekérdezés vagy az into kulcsszavas használat (átmeneti változó nélkül). A végeredmény minden esetben lekérdező operátorok lánca. Pl. nézzük a következő lekérdezést:
Csomagolt formába átírva:
Amikor lambda szintaxisá konvertálódik, az eredmény továbbra is operátorok láncolata, mint az előző példában:
A csomagolt lekérdezések megtévesztőek lehetnek, mert hasonlítanak az allekérdezésekhez. Mindkettő közös koncepciója, hogy van egy külső illetve egy belső lekérdezés. Lambda szintaxisra konvártálásnál látható, hogy a csomagolás csak egy stratégia az operátorok láncolásához. Az eredmény nem hasonlít egy allekérdezéshez, amely eredetilegi magában foglal egy belső lekérdezést egy másik lambda kifejezésen belül.
Eddig a select kifejezések skaláris típust képeztek le. C# objektum inicializálókkal sokkal összetettebb típusokba képezhetünk le. Pl. tételezzük fel, hogy a lekérdezés első részében a magánhangzókat akarjuk kivágni egy névlistából, emellett megtartjuk a régi listát is. Segítségképpen írjuk meg az alábbi osztályt:
és ezután képezzük ebbe az objektum inicializátorral:
Az eredmény IEnumerable
Ugyanazt az eredményt kapjuk, mint az előbbi esetben, de nem kell megírni egy felesleges osztályt. A fordító elvégzi a munkát úgy, hogy egy ideiglenes osztályt ír ugyanolyan stuktúrával, mint a leképezés. Ez azt jelenti, hogy az átmeneti leképezés az alábbi típusú lesz:
Csak a var kulcsszóval tudunk létrehozni ilyen típusú változót. Esetünkben a var több mint zsúfoltságcsökkentés, ez szükséges.
Megírhatjuk a lekérdezést még tömörebben az into kulcsszóval:
A query comprehension szintaxissal egyszerűbben tudunk ilyen típusú lekérdezést írni: let kulcsszóval.
A let egy új változót vezet be az iterációs változó mellett.
a let-tel a magánhangzómentes, legalább kettő hosszú stringeket így irhatuk meg:
A fordító a let-et egy névtelen típusba képzi le, amely tartalmazza az iterációs változót és ezt az új változót is. Más szóval az előbbi példába transzformálja a fordító a lekérdezést.
A let a következő kettőt teljesíti:
Bármennyi let-et lehet használni a where kifejezés előtt. A let kifejezés hivatkozhat egy korábbi let-ben definiált változóra. A let újraképezi a már létező változókat láthatatlanul.
A LINQ-ben két párhuzamos architektúra van: lokális lekérdezések lokális objektum gyűjtemények számára illetve interpretált lekérdezések távoli data source-ok számára. Eddig a lokális résszel foglalkoztunk, ami az IEnumerable<> interfészt implementáló objektumokról szólt. A delegáltak, amiket elfogadtak (legyen az comprehension sztinaxis, lambda szintaxis, vagy hagyományos delegáltak) teljesen lokális IL kód volt, mint bármely másik C# metódus.
Ezzel ellentétben az interpretált lekérdezések leíró elemek. Olyan sorozatokkal operálnak, melyek az IQueryable<> generikus interfészt implementálják, és az operátorokat a Queryable osztályból veszik, amelyek kifejezésfákat adnak vissza futásidőben interpretálva.
Kétféle implementációja van az IQueryable-nak a .NET Frameworkben:
Tegyük fel, hogy generálunk egy egyszerű Customer táblát az SQL adatbázisunkba, és feltöltjük nevekkel:
E tábla birtokában már irhatunk interpretált LINQ lekérdezést C#-ban, hogy megkapjuk az ’a’-t tartalmazó neveket:
A LINQ 2 SQL a lekérdezést az alábbi SQL-be generálja:
Ezzel az eredménnyel:
Nézzük meg, miként működik az előző lekérdezés.
Először is a fordító a comprehension-ből lambda szintaxisra konvertálja. Úgyanúgy csinálja, mint helyi lekérdezéseknél:
Következő lépésben feloldja a lekérdező operátorok metódusait. Ez az a pont, amiben a lokális és a távoli lekérdezések különböznek (az interpretált lekérdezések a Queryable class-ból kapják a metódusaikat az Enumerable osztály helyett).
Ahhoz, hogy lássuk, meg kell nézni a customers változót, a forrást, ahonnan az egész lekérdezés épít. A customers Table<> típusú, ami az IQueryable<> interfészt implementálja (az IEnumerable<> résztípusa). Ez azt jelenti, hogy a fordítónak megvan a lehetősége a Where befogadására: meghívhatja a kiterjesztett metódust az Enumerable-ből vagy az alábbi kiterjesztett metódust a Queryable-ből:
A fordító a Queryable.Where-t választja, mert szignatúrája jobban egyezik.
Egy lekérdezésben lehet mindkét féle operátor. Tipikus mintái a külső lokális operátorok és a belsők intepretáltak. Más szóval a lokális lekérdezéseket az interpretáltakkal töltjük meg. Ez a minta jól működik a LINQ 2 SQL lekérdezéseknél.
Például, ha egy saját kiterjesztett metódust akarunk írni stringek párosítására:
Ezt a metódust felhasználhatjuk egy kevert LINQ 2 SQL és lokális operátorokból álló lekérdezésben:
Mivel a customers IQueryable<> interfészt implementálja, ezért a Select operátor a Queryable.Select-et oldja fel. Ez egy IQueryable típusú kimenő sorozatot ad vissza. De a Pair operátornak nincs IQueryable<>-t implementáló túlterhelése (csak a kevésbé specifikus IEnumerable<>). Tehát a lokális Pair metódust oldja fel, becsomagolva ezzel egy lokális metódusba a lekérdezést. A Pair IEnumerable-t ad vissza, tehát az OrderBy szintén egy lokális operátorba csomagolódik.
A LINQ 2 SQL oldalon az SQL kifejezés:
A többi munka lokálisan történik.
A célja, hogy egy IQueryable
A LINQ 2 SQL-ben bármilyen osztályt felhasználhatunk adatok reprezentálására, ha megfelelő attribútumokkal szereljük fel őket. Egy egyszerű példa:
A [Table] attibútum a System.Data.Linq.Mapping névtérben található, megmondja a LINQ 2 SQL-nek, hogy egy ilyen típusú objektum reprezentál egy sort az adatbázis táblából. Alapértelmezetten azt feltételezi, hogy a tábla neve megegyezik az osztály nevével. Hogyha nem így akarjuk, megadhatjuk az alábbi módon:
Egy class a [Table] attribútummal fémjelezve egy entitás a LINQ 2 SQL-ben. Hogy használható legyen, a struktúrája közel (vagy teljesen) meg kell egyezzen az adatbázis táblájával.
A [Column] attribútum egy mező vagy property, ami a tábla egy oszlopára mutat. Szintén meg lehet adni eltérő névvel:
A [Column] IsPrimaryKey propertyvel lehet utalni a tábla elsődleges kulcsára, emellett kell az objektum azonosításához, illetve a változott adatok adatbázisba történő visszaírásához.
Publikus mezők definiálása helyett publikus propertyket használunk privát mezőkkel. Ezzel lehet validációs logikát írni a property elérésekbe. Ha ezt az utat választjuk, tájékoztathatjuk a LINQ 2 SQL-t a property elérés felülírására, hogy egyből a privát mezőbe írjuk az adatbázisból:
A Column(Storage=”_name”) mondja meg a LINQ 2 SQL-nek, hogy egyből a _name mezőbe írja (a Name property helyett) amikor feltölti az entitást. LINQ 2 SQL reflection-t használ, tehát a mező lehet privát.
Miután definiáltuk az entitás osztályokat, a lekérdezést egy DataContext objektum példányosítása után a GetTable hívással kezdhetjük meg. A következő példa az előbb definiált Customer osztályt használja fel:
A DataContext objektum két dolgot csinál: Először egy factory-ként működik, ami legenerálja a táblákat, melyekből lekérdezhetünk. Másodszor nyomon követi a változtatásokat az entitásokban, amit vissza lehet majd írni:
Mivel a LINQ 2 SQL entitás osztályoknak követniük kell az alattuk fekvő táblát, ezért automatikusan szeretnénk generálni őket egy meglévő adatbázis sémából. Erre lehetőség van SqlMetal parancssorából illetve a Visual Studio LINQ 2 SQL designeréből. Az entitásokat részleges osztályokként generálja le, így plusz logikát is írhatunk hozzájuk.
Pluszként kapunk egy erősen típusos DataContext osztályt. Ez csak egy leszármaztatott DataContext osztály az összes entitás típus tábláját visszaadó property-kel. Megspórolható vele a GetTable:
vagy egyszerűen:
A LINQ 2 SQL designer automatikusan többszörösíti az azonosítókat, ahol szükséges; a példánkban dataContext.Customers és nem dataContext.Customer.
Az entitásgeneráló eszközök még egy hasznos munkát elvégeznek. Valamennyi összefüggéshez az adatbázisban property-k generálódnak automatikusan az összefüggés mindkét oldalán. Pl. nézzük a customer és purchase táblákat egy a többhöz relációval:
Ha automatikusan generált entitásosztályokat használunk, a lekérdezést így kell megírni:
Továbbá, ha a cust1 és cust2 ugyanarra a customer-re hivatkozik, a c1 és c2 ugyanarra az objektumra mutat, azaz cust1==cust2 igazat ad vissza.
Nézzük az automatikusan generált Purchases property-t a Customer entitáson:
Az EntitySet olyan, mint egy előre definiált lekérdezés, egy beépített Where-rel, ami lekérdezi a hivatkozott entitásokat. Az [Association] attribútum adja a LINQ 2 SQL-nek az információkat a lekérdezés megírásához. Mint bármely másik lekérdezésben, a késleltetett végrehajtás lesz rá érvényes. Ez azt jelenti, hogy kerül végrehajtásra, amíg be nem járjuk a hivatkozott gyűjteményt.
Itt a Purchases.Customer property, a reláció másik oldalán:
Habár a property Customer típusú, a benne foglalt mező (_Customer) EntityRef típusú. Ez a típus késleltetett betöltést implementál, így a kapcsolódó Customer típus nem kerül lekérésre az adatbázisból, amíg mi nem kérjük.
A LINQ 2 SQL lekérdezések késleltetett végrehajtással működnek, ugyanúgy, mint a lokális lekérdezések. Ezért a lekérdezéseket írhatjuk progresszíven is. Azonban van egy aspektus, amiben a LINQ 2 SQL-nek speciális késleltetett végrehajtó szemantikája van, ez az, amikor egy allekérdezés van egy Select-en belül:
Bármilyen EntitySet, melyet explicit leképezünk, egy kör futásával teljesen feltöltődik:
De ha egy EntitySet property-t bejárunk, mielőtt leképezzük, a késleltetett végrehajtás szabályai érvényesülnek. A következő példában, a LINQ 2 SQL végrehajt még egy Purchases lekérdezést mindegyik ciklus iteráción:
Két különböző használata van:
Definiáljuk a ProcessCustomer-t így:
Tegyük fel, hogy a ProcessCustomer-t mindegyik valamennyi vevő vásárolt tárgyainak csak egy részhalmazával akarjuk táplálni. Itt egy megoldás:
Ez így rendezetlen. Még rendetlenebb lesz, ha a ProcessCustomer még több Customer mezőt tartalmaz. Egy sokkal jobb megoldás a DataLoadOptions AssociateWith metódusának használata:
Ez a DataContext példányt arra utasítja, hogy a Customer Purchases-en mindig a megadott feltételt hajtsa végre. Most már használhatjuk az eredeti ProcessCustomer metódust. Az AssociateWith nem változtatja meg a késleltetett végrehajtás szemantikáját. Amikor egy bizonyos összefüggést használunk, azt mondja meg, hogy implicit hozzá kell adni egy bizonyos szűrőt az egyenlőséghez.
Mohó betöltés:
A DataLoadOptions második felhasználási lehetősége egyes EntitySet-ek mohó betöltésének állítása a szülein keresztül. Pl. be akarjuk tölteni az összes vevőt és vásárlásaikat egyetlen SQL körrel. Az alábbi pont ezt teszi:
Amikor egy Customer-t lekérünk, a Purchases-ek is lekérésre kerülnek ugyanabban az időben. Sőt még az unokákat (v. gyerekek gyerekeit) is bevonhatjuk: //szülő-gyerek reláció SQLben
Lehet kombinálni a kettőt. Az alábbi lekérdezésben, amikor lekérünk egy vevőt, a magas árú vásárlásait akarjuk lekérni egy kör futásával:
A LINQ 2 SQL nyilvántartja a frissítéseket, melyeket végrehajtottunk az entitásokon, és lehetővé teszi, hogy visszaírjuk az adatbázisba a SubmitChanges metódussal a DataContext objektumon. A Table<> osztály InsertOnSubmit és DeleteOnSubmit metódusai teszik lehetővé sorok beszúrását illetve törlését a táblából. Itt van egy sor beszúrása:
Később lekérdezhetjük, frissíthetjük, majd kitörölhetjük ezt a sort:
A DataContext.SubmitChanges összegyűjti az összes változtatást, amit végrehajtottunk az entitásainkon a DataContext létrehozása óta (vagy a legutolsó SubmitChanges óta), utána végrehajt egy SQL utasítást az adatbázisba íráshoz. Bármelyik TransactionScope-t elfogadja; ha nem volt egy sem, akkor az utasításokat egy új tranzakcióba csomagolja.
Az Add-dal is hozzáadhatunk új vagy már létező sorokat egy EntitySet-hez. A LINQ 2 SQL automatikusan legenerálja az idegen kulcsokat, amikor ezt használjuk:
Amikor egy sort eltávolítunk egy EntitySet-ből, az idegen kulcs mezője automatikusan null-ra áll. Az alábbi példában szétválasztjuk a két vásárolt tárgyat a vevőjétől:
Mivel ezzel megpróbálja a két vásárolt tárgy CustomerID-jét null-ra állítani, a Purchase.CustomerID-nek nullable-nek kell lennie az adatbázisban, különben hibát kapunk. Ahhoz, hogy a gyerekeket teljesen töröljük, vegyük ki őket a Table<>-ből:
Amikor dinamikusan akartunk lekérdezéseket építeni, azt lekérdező operátorok láncolásával oldottuk meg. Habár ez sok esetben elegendő, néha egy sokkal kifinomultabb szinten kell dolgoznunk, és dinamikusan kell azokat a lambda kifejezéseket kifejezéseket komponálnunk, amivel az operátorokat tápláljuk.
Ebben a fejezetben az alábbi Product osztállyal dolgozunk:
Emlékezzünk vissza:
Amikor beágyazzuk egy lekérdezésbe, a lambda kifejezés ugyanúgy néz ki:
Viszont amikor egy lambda kifejezést egy közbenső változóhoz kötjük, explicit módon meg kell adnunk, hogy egy delegáltat (pl. Func<>) vagy egy
kifejezésfát (pl. Expression
Kifejezésfák fordítása:
Egy kifejezésfát átkonvertálhatunk egy delegálttá a Compile metódus meghívásával. Ez különleges érték, amikor újrafelhasználható kifejezéseket visszaadó metódusokat írunk. Hogy ábrázoljuk, egy statikus metódust adunk a Product osztályhoz, ami egy predikátumot ad vissza, mely igazat ad, ha a termék nem Discontinued és az elmúlt 30 napban adták el:
Ezt a metódust mind interpretált, mind lokális lekérdezéseknél is használhatjuk:
AsQueryable:
Az AsQueryable operátorral teljes lekérdezéseket írhatunk, amit lokális illetve távoli sorozatokon is lefuttathatunk:
Az AsQueryable IQueryable<> "ruhába" csomagol egy lokális lekérdezést így a későbbi lekérdező operátorok kifejezésfákká. Később, amikor bejárjuk az eredményt, a kifejezésfák implicit lefordítódnak, és a lokális sorozat normálisan lesz bejárva.
Már említettük, hogy egy lambda kifejezés Expression
A kifejezés DOM:
A kifejezésfa egy miniatűr kód DOM. Mindegyik csúcs a fában egy System.Linq.Expressions névtérbeli típussal reprezentált.
Az bázis típusa mindegyik csúcsnak az Expression (nem generikus) osztály. A generikus Expression
Az Expression<> alaptípusa a (nemgenerikus) LambdaExpression osztály. A LambdaExpression típusegységesítést nyújt a lambda kifejezésfáknak: bármelyik Expression<> típust LambdaExpression-né lehet kasztolni.
A dolog, ami megkülönböztet egy LambdaExpression-t egy hétköznapi Expression-től, hogy a lambda kifejezéseknek paramétereik vannak.
Ha egy kifejezésfát akarunk létrehozni, ne példányosítsunk csúcstípusokat közvetlenül; ehelyett hívjunk statikus metódusokat hívunk az Expression osztályból.
Építsünk egy kifejezést a semmiből. Az alapelv szerint a fa legaljáról indulunk és haladunk felfele. A legalsó elem a fában egy ParameterExpression, egy lambda kifejezés paramétere, melyet ’s’-nek hívunk:
A következő lépés, egy MemberExpression és ConstantExpression építése. Az előző esetben definiált ’s’-nek a Length property-jét akarjuk elérni:
Következő lépés a LessThan összehasonlítás:
Az utolsó lépés a lambda kifejezés szerkesztése, ami egy kifejezés Body-ját a paraméterek gyűjteményéhez kapcsolja:
Lambda kifejezés kényelmes delegálttá fordításának módja