BLOGas.lt
Sukurk savo BLOGą Kitas atsitiktinis BLOGas

LINQ to SQL meistriškumo klasė #2 (Attach/Detach kurie veikia)

Parašė Sergejus | 2010-06-06 13:14

Kaip žinia, didžiausias (bent jau man) LINQ to SQL apribojimas – normalaus Attach/Detach mechanizmo nebuvimas. Kai kurie gali pradėti prieštarauti – bet gi yra Attach! Taip jis yra, bet jis neveikia kaip turėtų veikti. Šiame straipsnyje aš parodysiu neidealų, bet veikiantį LINQ to SQL Attach/Detach sprendimą.

Pirmas mitas – Attach/Detach reikalauja TimeStamp tipo stulpelio lentelėje. Tikrai ne, savo pavyzdyje aš naudosiu modelį iš pirmos dalies:

image

Kaip žinia, Detach metodas skirtas atrišti esybę nuo duomenų konteksto, o savo ruožtu Attach metodas – atvirkščiai pririšti. Žemiau pateiktas kodas, kuris logiškai turėtų veikti, bet, deja, LINQ to SQL neveikia:

static void Main(string[] args)
{
    Post post;

    using (var db = new DemoDataContext())
    {
        post = db.Posts.First();
    }

    post.Title = "Testuojame";

    using (var db = new DemoDataContext())
    {
        db.Posts.Attach(post, true);
        db.SubmitChanges();
    }

    Console.ReadKey();
}

Vietoje Detach metodo aš naudoju using bloką, o Attach metodui išreikštinai nusakau, kad mano objektas buvo prieš tai modifikuotas. Atrodytų viskas gerai, bet vykdymo metu gausite klaidą: An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported. Internete vieni siūlo pridėti TimeStamp stulpelį, kiti siūlo dar kitus sprendimus. Tam kad tiksliai išsiaiškinti problemos priežastį, sugaišau virš dienos, bet dabar galiu pasidalinti su jumis. Problema atsiranda, kai jūsų objektas turi ryšius į kitus (tėvinį ar vaikinius) objektus. Mano atveju, objektas post turi savybę Category. Po using panaudojimo, pasirodo ne visai korektiškai išsivalo šios savybės (priminsiu, jos yra EntitySet arba EntityRef tipo), dėl ko ir atsiranda klaida kviečiant Attach metodą.

Problema aiški – reikia išvalyti tėvinius ir vaikinius objektus. Klausimas – kaip? Internete pavyko rasti kelis būdus, bet visi reikalavo pakankamai daug rankinio darbo (ko aš labai nemėgstu). Perskaitęs dešimtis įvairiausių straipsnių, agregavus visus duomenis bei galimus sprendimų variantus, apsistojau ties tokiu:

DBML dizaineryje nustatome, kad mūsų esybės gali būti serializuojamos:

image

Aprašome tokią pagalbinę klasę Detacher:

internal static class Detacher
{
    public static TEntity Detach<TEntity>(TEntity entity)
    {
        return Deserialize<TEntity>(Serialize(entity));
    }

    private static string Serialize<TEntity>(TEntity entity)
    {
        using (var stringWriter = new StringWriter())
        {
            using (var xmlWriter = XmlWriter.Create(stringWriter))
            {
                var serializer = new DataContractSerializer(typeof(TEntity));
                serializer.WriteObject(xmlWriter, entity);
            }
            return stringWriter.ToString();
        }
    }

    private static TEntity Deserialize<TEntity>(string entity)
    {
        using (var stringReader = new StringReader(entity))
        using (var reader = XmlReader.Create(stringReader))
        {
            var serializer = new DataContractSerializer(typeof(TEntity));

            return (TEntity)serializer.ReadObject(reader);
        }
    }
}

Bei kad natūraliau atrodytų – praplėtimo metodą Detach:

static class Extensions
{
    public static TEntity Detach<TEntity>(this DataContext db, TEntity entity)
        where TEntity : class
    {
        return Detacher.Detach(entity);
    }
}

Dabar atnaujinam mūsų pradinį pavyzdį pridedant vieną eilutę:

static void Main(string[] args)
{
    Post post;

    using (var db = new DemoDataContext())
    {
        post = db.Posts.First();
    }

    post.Title = "Testuojame";

    using (var db = new DemoDataContext())
    {
        post = db.Detach(post);
        db.Posts.Attach(post, true);
        db.SubmitChanges();
    }

    Console.ReadKey();
}

Staiga viskas pradeda veikti! Bet ne viskas taip gražu, kaip gali atrodyti iš pirmo žvilgsnio. Šis metodas turi vieną svarbų ribojimą – po Detach metodo iškvietimo, jus neturėsite Lazy Loading galimybės. Kaip pavyzdys, aukščiau pateiktame kode savybė Category bus visada NULL. Jeigu jums vis dėlto reikia ją pasiekti – pasinaudokite Include metodu iš praeito straipsnio:

static void Main(string[] args)
{
    Post post;

    using (var db = new DemoDataContext())
    {
        post = db.Posts.Include(p => p.Category).First();
    }

    Console.WriteLine(post.Category.Name);

    post.Title = "Testuojame";

    using (var db = new DemoDataContext())
    {
        post = db.Detach(post);
        db.Posts.Attach(post, true);
        db.SubmitChanges();
    }

    Console.ReadKey();
}

Nepaisant minėto apribojimo, per du metus intensyvaus darbo su LINQ to SQL nieko geresnio man sugalvoti nepavyko. Iš kitos pusės, Include ir Detach/Attach metodai man leidžia be problemų kurti tikrai dideles aplikacijas, kur LINQ to SQL naudojamas tiek Web, tiek WCF, tiek Windows dalyse.

Visą kodą jus galite rasti mano SBToolkit projekte.

Rodyk draugams

LINQ to SQL meistriškumo klasė #1 (LoadOptions analogas)

Parašė Sergejus | 2010-05-30 19:06

Šiandien pradedu naują, trijų dalių “LINQ to SQL meistriškumo klasės” straipsnių ciklą, skirtą aptarti LINQ to SQL ribojimus bei galimus apėjimo būdus, kurių prisireikia kuriant sudėtingas bei atjungtas (angl. disconnected) programas.

 

Apie SELECT N + 1 problemą aš esu jau kelis kartus rašęs, bet ji vis tiek lieka viena dažniausių klaidų dirbant su ORM įrankiais bendrai ir su LINQ to SQL konkrečiai. SELECT N + 1 problema atsiranda kaip Lazy Loading galimybės pasekmė.

Prisiminimui pateiksiu elementariausią SELECT N + 1 problemos pavyzdį. Tarkime, turime lenteles Post ir Category:

image

Žemiau pateiktas kodas įvykdys N + 1 SELECT užklausą, 1 – SELECT * FROM POST, o N – kiekvienam straipsniui po SELECT * FROM Category WHERE CategoryId = Category.Id.

static void Main(string[] args)
{
    using (var db = new DemoDataContext())
    {
        db.Log = Console.Out;

        var posts = db.Posts.ToList();
        foreach (var post in posts)
        {
            Console.WriteLine("Title: " + post.Title);
            Console.WriteLine("Category: " + post.Category.Name);
            Console.WriteLine();
        }
    }

    Console.ReadKey();
}

image

Anksčiau aš rašiau apie standartinį šios problemos sprendimo būdą – LoadOptions panaudojimą:

static void Main(string[] args)
{
    using (var db = new DemoDataContext())
    {
        db.Log = Console.Out;

        var loadOptions = new DataLoadOptions();
        loadOptions.LoadWith<Post>(p => p.Category);
        db.LoadOptions = loadOptions;

        var posts = db.Posts.ToList();
        foreach (var post in posts)
        {
            Console.WriteLine("Title: " + post.Title);
            Console.WriteLine("Category: " + post.Category.Name);
            Console.WriteLine();
        }
    }

    Console.ReadKey();
}

Pagrindinė šio būdo problema – LoadOptions galima nurodyti tik vieną kartą. Bandymas pakeisti LoadOptions reikšmę pasibaigs vykdymo klaida “Setting load options is not allowed after results have been returned from a query”. Tai reiškia, kad nurodžius DataContext visada kartu su straipsniais krauti ir kategorijas – taip bus daroma net kai mums reikalingi tik straipsniai. Pats nė kartą esu pakliuvęs į šiuos LoadOptions spąstus. Vienas iš šio apribojimo apėjimo būdų – tiesiog kurti naują DataContext.

Situacija pasikeitė prieš kelias savaites, kada Damien Guard (dirbo prie LINQ to SQL) savo straipsnyje aprašė dar vieną būdą, leidžiantį išnaudoti tą patį DataContext. Damieno pavyzdys veikia tik su .NET 4.0, o kadangi LINQ to SQL naudojamas pagrinde su .NET 3.5 SP1, nusprendžiau adaptuoti jo kodą šiai .NET Framework versijai.

Damieno būdas remiasi vidiniais LINQ to SQL veikimo principais. Kad būtų aiškiau, pirma pateiksiu konkretų prieš tai pateiktos užklausos pavyzdį, o paskui ir apibendrintą kodą.

Prieš pradedant, mums bus reikalinga pagalbinė klasė, kuria pavadinau Pair:

internal class Pair<TFirst, TSecond>
{
    public Pair() { }

    public Pair(TFirst first, TSecond second)
    {
        First = first;
        Second = second;
    }

    public TFirst First { get; set; }

    public TSecond Second { get; set; }
}

Perrašę mūsų originalią LINQ to SQL užklausą tokiu būdu, mes išvengsime SELECT N + 1 problemos nenaudojant LoadOptions mechanizmo:

static void Main(string[] args)
{
    using (var db = new DemoDataContext())
    {
        db.Log = Console.Out;

        var posts = db.Posts
                      .Select(p => new Pair<Post, Category>(p, p.Category))
                      .Select(p => p.First)
                      .ToList();
        foreach (var post in posts)
        {
            Console.WriteLine("Title: " + post.Title);
            Console.WriteLine("Category: " + post.Category.Name);
            Console.WriteLine();
        }
    }

    Console.ReadKey();
}

Paaiškinsiu kaip tai veikia. Užklausos viduje pirma mes nurodome, kad reikės sukurti projekciją, turinčią tiek patį straipsnį, tiek su juo susijusią kategoriją. Iš karto po klasės Pair sukūrimo mes projektuojame atgal, kad grąžintų vien straipsnius. Tokio užrašymo užtenka, kad LINQ to SQL mechanizmas nuspręstų padaryti JOIN su kategorijomis, išvengiant SELECT N + 1 problemos!

Tikriausiai pastebėjote, kad aukščiau pateiktą projekcijų logiką galima apibendrinti:

static void Main(string[] args)
{
    using (var db = new DemoDataContext())
    {
        db.Log = Console.Out;

        var posts = db.Posts
                      .Include(p => p.Category)
                      .ToList();
        foreach (var post in posts)
        {
            Console.WriteLine("Title: " + post.Title);
            Console.WriteLine("Category: " + post.Category.Name);
            Console.WriteLine();
        }
    }

    Console.ReadKey();
}

Svarbu pažymėti, kad Include grąžina IEnumerable, o ne IQueryable ir turi būti kviečiamas užklausos pabaigoje. Žemiau pateikta pilna metodo Include realizacija:

public static class LinqExtensions
{
    public static IEnumerable<T> Include<T, TInclude>(this IQueryable<T> query, Expression<Func<T, TInclude>> selector)
    {
        var elementParameter = selector.Parameters.Single();
        var pairType = typeof(Pair<T, TInclude>);
        var selectorExpression = Expression.Lambda<Func<T, Pair<T, TInclude>>>(
           Expression.New(pairType.GetConstructor(new[] { typeof(T), typeof(TInclude) }),
              new Expression[] { elementParameter, selector.Body },
              pairType.GetProperty("First"), pairType.GetProperty("Second")),
           elementParameter);

        return query.Select(selectorExpression).AsEnumerable().Select(pair => pair.First);
    }

    private class Pair<TFirst, TSecond>
    {
        public Pair() { }

        public Pair(TFirst first, TSecond second)
        {
            First = first;
            Second = second;
        }

        public TFirst First { get; set; }

        public TSecond Second { get; set; }
    }
}

Include metodą jus galite rasti ir mano SBToolkit projekte!

Rodyk draugams

TransactionScope pavojai

Parašė Sergejus | 2010-04-25 22:59

Tikriausiai didžioji jūsų dalis vienaip ar kitaip susidūrę su TransactionScope klase. Pagrindinis jos tikslas – užtikrinti unifikuotą transakcijų valdymą nepriklausomai nuo naudojamos technologijos. Pavyzdžiui, jeigu TransactionScope naudojamas kartu su SqlConnection, jis patampa SqlTransaction; jeigu naudojamas kartu su MSMQ eilėmis – MessageQueueTransaction. TransactionScope taipogi naudojamas transakcijų valdymui skirtinguose ORM karkasuose: LINQ-to-SQL, Entity Framework ir t.t.

Man buvo tikras atradimas, kad naudojant TransactionScope kartu su LINQ-to-SQL naudojamas ne standartinis SqlConnection izoliacijos lygis ReadCommited, bet pats aukščiausias – Serializeable. Praktikoje tai reiškia, kad vykdant net paprastą SELECT užklausą, visi nuskaityti duomenys bus užrakinti iki transakcijos pabaigos (Commit arba Rollback) ir yra didelė tikimybė gauti Timeout klaidą. Laimei, norėdami pakeisti izoliacijos lygį į ReadCommited užtenka tai nurodyti TransactionScope konstruktoriuje:

using(var transaction = new TransactionScope(
                            TransactionScopeOption.Required,
                            new TransactionOptions { IsolationLevel.ReadCommited }))
{ }

Rodyk draugams

LINQ to SQL audito biblioteka

Parašė Sergejus | 2009-09-01 00:17

Pastaruosius du metus aš labai intensyviai dirbu su LINQ to SQL. Per šį laikotarpį teko su juo daryti įvairiausius dalykus - nuo greitaveikos optimizavimo iki automatinio įrašų pakeitimų audito. Pastarąjį šiandien aptarsiu detaliau.

Viename iš projektų man reikėjo sekti visus vartotojų atliekamus veiksmus su duomenimis (pridėjimą, trynimą ir redagavimą) arba kitaip tariant - auditą (angl. Audit Trail). Kadangi visos operacijos su duomenų baze vyksta per duomenų kontekstą, užsinorėjo man specialiai tam skirtos LINQ to SQL audito bibliotekos. Geriausia ką pavyko rasti - Matto biblioteka DoddleAudit. Detaliau ją išanalizavus paaiškėjo, kad tai nevisai ko aš norėjau. Pirma Matto realizacija reikalauja, kad bet kuris duomenų kontekstas paveldėtų iš jo bazinės klasės, kas man buvo nepriimtina. Antra nebuvo galimybės iš karto įjungti visų esybių sekimo. Taigi nusprendžiau parašyti savo paprastą LINQ to SQL audito biblioteką.

Pagrindinis reikalavimas auditui iš mano pusės buvo nustatyti kuris naudotojas kokius veiksmus su duomenimis atliko, kokios laukų reikšmės buvo iki operacijos ir po jos. Tam aš apibrėžiau tokias C# klases:

Pagrindinė klasė yra AuditEntry, ji saugo informaciją apie tai kas atliko pakeitimus, kada, kokioje lentelėje ir kokiam tos lentelės įrašui (EntityTableKey). AuditField saugo senas ir naujas laukų reikšmes, o Action nurodo atliktą veiksmą.

Žemiau pateikiu šių klasių C# kodą:

Pasižiūrėję į klases AuditEntry ir AuditField jums gali kilti klausimas - kodėl aš naudoju POCO C# klases, bet ne LINQ to SQL klases. Aš norėjau šią biblioteką padaryti nepriklausoma nuo konkrečios duomenų bazės struktūros. Šiame pavyzdyje, aš naudosiu tokias AuditEntry ir AuditField atitinkančias duomenų bazės lenteles:

Paskutinė mums reikalinga klasė, o tiksliau atributas - IgnoreAudit:

Kaip ir seka iš pavadinimo, jeigu mums nereikia audituoti tam tikros LINQ to SQL esybės, užtenka partial klasėje tiesiog jai uždėti šį atributą.

Dabar mes jau turime pagrindą, laikas pereiti prie pačios audito logikos. Tam aš esu parašęs statinę klasę AuditExtensions su dviem praplėtimo metodais. Pagrindinis audito metodas yra AuditAndSaveChanges:

Trumpai aptarsiu kas yra daroma:

  • Duomenų kontekstas turi labai mums svarbų metodą GetChageSet. Jo pagalba galima gauti visas pridėtas, ištrintas ar redaguotas esybes.
  • Metodo viduje kiekvienam iš veiksmų aš kviečiu metodą Audit, kuris tiesiog pasiimą esybę ir pagal ja sukuria AuditEntry objektą.
  • Kadangi AuditAndSubmitChagnes bus kviečiamas SubmitChanges metode, mums papildomai reikalingas bazinis SubmitChanges metodas.
  • Delegatas transform atsakingas už AuditEntry ir AuditField transformaciją į konkrečias duomenų bazės esybes (mūsų atveju į AuditRecord ir AudifField atitinkamai.

Žemiau pateiksiu pagrindinį šios bibliotekos metodą Audit:

Kaip matote, metodas yra pakankamai ilgas ir sudėtingas. Viskas ką jis daro tai priklausomai nuo atlikto veiksmo (pridėjimo, trynimo, redagavimo) refleksijos pagalba pasiima senas ir naujas laukų reikšmes bei pagal tai sukuria AuditEntry objektus.

Šiame metode aš panaudojau keletą pagalbinių metodų iš SBToolkit projekto, tarp jų:

Štai ir viskas - biblioteka baigta! Bet kaip ja pasinaudoti? Pirma reikia sukurti esamam duomenų kontekstui partial klasę…

… ir perrašyti joje metodą SubmitChanges:

Štai taip, LINQ to SQL audito biblioteka savo rankomis. Čia aprašytą biblioteką galite parsisiųsti iš CodePlex kaip SBToolkit projekto dalį (projektai SBAudit ir SBAudit.LinqToSql).

Tikiuosi šis straipsnis jums bus naudingas ir laukiu komentarų / pasiūlymų dėl šios LINQ to SQL audito bibliotekos!

Rodyk draugams