Tag Archives: LINQ

DLINQ: Egyszerű tárolt eljárások esete az MSDN-nel

Egyszerűnek tekinthetjük az alábbi, elég tipikusnak mondható tárolt eljárást, ami visszaadja egy adott tábla rekordjainak számát:

    CREATE PROCEDURE dbo.GetCustomerCount
    AS
        SELECT Count(*) FROM Customers

Amikor megpróbáljuk meghívni, ugyanúgy járunk el, mint máskor, rádobjuk a tárolt eljárást a Visual Studio O/R Designerére, ami generál nekünk egy éppen megfelelő metódust. Szinte eláll a szívünk, amikor meglátjuk az így előállt C# metódust és már hívjuk is lelkesen, hiszen int a visszatérési érték:

    NorthwindDataContext dc = new NorthwindDataContext();
    int i = dc.GetCustomerCount();
    Console.WriteLine( i );

Bár a tárolt eljárás szinte művészien tökéletes, szomorúan konstatáljuk, hogy bizony mindig nullát ad vissza. Nézzük meg a generált metódus belsejét:

    [StoredProcedure( Name = "dbo.GetCustomerCount" )]
    public int GetCustomerCount()
    {
        IExecuteResults result = this.ExecuteMethodCall(
            this,
            (MethodInfo) MethodInfo.GetCurrentMethod() );
        return ( (int) ( result.ReturnValue ) );
    }

A kérdés – amire az MSDN-ből aligha kapunk választ a mostani béta változatban -, hogy mi az az IExecuteResults és mit tud a ReturnValue tulajdonsága? Némi Reflectorozással odáig sikerült eljutnom, hogy a DataContext.ExecuteMethodCall meghívja az aktuális provider ExecuteNonQuery metódusát, de mivel az is interfész alapú (IProvider), így az implementációval először nem sokáig jutottam. (Ezen a ponton arra tippeltem, hogy ez az ExecuteNonQuery ugyanúgy viselkedik, mint az IDbCommand esetén és az "érintett sorok" – rows affected – számát adja vissza. Mint kiderült nem egészen…)

Aztán tovább ásva megtaláltam a System.Data.Linq.SqlClient.SqlProvider osztályt, ahol az ExecuteNonQuery egy SqlExecuteResults típussal tér vissza, ami implementálja az IExecuteResults interfészt. Már csak az maradt kérdés, hogy hogyan kap értéket a ReturnValue tulajdonság? A konstruktorból kiderül, hogy a @RETURN_VALUE paramétertől.

Na de ki beszél itt SQL paraméterről, hiszen ennek a tárolt eljárásnak nincs se input, se output paramétere! Nosza kérdezzük meg, mi megy az adatbázisba, amihez használhatjuk a DataContext.Log tulajdonságát. Az eredmény:

    EXEC @RETURN_VALUE = [dbo].[GetCustomerCount]
    -- @RETURN_VALUE: Output Int (Size = 0; Prec = 0; Scale = 0) NOT NULL []

Hűha! Irány az SQL Server Books Online, hányféleképpen lehet beállítani egy tárolt eljárás visszatérési értékét? Nekem SELECT-tel nem sikerült, maradt a RETURN, amihez át kellett írni a tárolt eljárást, például így:

    DECLARE @Result int
    SELECT @Result = Count(*) FROM Customers
    RETURN @Result

Ez engem zavar, ezért kedves SQL guruk, mondjatok valami egyszerűbbet!

Ami pedig az MSDN-t illeti, egyetlen rövid mondat rengeteg időt megspórolhatott volna. Persze, tudom, béta termék, meg ez csak egy metódus a sok közül, de akkor is. Gondoltam egyet és megkeresetem az ExecuteMethodCall oldalát az MSDN Wikiben: abban reménykedtem, hogy ha ide megírom a felfedezéseimet, az majd segít másoknak és valamilyen formában bekerül az RTM dokumentációba. Nos, erről lepattantam, egyrészt mert úgy látszik a béta dokumentációhoz nem lehet hozzáírni, másrészt pedig ennek a metódusnak a leírása angolul nem található meg, csak japánul smile_sniff Ez remélem átmeneti bug és nem feature smile_sad

 

Technorati tags:

DLINQ: Tárolt eljárások visszatérési típusai (VS trükk)

DLINQ-ből tárolt eljárásokat hívni pofon egyszerű: ráncigálni kell egy kicsit a VS dizájnert, míg ki nem pottyan egy metódus, aminek a szintaktikája éppen a tárolt eljáráséval egyezik meg. Az viszont nagyon nem mindegy, hogy a dizájnerben hogyan kattintgatunk, és mivel a jelenlegi bétában nem sok visszajelzést kapunk, íme egy trükk!

Példaként használjuk a Northwind adatbázist, és akarjuk meghívni a következő nagy bonyolultságú tárolt eljárást:

    CREATE PROCEDURE dbo.GetCustomersInCity
    (
        @City nvarchar( 15 )
    )
    AS
        SELECT * 
        FROM Customers
        WHERE City = @City

Van tehát egy string bemenő paraméterünk, válaszként pedig a Customers tábla rekordjait adjuk vissza. Ennek burkolására úgy készíthetünk DLINQ-es metódust, hogy a Visual Studio "Orcas" O/R Designerében dolgozgatunk, ami akkor jelenik meg, ha Linq to SQL File típusú elemet adunk a projekthez. A dizájner mellé nyissuk ki a Server Explorert, majd ragadjuk meg a tárolt eljárásunkat és dobjuk a dizájner jobb felére, mert az a metódusok helye. Ennek eredménye a következő metódus lesz a DataContext osztályunkban (kicsit kipofozva és leegyszerűsítve):

    [StoredProcedure( Name="dbo.GetCustomersInCity" )]
    public IEnumerable<GetCustomersInCity> GetCustomersInCity( 
        [Parameter( Name="@City" )] string City )
    {
        IQueryResults<GetCustomersInCity> result = 
            this.ExecuteMethodCall<GetCustomersInCity>( 
                this, 
                (MethodInfo) MethodInfo.GetCurrentMethod(), 
                City ); 
        return (IEnumerable<GetCustomersInCity>) result;
    }

A problémát a piros aláhúzott típusok jelentik: a VS lelkesen generált egy új osztályt, aminek olyan tulajdonságai vannak, mint amilyen oszlopokkal a tárolt eljárás visszatér és azt elnevezte GetCustomersInCity-nek. Hagyjuk figyelmen kívül egy pillanatra ennek a típusnak a közel sem bájos nevét és koncentráljunk inkább arra, hogy ez nem a Customer típus, hanem egy ugyanolyan szerkezetű, de másik típus! Ugyanez történik akkor is, ha a dizájnerben már van Customer, a VS nem képes rájönni, hogy a kettő szemantikailag azonos.

Lelkesen felcsaphatjuk a Properties ablakot és örülhetünk, amikor meglátjuk, hogy létezik egy Return Type nevű tulajdonság, aminek az értéke éppen (Auto-generated Type), ám az örömünk hamar elillan, amikor rájövünk, hogy ez bizony read-only.

A rejtély kulcsa abban rejlik, hogy nem mindegy, hova ejtjük a Server Explorerből kifogott tárolt eljárást! Előbb tegyük be a Customers táblát a dizájner bal felére, majd utána fogjuk meg a tárolt eljárást és ne a dizájner jobb felére tegyük, hanem a Customers tábla felett engedjük el az egérgombot. Voila, a fenti kódrészletben az aláhúzott generált típus helyett mindenhol Customer fog szerepelni!

Még egy tanulság a fenti kódrészletből: ha azt akarjuk, hogy a metódus paramétereinek casingjére az FxCop ne panaszkodjon, akkor használjunk a tárolt eljárásokban kisbetűs paraméter neveket, különben kénytelen leszünk módosítani a Parameter attribútum Name tulajdonságát.

 

Technorati tags:

Dlinq alapok és SqlMetal, a Dlinq rabszolga

A márciusi Orcas CTP-t elindítva eleinte nehézkesnek tűnik a LINQ-kel történő ismerkedés, mert a 2006. májusi előzetestől eltérően itt már nincs külön LINQ projekt típus a Visual Studioban. 

Íme a szükséges első lépések:

  1. Hozzunk létre egy új konzol alkalmazást, például LinqApp néven.
  2. Referenciaként adjuk a projekthez a System.Data.Linq.dll-t, amit a C:WINDOWSMicrosoft.NETFrameworkv3.5.20209 mappában fogunk megtalálni. Érdekes módon a VS az Add Reference ablak .NET fülén nem sorolja fel, ezért használjuk a Browse fület és keressük meg a DLL-t.
  3. Júzingoljuk a System.Data.Linq névteret.

Miután ezzel így megvagyunk, bele is vethetjük magunkat a DLINQ-be. Ehhez először is adatbázisra lesz szükségünk, legyen a jól ismert Northwind, ami innen tölthető le.

Ahhoz, hogy a Northwindben lévő partner adatokat elérhessük, készítsünk egy Customer osztályt, ami C# szinten definiál egy partnert. A partnernek legyen ContactName, ContactTitle és City tulajdonsága, ezeket ugyanis tartalmazza az adatbázisban lévő Customers tábla.

Fontos, hogy az osztályunkat fel kell címkéznünk attribútumokkal, melyek azt jelzik, hogy ez az osztály melyik táblához tartozik és az egyes tulajdonságok a tábla mely oszlopához tartoznak. Ehhez a Table és Column attribútumokat használhatjuk, melyek a System.Data.Linq névtérben találhatóak. (Az Orcas VPC-ben lévő MSDN-ben megtalálhatjuk az osztályok összes tulajdonságát, itt csak a legegyszerűbbeket fogjuk használni.)

Ha mindent jól csináltunk, az osztály végül valahogy így fest, kihasználva az automatikus property gyártás új szintaxisát:

    using System;
    using System.Data.Linq;

    namespace LinqApp
    {
        [Table( Name = "Customers" )]
        class Customer
        {
            [Column]
            public string ContactName { get; set; }

            [Column]
            public string ContactTitle { get; set; }

            [Column]
            public string City { get; set; }
        }
    }

Definiáltuk a "sémát", már csak adatokkal kellene feltöltenünk. Ehhez persze kell egy connection string:

    string connStr = @"Data Source=.;Initial Catalog=Northwind;Integrated Security=True;";

Az objektumtér és az adatbázis között a kapcsolatot egy DataContext példány teremti meg, amely hasonló az ADO.NET Connection objektumához, ez ismeri például a connection stringet:

    DataContext dc = new DataContext( connStr );

A DataContext ojjektumtól kérhetjük el a táblákat is:

    Table<Customer> customers = dc.GetTable<Customer>();

A táblákon pedig lekérdezéseket definiálhatunk:

    var query = from c in customers        
                orderby c.ContactName
                where c.City == "London"
                select c;

A lekérdezés eredményét pedig feldolgozhatjuk, például így:

    foreach( Customer c in query )
    {
        Console.WriteLine( "{0}t{1}", c.ContactName, c.ContactTitle );
    }

Előfordulhat, hogy fordítás közben a következő hibaüzenetet kapjuk:

‘System.Data.Linq.Table<LinqApp.Customer>’ does not contain a definition for ‘OrderBy’ and no extension method ‘OrderBy’ accepting a first argument of type ‘System.Data.Linq.Table<LinqApp.Customer>’ could be found (are you missing a using directive or an assembly reference?)

A megoldás:

    using System.Linq;

Ezzel készen is van az első DLINQ alkalmazásunk!

 

Néhány dolgot mindenképp érteni kell:

1. Mi az a var, oda a típusosság?

Szó sincs róla! A var kulcsszó annyit jelent, hogy nem mondom meg a típust, kedves fordító, találd ki a jobb oldalról. A fordító tehát olyan típusúvá fogja tenni query változót, amilyennek azt a lekérdezés meghatározza. A lekérdezések egyébként System.Linq.IQueryable típusúak, ami lényegében egy felturbózott IEnumerable, ezért lehet forícselni.

2. Mikor fut a lekérdezés?

A LINQ egyik alapelve az úgynevezett deferred execution, azaz késleltetett lekérdezés, melynek lényege, hogy a lekérdezések nem akkor futnak, amikor definiáljuk őket, hanem amikor az eredményüket fel akarjuk dolgozni. Mivel a lekérdezések IEnumerable származékok, valójában akkor futnak, amikor végigiterálunk rajtuk.

Ezt egyébként a VS is tudja! Tessék betenni egy breakpointot a var és a foreach közé és megnézni a query változót az Autos ablakban! Az eredmények kibontása előtt ott a figyelmeztetés:

Expanding the Results View will enumerate the IEnumerable.

És valóban, ha SQL Profilerrel megnézzük, valóban csak akkor kap kérést az adatbázis szerver.

3. Milyen SQL utasításokra fordul a lekérdezés?

Ha nem akarunk SQL Profilerezni, akkor legegyszerűbben úgy tudhatjuk meg, hogy mit kell az adatbázisnak megennie, ha használjuk a DataContext Log tulajdonságát, aminek bármilyen TextWritert átadhatunk:

    dc.Log = Console.Out;

Jelen esetben ezt kapja meg az SQL Server:

    SELECT [t0].[ContactName], [t0].[ContactTitle], [t0].[City]
    FROM [Customers] AS [t0]
    WHERE [t0].[City] = @p0
ORDER BY [t0].[ContactName] -- @p0: Input NVarChar (Size = 6; Prec = 0; Scale = 0) NOT NULL [London]

Az IQueryable belső reprezentációja egy Expression objektum, amely az ún expression tree-t tartalmazza, tehát akár ezt is írhattuk volna (ha esetleg kedvelnénk a lambda expressionöket):

    var query = customers.OrderBy( c => c.ContactName ).Where( c => (c.City == "London") );

4. Tényleg minden táblához kell írnom egy osztályt?

Osztály kell, de nem kell megírni, van ugyanis osztálygyártó rabszolga, úgy hívják: SqlMetal.exe. Ez a kis jószág a C:Program FilesMicrosoft Visual Studio 9.0SDKv3.5Bin mappában található, ami sajnos nincs bent a PATH-ban a március CTP image-ben, így vagy hozzávesszük, vagy nyitunk egy SDK command promptot és belépünk a Bin almappába. Az alkalmazás egyékbént egyetlen 98KB-os exe, amit át is másolhatunk a projekt mappánkba.

Van neki jó sok paramétere, a mi példánkban használhatjuk így:

    SqlMetal.exe /server:. /database:Northwind /code:Northwind.cs /namespace:LinqAppMetal /pluralize

Azt hiszem minden kapcsoló magáért beszél, a gyakorlatban még valószínűleg a /sprocs kapcsolót fogjuk használni, amivel tárolt eljárások hívását is egy csapásra metódus hívásokká burkolhatjuk. A generált fájlban találunk egy DataContextből származó Northwind osztályt, illetve minden egyes táblának egy saját osztályt. Érdemes megnézni, hogy miben más a generált Customer osztály, ahhoz képest, amit mi írtunk!

Az SqlMetal által generált 2599 sornyi forráskódot felhasználva a következőre egyszerűsödik az adatbázis lekérdezésünk:

    Northwind dc = new Northwind( connStr );
    var query = from c in dc.Customers
                orderby c.ContactName
                where c.City == "London"
                select c;

Így már szerintem sokkal barátságosabb a feladat.

Technorati tags: ,

Objektum inicializálás LINQ-ban

Aki unja, hogy VS 2005-ben prop-tab-tabbal hozzon létre tulajdonságokat, biztos örömmel olvassa Scott Guthrie március 8-i blogbejegyzését, amelyben a C# 3.0 automatikus tulajdonsággyáráról szól. Ezentúl nem kell időt töltenünk a semmitmondó privát mezők gépelgetésével, sőt kerülgetnünk sem kell azokat, ilyen egyszerűen készíthetünk egy osztályt három tulajdonsággal (mondom tulajdonsággal és nem mezővel!):


    public class Kollega
    {
        public string Vezeteknev { get; set; }
        public string Keresztnev { get; set; }
        public string Varos { get; set; }
    }

Sőt, ezeket a tulajdonságokat egy kényelmesebb szintaxissal lehet inicializálni, ráadásul nem csak egyesével, de gyűjteményben is:


    List<Kollega> kollegak = new List<Kollega>();

    kollegak.Add( new Kollega { Vezeteknev = "Balássy", Keresztnev = "György", Varos = "Budapest" } );
    kollegak.Add( new Kollega { Vezeteknev = "Dávid", Keresztnev = "Zoltán", Varos = "Budapest" } );
    kollegak.Add( new Kollega { Vezeteknev = "Gincsai", Keresztnev = "Gábor", Varos = "Nyíregyháza" } );
    kollegak.Add( new Kollega { Vezeteknev = "Kereskényi", Keresztnev = "Róbert", Varos = "Nyíregyháza" } );
    kollegak.Add( new Kollega { Vezeteknev = "Virág", Keresztnev = "András", Varos = "Kecskemét" } );

Folytassuk ott, ahol Scotty abbahagyta, hozzuk össze ezt a LINQ-kel! Használhatjuk a LINQ-et arra, hogy lekérdezzünk elemeket a fenti gyűjteményből:

    var pestiek = from k in kollegak 
                  where k.Varos == "Budapest" 
                  select k.Vezeteknev;

    foreach( string p in pestiek )
    {
        Console.WriteLine( p );
    }

Ebben az esetben a pestiek nevű változó lényegében egy string gyűjteményt fog tartalmazni. (Ez durva csúsztatás, de nekünk most a lekérdezés visszatérési értéke a lényeg.)

Ha nem egyetlen string értékkel szeretnénk visszatérni, hanem többel, konstruálhatunk belőle egy ojjektumot a selectben:

    var szabolcsiak1 = from k in kollegak 
                       where k.Varos == "Nyíregyháza" 
                       select new { k.Vezeteknev, k.Keresztnev };

Ebben az esetben viszont már nem tudjuk megmondani kódolás közben, milyen típusú lesz a visszatérési érték, tehát gondban vagyunk a foreachnél. Erre találták ki a vart! A C# továbbra is egy erősen típusos nyelv, a var itt nem olyan, mint JavaScriptben, vagy VB-ben a variant, itt a var csak annyit jelent, hogy olyan típust használj kedves fordító, ami az egyenlőségek jobb oldaláról kijön:

    foreach( var s in szabolcsiak1 )
    {
        Console.WriteLine( "{0} {1}", s.Vezeteknev, s.Keresztnev );
    }

Sőt, mivel teljesen új objektumokat hozunk létre, aminek a típusát a select utáni rész alapján definiálja a fordító, semmi akadálya, hogy adjunk neveket a tulajdonságainak:

    var szabolcsiak2 = from k in kollegak
                       where k.Varos == "Nyíregyháza"
                       orderby k.Vezeteknev
                       select new { Csaladnev = k.Vezeteknev, Utonev = k.Keresztnev };

    foreach( var s in szabolcsiak2 )
    {
        Console.WriteLine( "{0} {1}", s.Csaladnev, s.Utonev );
    }

Íme, micsoda fegyvert ad a kezünkbe az object initializer szintaktika és a LINQ összeházasítása!

A korábbiakkal ellentétben ez a kód nem fut a 2006. májusi LINQ preview-val, de a 2007. márciusi Orcas CTP-vel megy remekül, ott lehet kísérletezni.

 

Technorati tags: ,