Soci (Soczó Zsolt) szakmai blogja

2015.03.18.

EF őrület

Filed under: .NET,Adatbázisok,ADO.NET,Entity Framework,NHibernate,Szakmai élet — Soczó Zsolt @ 20:23

Ha az NHibernate tudja, hogy identity id generálási stratégia mellett, ha egy ojjektumnak nem 0 az idja, akkor az perzisztens, így nem kell újra beszúrni, az miért nem megy az EF-nek?

Próbálom használni az EF-et anélkül, hogy lemappelném az FK-kat property-ként, de látom széllel szemben megyek. Nincs cascade szabályozás sem, így ez úton nem mondhatom neki, hogy csak a gyereket szúrd be, a szülőt ne, mert az már perzisztens. Ah. Értem én, hogy nem ebbe az irányba kell haladnom, mert szembe fúj a szél, de nem szeretem azokat a modelleket, ahol az fk le van mappelve. Leaky abstraction. Az EF inkontinens, ez kell neki, a fene a hugyos józsiját.

Védje meg valaki, vagy mindjárt visszatérek NHibre, és kivágom a projektből az EF-et.

EF gyerek kollekció rendezés

Filed under: .NET,Adatbázisok,C#,Entity Framework,SQL Server,Szakmai élet — Soczó Zsolt @ 18:03

Néha szeretnénk nem csak egy entitás listát, hanem annak gyerekeit is rendeztetni, azaz az egy szülő alá tartozó gyerekeket order by-olni.

Egy lehetséges megoldás:

using (var c = new EdbContext())
{
    var jobs = c.LoaderJobs
        .Include("LoaderJobSteps")
        .Include("LoaderJobSteps.ExtractPathLocation")
        .Include("LoaderJobSteps.FormatFilePathLocation")
        .OrderBy(j => j.ExecutionOrder).ToList();

    //A bit complicated to be able to order LoaderJobSteps properly
    var x = jobs.Select(job => new { J = job, JS = job.LoaderJobSteps.OrderBy(js => js.ExecutionOrder).ToList() });

    return x.Select(f => f.J).ToList();
}

Szándékaim szerint a gyerekkollekciók order by-át is az adatbázissal végeztettem volna, de jól látható a kódból, hogy itt .netből történik a gyerekek (LoaderJobSteps) rendezése.
Ha kiveszem az első ToList()-et, akkor az EF helyesen áttolja a 2. order by-t is az SQL Serverre, de akkor meg nem tölti be az unokákat (LoaderJobSteps.ExtractPathLocation).
Ebben a példában nincs jelentősége hol rendezek, de ha valaki tudja, mitől nem megy ilyenkor az Include, érdekelne a megoldás.

Ha benn van az első ToList(), akkor az SQL ok, benne van minden eagerly loaded entitás, de nincs benne order by a gyerekekre, az a LINQ2Objects fogja végrehajtani. (Extent2-re nincs order by).

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Description] AS [Description], 
    [Project1].[ExecutionOrder] AS [ExecutionOrder], 
    [Project1].[C1] AS [C1], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[StepName] AS [StepName], 
    [Project1].[ExecutionOrder1] AS [ExecutionOrder1], 
    [Project1].[ProcedureNamePrepare] AS [ProcedureNamePrepare], 
    [Project1].[ProcedureNameImport] AS [ProcedureNameImport], 
    [Project1].[ProcedureNameLoad] AS [ProcedureNameLoad], 
    [Project1].[ExtractFileName] AS [ExtractFileName], 
    [Project1].[FailOnMissingFile] AS [FailOnMissingFile], 
    [Project1].[FormatFileName] AS [FormatFileName], 
    [Project1].[Id2] AS [Id2], 
    [Project1].[FolderPath] AS [FolderPath], 
    [Project1].[Id3] AS [Id3], 
    [Project1].[FolderPath1] AS [FolderPath1], 
    [Project1].[LoaderJobId] AS [LoaderJobId]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Description] AS [Description], 
        [Extent1].[ExecutionOrder] AS [ExecutionOrder], 
        [Join2].[Id1] AS [Id1], 
        [Join2].[StepName] AS [StepName], 
        [Join2].[ExecutionOrder] AS [ExecutionOrder1], 
        [Join2].[ProcedureNamePrepare] AS [ProcedureNamePrepare], 
        [Join2].[ProcedureNameImport] AS [ProcedureNameImport], 
        [Join2].[ProcedureNameLoad] AS [ProcedureNameLoad], 
        [Join2].[ExtractFileName] AS [ExtractFileName], 
        [Join2].[FailOnMissingFile] AS [FailOnMissingFile], 
        [Join2].[FormatFileName] AS [FormatFileName], 
        [Join2].[LoaderJobId] AS [LoaderJobId], 
        [Join2].[Id2] AS [Id2], 
        [Join2].[FolderPath1] AS [FolderPath], 
        [Join2].[Id3] AS [Id3], 
        [Join2].[FolderPath2] AS [FolderPath1], 
        CASE WHEN ([Join2].[Id1] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM  [dbo].[LoaderJob] AS [Extent1]
        LEFT OUTER JOIN  (SELECT [Extent2].[Id] AS [Id1], [Extent2].[StepName] AS [StepName], [Extent2].[ExecutionOrder] AS [ExecutionOrder], [Extent2].[ProcedureNamePrepare] AS [ProcedureNamePrepare], [Extent2].[ProcedureNameImport] AS [ProcedureNameImport], [Extent2].[ProcedureNameLoad] AS [ProcedureNameLoad], [Extent2].[ExtractFileName] AS [ExtractFileName], [Extent2].[FailOnMissingFile] AS [FailOnMissingFile], [Extent2].[FormatFileName] AS [FormatFileName], [Extent2].[LoaderJobId] AS [LoaderJobId], [Extent3].[Id] AS [Id2], [Extent3].[FolderPath] AS [FolderPath1], [Extent4].[Id] AS [Id3], [Extent4].[FolderPath] AS [FolderPath2]
            FROM   [dbo].[LoaderJobStep] AS [Extent2]
            LEFT OUTER JOIN [dbo].[PathLocation] AS [Extent3] ON [Extent2].[ExtractPathLocationId] = [Extent3].[Id]
            LEFT OUTER JOIN [dbo].[PathLocation] AS [Extent4] ON [Extent2].[FormatFilePathLocationId] = [Extent4].[Id] ) AS [Join2] ON [Extent1].[Id] = [Join2].[LoaderJobId]
    )  AS [Project1]
    ORDER BY [Project1].[ExecutionOrder] ASC, [Project1].[Id] ASC, [Project1].[C1] ASC

Az első ToList() nélkül:

SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Description] AS [Description], 
    [Extent1].[ExecutionOrder] AS [ExecutionOrder]
    FROM [dbo].[LoaderJob] AS [Extent1]
    ORDER BY [Extent1].[ExecutionOrder] ASC

Hm. Tippek?

Közben rájöttem, megválaszolom magamnak a kérdést. :) A végére kell rakni az include-okat:

using (var c = new EdbContext())
{
var jobs = c.LoaderJobs
.OrderBy(j => j.ExecutionOrder);

//A bit complicated to be able to order LoaderJobSteps properly
var x = jobs.Select(job => new { J = job, JS = job.LoaderJobSteps.OrderBy(js => js.ExecutionOrder) });

return x.Select(f => f.J).Include(“LoaderJobSteps”)
.Include(“LoaderJobSteps.ExtractPathLocation”)
.Include(“LoaderJobSteps.FormatFilePathLocation”).ToList();
}
[/source]

SELECT 
    [Project1].[Id] AS [Id], 
    [Project1].[Description] AS [Description], 
    [Project1].[ExecutionOrder] AS [ExecutionOrder], 
    [Project1].[C1] AS [C1], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[StepName] AS [StepName], 
    [Project1].[ExecutionOrder1] AS [ExecutionOrder1], 
    [Project1].[ProcedureNamePrepare] AS [ProcedureNamePrepare], 
    [Project1].[ProcedureNameImport] AS [ProcedureNameImport], 
    [Project1].[ProcedureNameLoad] AS [ProcedureNameLoad], 
    [Project1].[ExtractFileName] AS [ExtractFileName], 
    [Project1].[FailOnMissingFile] AS [FailOnMissingFile], 
    [Project1].[FormatFileName] AS [FormatFileName], 
    [Project1].[Id2] AS [Id2], 
    [Project1].[FolderPath] AS [FolderPath], 
    [Project1].[Id3] AS [Id3], 
    [Project1].[FolderPath1] AS [FolderPath1], 
    [Project1].[LoaderJobId] AS [LoaderJobId]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Description] AS [Description], 
        [Extent1].[ExecutionOrder] AS [ExecutionOrder], 
        [Join2].[Id1] AS [Id1], 
        [Join2].[StepName] AS [StepName], 
        [Join2].[ExecutionOrder] AS [ExecutionOrder1], 
        [Join2].[ProcedureNamePrepare] AS [ProcedureNamePrepare], 
        [Join2].[ProcedureNameImport] AS [ProcedureNameImport], 
        [Join2].[ProcedureNameLoad] AS [ProcedureNameLoad], 
        [Join2].[ExtractFileName] AS [ExtractFileName], 
        [Join2].[FailOnMissingFile] AS [FailOnMissingFile], 
        [Join2].[FormatFileName] AS [FormatFileName], 
        [Join2].[LoaderJobId] AS [LoaderJobId], 
        [Join2].[Id2] AS [Id2], 
        [Join2].[FolderPath1] AS [FolderPath], 
        [Join2].[Id3] AS [Id3], 
        [Join2].[FolderPath2] AS [FolderPath1], 
        CASE WHEN ([Join2].[Id1] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM  [dbo].[LoaderJob] AS [Extent1]
        LEFT OUTER JOIN  (SELECT [Extent2].[Id] AS [Id1], [Extent2].[StepName] AS [StepName], [Extent2].[ExecutionOrder] AS [ExecutionOrder], [Extent2].[ProcedureNamePrepare] AS [ProcedureNamePrepare], [Extent2].[ProcedureNameImport] AS [ProcedureNameImport], [Extent2].[ProcedureNameLoad] AS [ProcedureNameLoad], [Extent2].[ExtractFileName] AS [ExtractFileName], [Extent2].[FailOnMissingFile] AS [FailOnMissingFile], [Extent2].[FormatFileName] AS [FormatFileName], [Extent2].[LoaderJobId] AS [LoaderJobId], [Extent3].[Id] AS [Id2], [Extent3].[FolderPath] AS [FolderPath1], [Extent4].[Id] AS [Id3], [Extent4].[FolderPath] AS [FolderPath2]
            FROM   [dbo].[LoaderJobStep] AS [Extent2]
            LEFT OUTER JOIN [dbo].[PathLocation] AS [Extent3] ON [Extent2].[ExtractPathLocationId] = [Extent3].[Id]
            LEFT OUTER JOIN [dbo].[PathLocation] AS [Extent4] ON [Extent2].[FormatFilePathLocationId] = [Extent4].[Id] ) AS [Join2] ON [Extent1].[Id] = [Join2].[LoaderJobId]
    )  AS [Project1]
    ORDER BY [Project1].[ExecutionOrder] ASC, [Project1].[Id] ASC, [Project1].[C1] ASC

Hm, mégse jó, nincs benne az sql-ben a 2. order by. Szóval a kérdés nyitott, át lehet tolni adatbázis oldalra a child kollekció order by-t?

2015.03.06.

Entity Framework bulk műveletek

Filed under: .NET,Adatbázisok,ADO.NET,Entity Framework,NHibernate,Szakmai élet — Soczó Zsolt @ 11:56

Amint ezt beépítik az EF-be, attól a pillanattól kezdve az EF végre rendes versenytársa lesz az NHibernate-nek.

Ráadásul, a doski azt sejteti, hogy menni fog identity oszlopokra is, az nem megy nhibbel.

Izgalmas fejlemények, meg kéne venni az egész motyót az msnek, de gyorsan.

2015.01.15.

EF WithRequiredDependent – mit csinál ez?

Filed under: .NET,Entity Framework,Szakmai élet — Soczó Zsolt @ 15:10

Nem jövök rá, pontosan mit is csinál ez? A HasRequired már önmagában megcsinálja a foreign keyt, mit ad ez még a modellhez?

2014.04.22.

Memória táblák meghajtása OR mapperrel

Az SQL Server 2014-es memóriatáblákat csak akkor lehet a lehető legközvetlenebb módon elérni, ha natív kódra fordított tárolt eljárásokkal érjük el. Az OR mapperek viszont alapban sima, dinamikus SQL-eket generálnak. Ha így akarunk CRUD műveleteket végrehajtani egy memória táblán, akkor az átmegy a szerver interop rétegén, ami jelentősen belassítja azt. Többszörösére. Emiatt -jelen tudásom szerin-, ha OR mapperel akarjuk meghajtani a memória táblákat, akkor saját spket kell írni. Kipróbáltam a dolgot, de falakba ütköztem. Ugyanis az OR mapperek elvárják, hogy pl. egy update után visszaadja a szerver az xxx rows affected information message-et, ebből tudja az OR mapper, hogy sikerült a sor módosítása. Ha nem jön vissza semmi, akkor azt hiszi, hogy az optimista konkurencia ellenőrzés miatt nem sikerült a sor módosítása (azt hiszi valaki közben módosította vagy törölte a sort), így exceptiont dob. Viszont a memória táblás update NEM adott vissza XXX rows affected üzenetet, így az OR mapper exceptiont dob (NHibernate.StaleObjectStateException és ).
A megoldás tehát valószínűleg az lehet, hogy ki kell kapcsolni az OR mapper optimista konkurrencia ellenőrzőjét.
Egyelőre NHibernate-nél nem találtam megoldást rá, legalábbis a Conf ORM-es “code first” mapping módszerrel. EF-en még nem próbáltam.
Mindenesetre kiírtam msékhez a kérdést, ha jön válasz, megírom.

2014.04.18.

Az ORM batching perf vonzata

Az egyik ügyfelemnél letöltenek pár száz sort memóriába Entity Frameworkkel. Ott C# kóddal mindenféle komplex módon kitalálják, melyik entitást hogyan kell módosítani, majd a módosítások eredményét az EF betolja adatbázisba. Bár a szerveren nem volt nagy terhelés, mégis volt amikor 1000 fölött volt a batch request/second.

Miért? Mert az EF nem tudja összenyalábolni a módosításokat, hanem egyesével küldi be őket. Azaz, ha 300 sor módosult, akkor 300 update fog bemenni, ennyi roundtrip lesz. Ez annyira gáz, hogy egyszerűen nem értem az MS-t, miért nincs még batching a 6-os EF-ben.

Mutatok egy példát, mekkora hatása van ennek. Az NHibernate bármennyire is elhanyagolt manapság, de ő szépen tud batchelni.
A következő példában letöltök 20000 sort, módosítom mindet, majd visszamentem a módosításokkal. EF-fel ez 11mp.
NHibernate-tel meg tudom mondani, hány utasítást küldjön be egyszerre. Ettől függően az alábbi számok jönnek ki:

NHib test - BatchSize: 1000000, Duration: 00:00:00.9390957
NHib test - BatchSize: 100000, Duration: 00:00:00.8363326
NHib test - BatchSize: 10000, Duration: 00:00:00.8463299
NHib test - BatchSize: 1000, Duration: 00:00:01.0654756
NHib test - BatchSize: 100, Duration: 00:00:01.3361271
NHib test - BatchSize: 10, Duration: 00:00:02.4312424
NHib test - BatchSize: 1, Duration: 00:00:10.4669159

Elég nagy batch méretnél a 10 mp lemegy 1mp alá!
EF-nél 11, és kész. Cáfoljon meg valaki, hogy rosszul tudom, és az EF is tudja ezt.

2014.04.15.

Entity Framework 6 null kezelés

Ha egy nullos oszlopon szűrünk, akkor előfordulhat, hogy a bemeneti paraméter is null.
where oszlop = @param

Ebben az esetben az SQL Server alapban az ansi nullságot alkalmazza, így a null != nullal, azaz nem jön vissza egy sor se. Ezért EF6-ban az SQL generátor más sqlt generál, az ilyen esetekben egy IS NULL-os ággal megoldja, hogy nullokra is menjen a keresés.
Viszont ennek drámai mellékhatásai lehetnek. Lássunk egy egyszerű példát:

using (var e = new AdventureWorks2012Entities())
{
var matthew = "Matthew";
e.Person.FirstOrDefault(p => p.LastName == matthew);
}

Ez esik ki az EF-ből:

declare @p__linq__0 nvarchar(4000) = N'Matthew'

SELECT TOP (1) 
    [Extent1].[BusinessEntityID] AS [BusinessEntityID], 
    [Extent1].[PersonType] AS [PersonType], 
    [Extent1].[NameStyle] AS [NameStyle], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleName] AS [MiddleName], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[Suffix] AS [Suffix], 
    [Extent1].[EmailPromotion] AS [EmailPromotion], 
    [Extent1].[AdditionalContactInfo] AS [AdditionalContactInfo], 
    [Extent1].[Demographics] AS [Demographics], 
    [Extent1].[rowguid] AS [rowguid], 
    [Extent1].[ModifiedDate] AS [ModifiedDate]
    FROM [Person].[Person] AS [Extent1]
    WHERE (([Extent1].[LastName] = @p__linq__0) 
	AND ( NOT ([Extent1].[LastName] IS NULL OR @p__linq__0 IS NULL))) 
	OR (([Extent1].[LastName] IS NULL) AND (@p__linq__0 IS NULL))

Látható, hogy bool algebrával összehozták, hogy ha a paraméter null és az oszlop is null (is nullal), akkor lejönnek szépen a sorok. Viszont az ilyen query-ket utálja az sql server, nehezen tudja őket optimalizálni. option(recompile) segítene rajta, de ezt meg nem lehet kiadni EF-en keresztül.

Szerencsére vissza lehet állítani a régi kódgenerátort is:

using (var e = new AdventureWorks2012Entities())
{
    e.Configuration.UseDatabaseNullSemantics = true;
    var matthew = "Matthew";
    e.Person.FirstOrDefault(p => p.LastName == matthew);
}

Generált SQL:

	SELECT TOP (1) 
    [Extent1].[BusinessEntityID] AS [BusinessEntityID], 
    [Extent1].[PersonType] AS [PersonType], 
    [Extent1].[NameStyle] AS [NameStyle], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleName] AS [MiddleName], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[Suffix] AS [Suffix], 
    [Extent1].[EmailPromotion] AS [EmailPromotion], 
    [Extent1].[AdditionalContactInfo] AS [AdditionalContactInfo], 
    [Extent1].[Demographics] AS [Demographics], 
    [Extent1].[rowguid] AS [rowguid], 
    [Extent1].[ModifiedDate] AS [ModifiedDate]
    FROM [Person].[Person] AS [Extent1]
    WHERE [Extent1].[LastName] = @p__linq__0

Így már egy teljesen tiszta szűrést kapunk, de ez NEM menne null bemeneti paraméterre. Ha nem is kell, hogy menjen, akkor viszont ez sokkal gyorsabb lehet, mint az első, megfelelő indexek esetén.

A két lekérdezés tervét egymás mellé rakva jól látható a különbség:
EFUseDatabaseNullSemanticsDiff

Nagyobb tábláknál a hatás sokkal radikálisabb lenne, érdemes szem előtt tartani ezt.

2014.01.23.

Azért néha a hibárnát is elküldöm melegebb éghajlatra

Filed under: .NET,Adatbázisok,ADO.NET,Szakmai élet — Soczó Zsolt @ 22:00

Egy Flages Enumban az All nem tartalmaz minden flaget.

Így az all cascade opció nem foglalja magában a delete orphant.

Szerencsére más is megírta.

WTF gyanús.

2012.07.23.

SP vs. OR mapper

Az előző bejegyzésből a kommentek alapján lehet az jött le, hogy én minden dolgot OR mapperrel valósítanék meg. Csudákat, szó sincs erről.

Nézzünk egy példát. Az előző architektúra tervemben (ami prototípus majd framework is lett, nem csak terv) NHibernate és Oracle alapon készült az app. Sok más mellett tetszőleges országban használható emberneveket kellett tudni tárolni, ezért a db terv elég általános lett. Person-PersonName-PersonNamePart, ez a 3 tábla volt a fő váz. PersonNameType, PersonNamePartType, ezek enum jellegű lookup táblák.
Egy lekérdezés célja visszaadni tetszőleges név komponens alapján a beteg nevét. Azaz a PersonNamePart tábla egy oszlopában kellett keresni, ez jól indexelhető, hatékony lekérdezés. Utána jött a mókásabb rész. A PersonNamePartból vissza kellett navigálni a PersonName-re, onnan a Personre, majd a megjelenítés miatt vissza le az összes PersonName-re és onnan tovább a PersonNamePartra.
Ezt a lekérdezést először megírtam P/L SQL-ben, hogy lássam, kb. mit akarok csinálni, ez mennyire hatékony, kell-e hozzá Cluster (nem clustered, ez oracle, ez teljesen más) index az IO csökkentésére, stb.
Amikor a lekérdezéssel elégedett voltam, elkezdtem átírni az NHibernate LINQ providerére. A célom az volt, hogy a query gondolkodásmódját pontosan visszatükrözze a LINQ query, így remélhetőleg a generált sql is hasonlóan hatékony lesz. Sajnos azonban a kísérlet kudarc volt. Az NHibernate LINQ providere egyszerűen nem implementálta amit én akartam (ha jól emlékszem Take(n) után Select). Megnéztem a forrását, ott volt egy nagy büdös throw new NotImplementedException().
Mi maradt? Sp, aminek a kimeneti joinolt sorait az NHibernate alakította vissza entitásokká (ez viszont nagyon ügyes része az NHibnek.)
Azaz a lekérdezés a lehető leghatékonyabb volt, de a végén mégis a domain modellel dolgoztam, ami végül szimpatikus hibrid lett, bár jobb lett volna tisztán LINQ-ban megírni.
Mi lett volna, hogy rendesen megírták volna a LINQ providert? Megnéztem volna a generált SQL-t, és ha az megfelelően hatékony lett volna, akkor maradt volna az.
Ha nincs rá különösebb okom, én szeretem a logikákat C#-ban írni. Az objektumorientáltság miatt jól lehet szervezni az összetettebb kódokat, sokkal jobban, mint a procedurális TSQL-ben.
Lehet memóriában cache-lni dolgokat, SQL Serveren belül nem nagyon. Az appszervereket sokkal könnyebb horizontálisan skálázni mint a DB-t. Magyarul egymás mellé berakni sok azonos szervert, még ha ez marhára nem is triviális, de legalább lehetséges. Emiatt a DB-ről terhelést levenni nem kell félnetek, mindenképpen üdvös tevékenység.
Viszont, ha az adatokhoz közel kell műveleteket végezni, akkor nem fogok feleslegesen átvinni nagyszámú sort az appszerverre. Ha egy gyakran futó kritikus tranzakció hossza fontos, akkor lehet spbe rakom. Ha sok sort érint a művelet nem hozom ki őket a dbből.

Próbálok pár példát konstruálni, illusztrálandó az előbbieket.

1. Be kell olvasni 100 sort egy táblából, lefuttatni valami összetett műveletet rajta, amely kb. 1000 sornyi programkódnak felel meg, majd az eredményt visszaírni egy másik táblába, amely kb. 20 sornyi insertet jelent. OR mapper. A bonyolult logika valószínűleg erősebb érv lesz, mint az, hogy 100 sort át kell vinni a dróton, illetve, hogy 2 roundtrippel jár a művelet. Mivel a logika C#-ban lesz, könnyű lesz teszteket írni rá (a db könnyen kifake-elhető), könnyebb lesz karbantartani, ha módosítani kell.
Ha esetleg az egésznek egy tranzakcióban kell lenni, és a round tripek ideje jelentősen nyújtja a tranzakciók futási idejét, ami miatt az esetleg szigorúbb izolációs szinten futó tranzakciók elkezdenek torlódni akkor, és csakis akkor kezdenék el filózni az spn. De ezt csak méréssel lehet eldönteni, mert az sp esetén meg valószínűleg a logika lesz lassabb, így lehet többet vesztünk, mint nyerünk.

2. Be kell olvasni 5 táblából 1 sort, ezek alapján dönteni, majd visszaírni 2 táblába 1-1 sort. Az alapeset ugye az, hogy megpróbáljuk OR mapperen keresztül megoldani a dolgot, majd belátni, hogy az elég hatékony-e, ha nem akkor áttérünk sp-re. NHibernate vagy EF Extension Pack Future-ökkel az 5 select valószínűleg kiadható 1 roundtrip alatt, ha nincs közöttük függőség. Ha van, akkor lehet, hogy join-nal vagy navigácós relációkon haladva még mindig megoldható 1 roundtrip alatt. Ha nem, akkor nem. A visszaírások normális OR mapper esetén 1 roundtrip alatt végrehajthatók (batching), így 2 roundtrip alatt megvan a feladat. Valószínűleg bőven belefér, marad az OR mapper.

3. Le kell törölni sok sort, ahol a DueDate nem mai dátum. Egyértelműen sp, semmi értelme OR mapperbe behúzni a törlendő sorokat, könnyedén megfogalmazható a feltétel egy sima SQL where-ben. Hozzáteszem, NHibbel lehet delete és update műveleteket is kiadni entitás fogalmak használatával, így annál ezt se spzném, a generált sql az lenne, ami kézzel beírnék az spbe, csak direkt sqlként. Akkor meg minek sp-zzek?

4. Be kell szúrni 500 sort, nagyon gyorsan, naponta sokszor. pl. laborgépek okádják magukból az eredményeket, ezeket kell eltárolni. Ez már érdekesebb kérdés. Első körben azt mondanánk, OR mapper erre nem való. Az első gondolatom az ilyenre valamilyen bulk copy megoldás lenne, pl. SQL Server esetén SqlBulkCopy osztállyal. De pl. Oracle esetén van un. Array Binding, amivel a bulk műveletekkel közel azonos teljesítményt lehet elérni, a bulk műveletek limitációi (nem lehet trigger a táblán, stb.) nélkül. Ami meglepő, hogy a NHibernate oracle esetén kihasználja az array bindingot, és nem csak egyszerűen batcheli a műveleteket, de array bindinggel küldi be. Emiatt veszett gyorsan tud beszúrni, így NHib esetén simán OR mappert használnék erre is. Egy konkrét mérésemben 10000 sor beszúrása így nézett ki Oracle-be (fejből mondom, de a nagyságrendek jól lesznek):
1. NHibernate identity id generator (othodox SQL Serveres gondolkodásmód): 52 sec
2. ADO.NET soronkénti insert: 12 sec
3. NHibernate hilo id generátorral: 2.7 sec
4. ADO.NET Array Bindinggal: 2.2 sec
5. Direct Path Loading: 2.1 sec

Mik a tanulságok ebből a mérésből?
A. Az identity alapú ID generálás megöli az insertek teljesítményét (1. és 2. példa alapján)
B. A sok roundtrip mindenképpen káros, emiatt lett lassú 2. is, mivel itt nem volt OR mapper overhead.
C. Az NHib elképesztően gyors volt (3. példa), csak 20%-kal lassabb volt, mint az Array Binding nyersen használva (2.2/2.7).
D. Az Array Binding eszement gyors, SQL Serveren a tábla típusú paraméterekkel lehet hasonlót elérni, de ehhez nem tudok OR mapper támogatást (mivel ehhez explicit kell fogadó kódot írni, az orához NEM).
E. A bulk copy mindenhol a leggyorsabb, de az oránál ez nagyon limitált, le is van írva, hogy arra gyúrnak, hogy az array binding legyen ugyanolyan gyors, így kiválthatja azt.

Azaz, ha megfelelően optimalizált az OR mapper adatelérése, akkor még olyan esetekben is használható, ahol első körben nem jutna az eszünkbe.

5. Két tábla között kell átmozgatni sorokat, egyszerű leképezések mentén. Sp, insert-select és delete, tranzakcióban.

6. Pessimistic Offline Lock pattern implementációban volt a következő. Meg kell nézni, létezik-e lock egy erőforrásra, ez egy egy szűrt select. Ha nem, akkor beszúrni egyet, ha igen, beszúrni egy várakozó sort. Mindezt serializable izolációs szinten, mivel meg kell akadályozni fantom sorok beszúrást a select és az insert között (orának is van ilyenje, bár nem lockol mint az Sql server alapban, hanem úgy működik, mint az RCSI az SQL servernél).
Érezhetően itt észnél kell lenni, mivel ha sokszor fut le a folyamat, akkor a serializable miatt lassú lehet a dolog. Design szempontból értelemszerűen az offline lockkal védeni kívánt dolgok lekérdezése NEM a serializable tranzakcióban volt. A védendő lekérdezés előbb lefut, majd utána jön a lock fogása, és ha sikerül, megkapja a kliens az eredményt, ha nem, akkor eldobjuk a lekérdezése eredményét. Ez pazarlás, ha ütközés van, de nem számítunk sok ütközésre. Ha egy tranzakcióban lenne a két feladat torlódás lenne, ha előbb a lock, aztán a művelet, akkor meg sikertelen művelet esetén (pl. secu beint) törölni kellene a lockot, de közben már lehet más vár rá…
Ezt a feladatot OR mapperrel írtam meg. Nem tudok pontos végrehajtási időt mondani, de ha jól emlékszem kb. 20 ms volt a teljes lock vizsgálat ideje. Ha serializable szinten egy erőforráson versengenének a folyamatok, akkor másodpercenként 50-nél többet nem tudna a szerver végrehajtani. Valójában azonban szórnak a lockolandó erőforrások, így a serializable nem fog jelentős torlódást okozni (megfelelő csak indexekkel persze).
Sokan szerintem ezt kapásból SP-ben írnák meg. Mit nyernénk vele? Mit akarunk minimalizálni? Az SQL szintű tranzakció idejét. A tranzakció kb. így nézne ki:
begin tran
select …
if () insert…
else másik insert…
commit

A kérdés, ezek ideje hogy aránylik a roundtripek által megnövelt időhöz? 3 roundtrip van, 1 select, 1 valamilyen insert és egy commit. Ha a roundtrip ideje mondjuk 3ms, akkor 9 ms a roundtrip overheadje. Ha az SQL műveletek kb. 10ms-ig tartottak, akkor kétszer olyan gyors lehet az sp-s megoldás. Azaz itt elgondolkodtató a dolog, de megint, én csak konkrét mérések alapján állnék neki átrefactorolni a megoldást spre, ilyen spekulatív úton nem. Hisz ha a roundtrip valójában csak 500 usec, akkor máris 1 napig felesleges dologgal múlattuk az időt.

7. Sok id alapján kell behozni pár száz sort. Lehetsége megoldás OR-mapper future queryvel, egy batchben, de ennek hátránya, hogy pl. SQL Servernél nem lehet 1-2 ezérnél több paramétert definiálni, így a generált sql nem lesz végrehajtható. Itt spt írtunk, amely oracle collection paraméterként vette át a kulcsok listáját, belül pedig joinoltunk az alaptáblához. SQL Servernél ezt tábla típusú paraméterrel csináltam volna meg. A bemenetet elegánsan át lehetett adni, mivel az NHibben lehet saját típust definiálni, így Guid tömb típus direkben átpasszolható volt az spbe.

Más. A már említett architektúrában volt sok olyan cross-cutting concern, amit db szinten implementálni véres lett volna. Például a WCF-ből kijövő objektum gráfot módosítani kellett, mivel sor (entitás) is mező (property) szintű ACL secu miatt meg kellett metszeni a kimenete fát (gráfot, mikor-mit), property-k értékét kimaszkolni, de közben feljegyezni a maszkoltakat, szöveget nyelvi fordítását beinjektálni a gráf tetszőleges helyére, tetszőleges entitást kiegészítő adatokat belerakni a fa megfelelő részére, be kellett küldeni minden tranzakció nyitása után egy user és tran azonosítót, offline lock információt, stb. Ezek nagyon komplex feladatok voltak, amelyeket ráadásul millisecundum nagyságrendű idő alatt kellett tudni megcsinálni tízezer elem feletti gráfokra is. Ezt én dbben nem tudtam volna megcsinálni, intenzív appszerver szintű cache-eléssel, illetve kihasználva a .NET adatstruktúráit sikerrel és elegánsan (központilag implementálva, mint az AOP-ban) megoldottuk.
Ha viszont ilyen mindent keresztülvágó követelményektől hemzseg a feladat, akkor ez ember kétszer is meggondolja, hogy kilépjen az entitások, a logikai modell világából, és elkezdjen spzni, mivel az spkre ráhúzni ezeket a megoldásokat nehézkes.
Szóval az OR mapper vs. sp kérdés igen komplex, nem lehet 2 perc alatt dönteni róla, és mindkettőnek megvan a helye, feladatonként egyesével kell eldönteni, melyiket használjuk.

2012.07.19.

Furcsa deadlockok

Mostanában valahogy utolérnek a code review jellegű munkák, amiket egyébként nagyon szeretek, mivel ezek általában intenzív, agyalós nyomozások szoktak lenni, amit imádok.
Az előzőben deadlockok jelentek meg egy SQL Server alapú rendszerben, nagyobb terhelés esetén. A deadlockokat általában elég egyszerű megfogni, az SQL Server Profiler Deadlock graph tálcán kínálja az összeakadt folyamatokat és erőforrásokat. A legtöbb deadlock vagy conversion deadlock, amelyek repeatable read izolációs szinten végrehajtott select-update vált ki, vagy a más sorrendben elért táblák miatt előálló deadlock.
Esetünkben a selectek ki voltak egészítve with(updlock)-kal, amely pont a konverziós deadlockot hivatott megakadályozni (nem kell a shared lockot átkonvertáltni update lockká, mivel már a select is update lockot használ).
No, de ami most más volt mint szokott, hogy általam eddig nem látott volt a deadlock graph. Megörökítettük az utókornak, íme (shift clickkel kell megnyitni, így külön ablakban nyílik meg, így össze lehet nézni a szöveggel):

Mi a furcsa benne? Középen van a két erőforrás, amely esetünkben ugyanaz a tábla. Ez még ok, de a HoBtId is ugyanaz, azaz ugyanarról a sorról van szó.

Update: ez marhaság. a HobtID csak azt mondja meg, hogy azonos allokációs egység, heap vagy index (btree). De az, hogy ugyanarra a sorra várakoznak igaz, a waitresource=”KEY: 6:72057594959101952 (8194443284a0)” attributumból látszik, de ez nem látszik a grafikus nézeten.

Még ez is rendben van. Nézzük meg, milyen lockok vannak kiosztva! A bal és a jobb oldali processz is update lockot tart az erőforrásokon, amelyek a korábbiak alapján úgy néz ki egy erőforrást reprezentálnak. Na, itt van a bibi. Két update lock nem kompatibilis, pont erre találták ki őket, hogy ne szeressék egymást. Akkor hogy tud kiosztani a szerver mégis ilyeneket? Igazából ez a talány, utána már természetes, nem tudnak fogni még egy update és egy exclusive lockot a közös sorokra. Mi a túró történik itt?

Nekem van egy bűnbakom, aki ezt a furcsaságot csinálja, de csak pár nap múlva írom le, amikor már többet tudok róla, hogy a módosítások után megjavult-e a rendszer.

Aki látott már ilyet, kérem ossza meg velünk.

Zárásul csemegének itt van egy nagyobb gráfocska, amikor többen is összeakadnak:

2012.03.31.

NHibernate futures

Filed under: .NET,Adatbázisok,ADO.NET,Entity Framework,NHibernate,Szakmai élet — Soczó Zsolt @ 16:59

Korábban írtam, hogy a Future query-k milyen jók, mivel ha tudjuk, hogy sok lekérdezést egymás után akarunk végrehajtani, akkor össze lehet őket rakni egy batchbe, ezzel igen jelentős gyorsulást lehet elérni. Álljon itt egy konkrét példa:

private IQueryable LoadTradeStatisticsQueryable(ISession session, TaConfig config, TradeDirection dir)
{
return from ts in session.Query()
where ts.TaConfig == config && ts.Dir == dir
select ts;
}

public Dictionary> LoadTradeStatisticsBulk(TradeDirection dir, IEnumerable taSubPortfolios)
{
var futures = new Dictionary>();
using (var session = AtsNHibSessionFactory.Factory.OpenSession())
{
foreach (var taSubPortfolio in taSubPortfolios)
{
futures.Add(taSubPortfolio, LoadTradeStatisticsQueryable(session, taSubPortfolio.TaConfig, dir).ToFuture());
}
foreach (TaSubPortfolio p in futures.Keys.ToList())
{
futures[p] = futures[p].ToList();
}
}

return futures;
}

A trükk egyszerű. A lekérdezéseket nem hajtjuk végre, hanem a Queryable-öket a ToFuture segítségével betárazzuk későbbre. Aztán mikor mind megvan, a ToList()-tel kikényszerítjük az IQueryable-ök végrehajtását. A poén az, hogy ilyenkor az NHib tudja, hogy ezek future query-k, így a 2. ciklus első iterációjakor egyszerre kiadja az összes lekérdezést, így a ciklus 2..n. iterációjában már csak kivesszük az eredményeket.
Az adatbázisba ez sok-sok selectként megy be egy batchben, majd a több resultsetből az NHib szépen materializálja az entitásokat.
Sokszoros sebességnövekedést lehet elérni csak ezzel az egyszerű trükkel. Ez nem csak homogén lekérdezésekre megy, tetszőlegeseket össze lehet rakni így egybe.
EF-hez itt van hasonló kiegészítés.

2012.03.30.

Miért NHibernate?

Jeleztem korábban, hogy mióta megismertem az NHibernate-et, azóta nem nagyon foglalkozok az Entity Frameworkkel. Miért? Pár gondolatot leírok, aztán majd kifejtem őket bővebben is.

  • Sokféle kulcsgenerálást ismer. Az id generálásnak rendkívül nagy hatása van a módosítások felviteli sebességére. Eddig az assigneded és a HILO-t használtam élőben. SQL Serve Identity elfelejtve, considered harmful. Az SQL Server 2012 Sequence + HILO viszont egy jó kombináció lesz.
  • Batch-elt műveletek. Komplex objektum gráfok módosításakor ez is igen sokat jelent, mivel 1 roundtrip alatt lehet sok insert-update-delete műveletet végrehajtani. Ráadásul pl. Oracle esetén kihasználják az Array Bindingot, ami kb. olyan, mint SQL servernél a tábla típusú paraméter, így eszement gyorsan tud DML műveleteket végrehajtani.
  • Rendkívüli kibővíthetőség. Az IInterceptor interfészen keresztül mélyen bele lehet nyúlni a működésébe. Ezt pl. field szintű securityhoz használtuk ki, e nélkül nem nagyon tudtuk volna megcsinálni.
    Vagy pl. a tranzakciók megkezdése után, de még az SQL parancsok végrehajtása előtt be kellett tolni egy a kliensoldali felhasználót reprezentáló információt (audit miatt). Erre is volt kézenfekvő kibővítési pont.
    Vagy oraclenek collection típusú paramétert kellett átadni, ezt is meg tudtuk szépen oldani saját adattípus definiálásával.
  • Direkt DML műveltek. Update vagy Delete anélkül, hogy behúznánk az entitásokat memóriába. Lehet érvelni, hogy nem erre való egy O-R mapper, de ha már egyszer az ember entitásokban és nem táblákban fogalmazza meg a dolgait, egyszerűbb a DML-eknél is ezt használni. Főleg pl. egy öröklődéses esetben, amikor több táblában is kell törölni.
  • Flexibilis materializáló. Joinolt direkt SQL select kimenetét szépen bele lehet passzírozni egy objektumfába, egyszerűen.
  • Saját adattípusok használta. Az Oraclenek nincs GUID típusa, ezért RAW(16)-ként tároltuk a guidokat. Entitás oldalon viszont szeretjük a GUID-ot, ezért egy saját típussal oldottuk meg a leképezést, így mindkét oldal természetes absztrakciókkal dolgozik.
  • Enum mapping. Ha akarom intként menti el, ha akarom stringként. Hogy is van ez EF-nél?
  • Cascade műveletek gyerekekre való használata szabályozható.
  • Szabályozható, hogy csak a megváltozott property-ket updatelje, vagy mindet?
  • Van stateless sessionje (NHib Session kb. EF ObjectContext) nagytömegű adatkezeléshez
  • Tud natívan pl. tömböt adatbázisra mappelni. Azaz sorszáma van az elemeknek, ha kitörlök egy elemet, akkor a felette levő sorszámúakat automatikusan updateli.
  • Vannak current session kontérerei. Ezek pl. WCF szerviz és DI használata esetén nélkülözhetetlenek, így pl. a session élettartama a WCF OperationContexthez láncolható. Ezt annak idején letöltöttem EF-hez, elég bonyolult volt, de ez megy ott is.
  • Future Query-k: ezekkel több lekérdezést egybe lehet fűzni, hogy egy batchben hajtsa végre. EF-hez láttam ilyet a kiegészítő packben.
  • Secondary Cache. Ezt még nem használtam, de érzésre hatalmas potenciál van benne. Cache réteg, így a lekérdezések egy részét memóriából adja vissza.
  • Sokféle adatbázis támogatása.
  • Lazy properties. Betölti az entitást, de nem tölti be pl. a benne levő byte tömböt, ami egy blobra meppelődik a táblában. Csak akkor töltődik be, ha tényleg hivatkozunk a property-re. Ehhez ugyanazt a virtuális proxy megoldást használja, mint az EF.

Hirtelen ennyi jut az eszembe. Egyetlen, de ez nagy negatívum az EF-fel szemben a Linq providere. Az egyszerűen szar, félkész, még most, 2012. márciusában is. Ami kész van, az is elég ostoba, az EF-é viszont okos, tudja, mi a jó az adatbázisnak, így optimalizálja a generált SQL kódot.
Pl. az alábbi NHibernate LINQ-ból: … where !a.IsValid ezt csinálta Oracle alatt:
where not(a.IsValid).
Ez persze table scant okoz orán is. Így kellett átírni: where a.IsValid == false. Ebből lett: where a.IsValid = 0. Ez már seek. De basszus, ezt elvárnám a LINQ providertől!
Nincs a LINQ providerében outer join implementálva! Vagy amikor írtam egy allekérdezéses, Take-es majd select-es nagyobb LINQ kifejezést egyszerűen NotImplementedExceptiont kaptam, és láttam, az egyik NHib fejlesztő megindokolta, hogy ez miért jó így.
Szóval ez a része beteg, párszor LINQ helyett Criteria vagy HQL alapú lekérdezést kellett írnom, ez bosszantó, ront a jó benyomáson.

Tudom, hogy ez a bejegyzés egyesek agyát felhúzza majd. Lesz, aki utána fog nézni, hogy amiket pozitívumként írtam, valójában megvan vagy lesz EF-ben, vagy az EF Extensions packben. Lehet, nem tudok mindent én se fejből.
Nyissunk vitát, nézzük meg, melyik-miben jó, ebből mindenki tanulhat.

2010.04.20.

Entity Framework 4 többrétegű appokban

Filed under: .NET,.NET 4,Adatbázisok,ADO.NET,Entity Framework,Szakmai élet — Soczó Zsolt @ 22:45

Játszottam kicsit az EF4-gyel. Az alábbi kód egy n rétegű app adatmozgását szimulálja a WCF xml szerializálóját használva. Mindhárom template-tel kipróbáltam, alább láthatóak az adatmozgások.
A tesztkód messze nem korrekt, de kiindulópontként további vizsgálatokhoz elfogatható:

using System;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;

namespace POCO1
{
class Program
{
static void Main()
{
Department d;
using (var e = new SchoolEntities())
{
e.ContextOptions.ProxyCreationEnabled = false;
e.ContextOptions.LazyLoadingEnabled = false;
d = e.Departments.Include(“Courses”).Single(dep => dep.DepartmentID == 1);
Console.WriteLine(“{0}”, d.Name);
Console.WriteLine(“———————“);
foreach (Course c in d.Courses)
{
Console.WriteLine(“{0}”, c.Title);
}
e.Detach(d);
}

var ser = new DataContractSerializer(d.GetType());
//var ser = new DataContractSerializer(d.GetType(),
//null, 50000, true, true, null, new ProxyDataContractResolver());

using (var s2c = new FileStream(@”c:\temp\Server2Client.xml”, FileMode.Create, FileAccess.ReadWrite))
{
//1. Server küld kliensre
ser.WriteObject(s2c, d);
s2c.Position = 0;
//2. Kliens deserializál
var clientSideDep = (Department)ser.ReadObject(s2c);

//Csak ST
//bool ce = clientSideDep.ChangeTracker.ChangeTrackingEnabled;

//3. Kliens módosít
clientSideDep.Name += “a”;

using (var c2s = new FileStream(@”c:\temp\Client2Server.xml”, FileMode.Create, FileAccess.ReadWrite))
{
//4. Kliens visszaküld
ser.WriteObject(c2s, clientSideDep);
c2s.Position = 0;

//5.Server deserializál
var sentBackDepartment = (Department)ser.ReadObject(c2s);
using (var e = new SchoolEntities())
{
//6. Server visszamódosít

//Normál entitás
e.Departments.Attach(sentBackDepartment);
e.ObjectStateManager.GetObjectStateEntry(sentBackDepartment).SetModified();
e.ObjectStateManager.GetObjectStateEntry(sentBackDepartment).SetModifiedProperty(“Name”);

//POCO
//e.Departments.Include(“Courses”).Single(dep => dep.DepartmentID == 1);
//e.Departments.ApplyCurrentValues(sentBackDepartment);

//Self-tracking entity
//e.Departments.ApplyChanges(sentBackDepartment);

e.SaveChanges();
}
}
}
}
}
}

EF alapobjektumok szerviz => kliens:

<Department z:Id="i1" xmlns="http://schemas.datacontract.org/2004/07/POCO1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
  <EntityKey z:Id="i2" xmlns="http://schemas.datacontract.org/2004/07/System.Data.Objects.DataClasses" xmlns:a="http://schemas.datacontract.org/2004/07/System.Data">
    <a:EntityContainerName>SchoolEntities</a:EntityContainerName>
    <a:EntityKeyValues>
      <a:EntityKeyMember>
        <a:Key>DepartmentID</a:Key>
        <a:Value i:type="b:int" xmlns:b="http://www.w3.org/2001/XMLSchema">1</a:Value>
      </a:EntityKeyMember>
    </a:EntityKeyValues>
    <a:EntitySetName>Departments</a:EntitySetName>
  </EntityKey>
  <Administrator>2</Administrator>
  <Budget>350000.0000</Budget>
  <Courses/>
  <DepartmentID>1</DepartmentID>
  <Name>Engineering</Name>
  <StartDate>2007-09-01T00:00:00</StartDate>
</Department>

EF alapobjektumok kliens => szerviz:

<Department z:Id="i1" xmlns="http://schemas.datacontract.org/2004/07/POCO1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
  <EntityKey z:Id="i2" xmlns="http://schemas.datacontract.org/2004/07/System.Data.Objects.DataClasses" xmlns:a="http://schemas.datacontract.org/2004/07/System.Data">
    <a:EntityContainerName>SchoolEntities</a:EntityContainerName>
    <a:EntityKeyValues>
      <a:EntityKeyMember>
        <a:Key>DepartmentID</a:Key>
        <a:Value i:type="b:int" xmlns:b="http://www.w3.org/2001/XMLSchema">1</a:Value>
      </a:EntityKeyMember>
    </a:EntityKeyValues>
    <a:EntitySetName>Departments</a:EntitySetName>
  </EntityKey>
  <Administrator>2</Administrator>
  <Budget>350000.0000</Budget>
  <Courses/>
  <DepartmentID>1</DepartmentID>
  <Name>Engineeringa</Name>
  <StartDate>2007-09-01T00:00:00</StartDate>
</Department>

POCO szerviz => kliens:

<Department xmlns="http://schemas.datacontract.org/2004/07/POCO1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <Administrator>2</Administrator>
  <Budget>350000.0000</Budget>
  <Courses/>
  <DepartmentID>1</DepartmentID>
  <Name>Engineeringa</Name>
  <StartDate>2007-09-01T00:00:00</StartDate>
</Department>

POCO kliens => szerviz:


2
350000.0000

1
Engineeringaa
2007-09-01T00:00:00

Self-tracking entity, szerviz => kliens:

<Department z:Id="i1" xmlns="http://schemas.datacontract.org/2004/07/POCO1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
  <Administrator>2</Administrator>
  <Budget>350000.0000</Budget>
  <ChangeTracker z:Id="i2">
    <ExtendedProperties/>
    <ObjectsAddedToCollectionProperties/>
    <ObjectsRemovedFromCollectionProperties/>
    <OriginalValues/>
    <State>Unchanged</State>
  </ChangeTracker>
  <Courses/>
  <DepartmentID>1</DepartmentID>
  <Name>Engineeringaaaaaaaa</Name>
  <StartDate>2007-09-01T00:00:00</StartDate>
</Department>

Self-tracking entity, kliens => szerviz:

<Department z:Id="i1" xmlns="http://schemas.datacontract.org/2004/07/POCO1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
  <Administrator>2</Administrator>
  <Budget>350000.0000</Budget>
  <ChangeTracker z:Id="i2">
    <ExtendedProperties/>
    <ObjectsAddedToCollectionProperties/>
    <ObjectsRemovedFromCollectionProperties/>
    <OriginalValues/>
    <State>Modified</State>
  </ChangeTracker>
  <Courses/>
  <DepartmentID>1</DepartmentID>
  <Name>Engineeringaaaaaaaaa</Name>
  <StartDate>2007-09-01T00:00:00</StartDate>
</Department>

Érdekes, hogy a módosítás ténye csak a ST-ben látszik, még az eredeti entity-sben sem. A POCO-tól nem is vártuk persze.
Később még majd foglalkozok bővebben a témával. Aki játszani akar vele, hozza létre a School EF példaadatbázist, azon lehet futtatni.

2010.02.01.

EF SSDL alapú felhasználói bemenet ellenőrzés

Filed under: .NET,Adatbázisok,ADO.NET,Entity Framework,Szakmai élet — Soczó Zsolt @ 16:23

A következőn töröm a fejem. Az Entity Framework SSDL-jében definiálva vannak az entitás property-k alapvető jellemzői: nullázhatóság, max hossz. Ezeket a GUI-n ki kell kényszeríteni. Nyilván vannak összetettebb validálási szabályok, de most koncentráljunk ezekre az elemiekre.
Utálok minden redundanciát egy rendszerben, ezért azt gondoltam, a szabályokat kiolvasom az EF sémájából, és ebből táplálom meg a validáló részeket, így nem kell törődni az egyszerű validálásokkal, automatikusan működni fognak.

A következő kis kódocska mutatja meg a metaadatok használatát:

o.ForceLoadingSchemas();

var sspaceEntitySets = o.MetadataWorkspace
.GetItems(DataSpace.SSpace)
.First().BaseEntitySets.OfType();

foreach (EntitySet es in sspaceEntitySets)
{
foreach (EdmProperty p in es.ElementType.Properties)
{
ReadOnlyMetadataCollection facets = p.TypeUsage.Facets;
Debug.WriteLine(“{0} is {1} nullable”, p.Name, (bool)facets[“Nullable”].Value ? “” : “not”);
if (facets.Contains(“MaxLength”))
{
Debug.WriteLine(“{0} MaxLenght is {1}”, p.Name, (int)facets[“MaxLength”].Value);
}
Debug.WriteLine(“{0} is {1} nullable”, p.Name, (bool)facets[“Nullable”].Value ? “” : “not”);
}
}

A ForceLoadingSchemas az ObjectContext partial classában van:

public void ForceLoadingSchemas()
{
CreateQuery(“AdventureWorks2008Entities3.BusinessEntities”).ToTraceString();
}

Csinált már valaki ilyet? Van benne valami csapda, amit most nem látok?

2010.01.27.

SQLCLR deplyment hiba

Filed under: .NET,Adatbázisok,ADO.NET,SQL Server 2005,SQL Server 2008,Szakmai élet — Soczó Zsolt @ 10:24

Error: Incorrect syntax near valami.
Akkor jön elő, ha az SQLCLR assemblyt és benne a függvényeket akarja az VS deployolni. Több oka lehet, most az volt, hogy egy .NET oldalon double-t visszaadó függvény véletlenül így lett deklarálva:

[SqlFunction(…, TableDefinition = “Datum datetime, Szazalek double”)]

Mi a hiba benne? SQL Serverben nincs double, csak real és float. Ráadásul a C# float az az SQL real és a C# double az SQL Server float (kb.). :)

Az előbbi helyesen:

[SqlFunction(…, TableDefinition = “Datum datetime, Szazalek float”)]

Miért kellett SQLCLR függvényt írni? A futó aggregálások (én legalábbis nem tudok jobbat kurzor nélkül) o(n2)-es algoritmusok, ezt CLR-ben könnyen meg lehet írni o(n)-re. Pl:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public class UserDefinedFunctions
{
internal class Result
{
public long RowId;
public double CumDollarGain;
public double TopDollarGain;
public double DollarDrawDown;
}

//Input table: create table #trades(RowId long, DollarGain money, other columns possible)
[SqlFunction(FillRowMethodName = “FillRow”, DataAccess = DataAccessKind.Read,
TableDefinition = “RowId bigint, CumDollarGain float, TopDollarGain float, DollarDrawDown float”)]
public static IEnumerable Cumul()
{
using (var conn = new SqlConnection(“Context connection=true”))
{
using (var cmd = new SqlCommand(“select RowID, DollarGain from #trades”, conn))
{
var res = new List();
conn.Open();
double cumulPrice = 0, topPrice = 0, drawDawn = 0;

using (SqlDataReader r = cmd.ExecuteReader())
{
int idCol = r.GetOrdinal(“RowID”);
int gainCol = r.GetOrdinal(“DollarGain”);

while (r.Read())
{
var price = r.GetDouble(gainCol);

cumulPrice += price;
topPrice = Math.Max(price, topPrice);

drawDawn += price;
drawDawn = Math.Min(drawDawn, 0);

res.Add(new Result
{
RowId = r.GetInt64(idCol),
CumDollarGain = cumulPrice,
TopDollarGain = topPrice,
DollarDrawDown = drawDawn
});
}
return res;
}
}
}
}
public static void FillRow(object obj,
out long id,
out double cumDollarGain,
out double topDollarGain,
out double dollarDrawDown)
{
var r = (Result)obj;
id = r.RowId;
cumDollarGain = r.CumDollarGain;
topDollarGain = r.TopDollarGain;
dollarDrawDown = r.DollarDrawDown;
}
};

Sajnos nem lehet átpasszolni a megnyitott SqlDataReadert a két metódus között, ezért kénytelen az ember letárolni az eredményhalmazt. Persze pár ezer sornál ez nem gond.

2010.01.21.

Parallel.ForEach in action

Filed under: Adatbázisok,ADO.NET,SQL Server 2008,Szakmai élet — Soczó Zsolt @ 23:20

Imádom, nagyon ügyes dolog. Csak azért nem hajtotta ki jobban a procikat, mert már nem győzték a diszkek.
Érdekes problémaként még az jött elő, hogy több mint 100 szálat indított a Parallel, mert úgy érezte ez jó lesz, de az Sql Server connection poolja alapban max. 100 kapcsolatot engedélyez, így egyes szálak bedugultak. Lejjebb lehet venni a szálak számát, vagy feljebb a poolt. Nekem 80 szál elég volt, úgyse győzte már az SQL Server az adatokat.

2010.01.19.

Jó Entity Framework architektúra cikkek

Filed under: .NET,ADO.NET,Architektúra,Entity Framework,Szakmai élet — Soczó Zsolt @ 15:33

Hogyan építsünk EF-re többrétegű appot?
1. Anti-Patterns To Avoid In N-Tier Applications
2. N-Tier Application Patterns
3. Building N-Tier Apps with EF4

Jó cikkek.

2009.02.23.

.NET teljesítményhangolási tapasztalatok 6.

A Connection Poolról még pár gondolat. Jó, hogy van ez a pool, meg gyorsít is, örülünk neki, főleg webalkalmazásokban. Olyan appokban viszont, ahol állandóan futnak a dolgaink, mint egy asztali alkalmazás esetén, nem biztos, hogy érdemes mohón nyitni-zárni a kapcsolatot.
A tőzsdei kereskedési algoritmusaim backtestje során sok százezer paraméterkombinációt néz végig a gép, próbálja meghatározni a legvalószínűbb nyerési esélyűt vagy legnagyobb profit/szórással rendelkezőt (ennél jóval bonyolultabb a dolog, de ez most nem tőzsdés bejegyzés lesz).
A DAL-t úgy írtam meg ahogy webalkalmazásokban megszoktam, így minden egyes trade mentésénél nyitottam-zártam a connectiont. Profilerrel megnézve kiderült, hogy a pool ellenére is a futási idő harmada az open/close-zal ment el. Emiatt készítettem kétféle SqlHelper osztályt, az egyik állandóan nyitott kapcsolattal működik, a másik az egyéb helyekre nyit-zár mind eddig.
Összegezve, a connection pool kiváló dolog pillanatokra futó alkalmazásokhoz, mint a webalkalmazások, de nem érdemes erőltetni, ha több százezerszer kell nyitni-zárni a kapcsolatot.

2009.02.12.

.NET teljesítményhangolási tapasztalatok 5.

Szeretjük a connection poolt, de nem mindig.

Tudjuk, hogy manapság nem illik sokáig nyitva tartani az adatbázis kapcsolatot, hanem bemegyünk, kihozzuk a szükséges adatokat, majd lezárjuk a kapcsolatot. Régen (n > 10 év) ez nem volt okos dolog, mert lassú volt a belépés-kilépés. Ezért találták ki a Connection Poolt, amit OLEDB-ben még Session Poolnak hívtak. Egykutya.
Ez arról szól, hogy a kliens oldali adatelérő komponens, pl. az ADO.NET SqlClient providere (System.Data.SqlClient névtér és kölkei) újrahasznosítja a lezárt kapcsolatot. Azaz mi SqlConnection.Close-t mondunk, ő viszont a háttérben nem zárja le a kapcsolatot az adatbázis felé. Ha ezek után újra meg akarjuk nyitni egy kapcsolatot az adatbázis felé, nem hoz létre új kapcsolatot a provider, hanem visszaad egy használtat a poolból. Miért jó ez? Nyilván egy tényleges kapcsolat kiépítése igen költséges, TCP csatorna vagy Named Pipe session felépítése, autentikáció, ecetera. Ezt nagyon sokszor megússzuk, hála a poolnak. A pool 4-8 perc közötti véletlenszerű időközönként azért lezárogatja a használt kapcsolatokat, ott ne zápuljanak.
Nyilvánvaló, hogy felhasználónként egyedi poolok vannak, így egy sysadmin által eldobott kapcsolatot nem adja oda nekem, lópicinek a provider, ez csúnya luk lenne.
Lehet azért így is aljaskodni az új bérlőkkel. Mi van, ha becsatlakozok, kiadok egy use másik adatbázist, majd röhögve lezárom a kapcsolatot? És ha egy-ket set opciót átírok, pl. SET LANGUAGE urdi? Mit lát a következő hívó, aki megkapja a használt kapcsolatot? Hát amit beállítottunk az előző lépésben. Azért ez durván hangzik, ugye? Szerencsére a helyzet az, hogy alapban tiszta környezetet kapunk, köszönhetően annak, hogy a provider pool manager-e végrehajt egy sp_reset_connection hívást mielőtt visszaadná nekünk a használt kapcsolatot. Hogy ez pontosan mit csinál, azt egyszer már leírtam, tessék megnézni a listát.
Azaz bár nem kell minden egyes kéréshez új kapcsolatot megnyitni, azért egy sp hívás csak történik pluszban a háttérben. SQL Profilerben látszik, hogy ez nagyon gyors, de azért ez mégis csak egy hálózati körülfordulás.
És most jön a trükk. Ha teljesen biztosak vagyunk benne, hogy nem tolunk ki a következő hívóval aki megkapja a használt kapcsolatunkat, akkor kikapcsolhatjuk a takarítást. A connection stringbe ezt kell beírni:

Connection Reset=false;

Nézzük meg az alábbi kódot:

SqlConnection conn = new SqlConnection(“data source=.;initial catalog=ATS;Integrated security=true;”);
SqlCommand cmd = new SqlCommand(“sp_who”, conn);
cmd.CommandType = CommandType.StoredProcedure;

for (int i = 0; i < 3; i++) { conn.Open(); cmd.ExecuteNonQuery(); conn.Close(); } [/source] Az SQL Profilerben ez látszik: [source='C'] EventClass TextData SPID Audit Login -- network protocol: LPC RPC:Completed exec sp_who 52 Audit Logout 52 RPC:Completed exec sp_reset_connection 52 Audit Login -- network protocol: LPC RPC:Completed exec sp_who 52 Audit Logout 52 RPC:Completed exec sp_reset_connection 52 Audit Login -- network protocol: LPC RPC:Completed exec sp_who 52 Audit Logout 52 [/source] Hoppá, ez nem az, amit vártunk! Csak 1 logint az elején és 1 logoutot a végén, közben meg az sp_resetconnectiont és az sp_who-nkat. Nem működik a pooling? De. Csibész a profiler. "The Audit Login event class indicates that a user has successfully logged in to Microsoft SQL Server. Events in this class are fired by new connections or by connections that are reused from a connection pool." Ha bekapcsoljuk a profilerben az Event Subclasst, kicsit tisztul a kép: [source='C'] EventClass TextData SPID EventSubClass Audit Login -- network protocol: LPC 1 - Nonpooled RPC:Completed exec sp_who 52 Audit Logout 52 2 - Pooled RPC:Completed exec sp_reset_connection 52 Audit Login -- network protocol: LPC 2 - Pooled RPC:Completed exec sp_who 52 Audit Logout 52 2 - Pooled RPC:Completed exec sp_reset_connection 52 Audit Login -- network protocol: LPC 2 - Pooled RPC:Completed exec sp_who 52 Audit Logout 52 1 - Nonpooled [/source] Szóval van pooling, csak a profiler ravaszkodik. Már kivert a víz. Viszont valami ebben zavar. A pooling ugye a kliensen van. Honnan tudja a szerver egy új parancs beérkeztekor az egy új parancsnak számít a kliens részéről a még nyitott kapcsolaton, vagy csak a poolból visszakapott kapcsolaton hajtanak végre egy új parancsot? Szerintem ő mesterségesen szintetizálja a poolos login-logoutokat, az sp_reset_connection-ök beérkeztekor veszi őket körbe login-logout párossal, a feeling kedvéért. De ez csak tipp. Kapcsoljuk ki a connection resetet: [source='C#'] SqlConnection conn = new SqlConnection( "data source=.;initial catalog=ATS;Integrated security=true;connection reset=false;"); [/source] Mit látunk a profilerben? Semmi nem változott! .NET fw. 2.0 felett nem lehet kikapcsolni az sp_reset_connection-t! Ezért nincs már a legújabb doksiban benne ez a beállítás.

Foly. köv.

2008.10.02.

Logout események a profilerben

Emailben kérdezték tőlem gáz-e, hogy a profilerben a logut eseményeknél jó nagy duration és egyéb értékek vannak.
Röviden: nem.

Hosszabban: a logout események adatai nem túl érdekesek, azok az adott connection által végzett összes munka és várakozás idejét tartalmazzák. Ha lezárod rendesen a kapcsolatokat kliens oldalon, akkor azt várod, hogy a login-logut események rövid időn belül követi egymást, de nem ez történik a
connection pooling miatt. A pooling alapban be van kapcsolva az összes adatelérő komponensben, ezért egy kapcsolat percekig fennáll a kliens és a szerver között, amin sok általad megnyitott kapcsolat pöfögi át a parancsait. Ezért a logoutnál látott érték sok lekérdezés és a közöttük eltelt várakozási idő együttes eredménye.

A pooling=false connection string paraméterrel teszt célból kikapcsolhatod a poolingot, hogy lásd a különbséget. De ezt élőben ne tedd, mert nagyon lelassul tőle az appod, nem véletlenül van bekapcsolva.

Szóval általában nem kell foglalkozni a logout idejével.

Newer Posts »

Powered by WordPress