BLOGas.lt
Sukurk savo BLOGą Kitas atsitiktinis BLOGas

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

Tikslios SQL klaidos gavimas iš SqlException

Parašė Sergejus | 2010-05-27 22:52

Dirbant su duomenų bazėmis, kartais svarbu žinoti kokia tiksliai SQL klaida įvyko serveryje. Iš T-SQL pusės tai sužinoti labai paprasta – SELECT @@ERROR. Iš C# pusės mes gauname SqlException, bet savybė ErrorCode nėra @@ERROR atitikmuo.

Jeigu jums, pavyzdžiui, reikia apdoroti SQL klaidą ‘Delete statement conflicted with column reference…’, mes pirma turime sužinoti šios klaidos numerį SQL serveryje. Tai daroma užklausos SELECT * FROM master.sys.sysmessages pagalba. Prieš tai minėtos klaidos kodas yra 547. Toliau rašome paprastą C# praplėtimo metodą IsSqlDeleteConflictException:

public static class ExceptionExtensions
{
    public static bool IsSqlDeleteConflictException(this Exception exception)
    {
        var errorNumber = 547;
        var e = exception as SqlException;

        return e != null &&
               e.Errors.Cast<SqlError>().Any(err => err.Number == errorNumber);
    }
}

Rodyk draugams

Kito Lietuvos .NET vartotojų grupės tema – metodologijos?!

Parašė Sergejus | 2010-05-24 18:13

Nuo paskutinio Lietuvos .NET vartotojų grupės susitikimo praėjo jau pusantro mėnesio ir laikas ruoštis sekančias. Šiandien kartu su Tautvydu kilo mintis kitą Lietuvos .NET vartotojų grupės susitikimą skirti programinės įrangos kūrimo metodologijoms. Manau, didžiausias dėmėsis būtų skirtas Agile ir viskam kas aplink jį.

Šiuo metu mintis yra turėti pristatymą (-us) + apie 1 valandos trukmės diskusiją. Ką manote apie tai? Ar gerai, kad .NET vartotojų grupė pirmą kartą šnekės netiesiogiai apie .NET? Gal turite kokių konkrečių pageidavimų?

UPDATE

Gal tarp mūsų yra su KANBAN susipažinusių žinių?

Rodyk draugams

Keista IIS7 klaida - [PolicyException: Required permissions cannot be acquired]

Parašė Sergejus | 2010-05-19 21:37

Suinstaliavus Visual Studio 2010 viename iš kompiuterių, staiga negalėjau prieiti prie esamų ASP.NET puslapių. Bandant pasiekti puslapį – gaudavau klaidą:

[PolicyException: Required permissions cannot be acquired]

[FileLoadException: Could not load file or assembly 'NServiceBus …' or one of its dependencies. Failed to grant minimum permission requests. (Exception from HRESULT: 0x80131417)]

Pirmas įtarimas kuris man kilo: gal nusimušė teisės ar FullTrust. Tikrinimas nieko keisto neparodė, visos teisės buvo suteiktos. Tolesnė analizė parodė, kad nusimušė Load User Profile nustatymas IIS7 Web serveryje. Man padėjo tokie žingsniai:

  1. IIS Manager lange pasirinkti Application Pools
  2. Pasirinkti Web aplikacijos naudojamą Application Pool (mano atveju buvo DefaultAppPool)
  3. Paspausti Advanced Setting
    image 
  4. Atsiradusiame lange Process Model skiltyje parametrui Load User Profile pakeisti reikšmę į True
    image

Tikiuosi jums (ir man pačiam) ateityje, šitas straipsnis išsaugos pusvalandį-kitą.

Rodyk draugams

Pasikeitimų kolekcijoje aptikimas

Parašė Sergejus | 2010-05-17 20:29

Dirbant su kolekcijomis, dažna užduotis yra palyginti kaip jos pasikeitė su laiku: kokie įrašai buvo pridėti, kokie ištrinti, o kokie iš viso nesikeitė. Tai ypatingai dažnai tenka daryti atvaizduojant 1:N ir N:M ryšius formose (puslapiuose). Šiai užduočiai atlikti geriausia panaudoti aibių teoriją, o kur aibių teorija – ten ir LINQ (metodai Except bei Union). Šiandien nusprendžiau pasirašyti pagalbinę klasę Diff, kuri kiek palengvina pakeitimų sąraše aptikimą:

public class Diff<TType>
{
    private readonly IEnumerable<TType> original;

    private readonly IEnumerable<TType> current;

    private readonly IEqualityComparer<TType> comparer;

    public Diff(IEnumerable<TType> original, IEnumerable<TType> current)
    {
        if (original == null)
        {
            throw new ArgumentNullException("original");
        }

        if (current == null)
        {
            throw new ArgumentNullException("current");
        }

        this.original = original;
        this.current = current;
    }

    public Diff(IEnumerable<TType> original, IEnumerable<TType> current, IEqualityComparer<TType> comparer)
        : this(original, current)
    {
        if (comparer == null)
        {
            throw new ArgumentNullException("comparer");
        }

        this.comparer = comparer;
    }

    public IEnumerable<TType> Added
    {
        get
        {
            return comparer == null ? current.Except(original) : current.Except(original, comparer);
        }
    }

    public IEnumerable<TType> Removed
    {
        get
        {
            return comparer == null ? original.Except(current) : original.Except(current, comparer);
        }
    }

    public IEnumerable<TType> Unchanged
    {
        get
        {
            return comparer == null ? current.Except(Added) : current.Except(Added, comparer);
        }
    }
}

 

Kaip matyti, vienas iš Diff klasės konstruktorių priima IEqualityComparer<T> interfeisą, kuris leidžia aprašyti elementų palyginimo taisykles. Tai nėra aktualu primityviems tipams (int, string, double), bet pasidaro svarbu dirbant su objektais. Iš praktikos galiu pasakyti, kad tokiais atvejais dažnai norima lyginti ne pagal visas objekto savybes, bet tik pagal konkrečią. Šiai operacijai palengvinti, galima pasinaudoti pagalbine DefaultComparer<T> klase. Norint TTipas tipo objektus lyginti, tarkime, pagal savybę Name, šios klasės pagalba jums užtenka nurodyti DefaultComparer<TTipas>.For(obj => obj.Name). DefaultComparer<T> realizacija pateikta žemiau:

public static class DefaultComparer<T>
{
    public static IEqualityComparer<T> For<V>(Func<T, V> getValue)
    {
        return new DefaultComparer<T, V>(getValue);
    }
}

public sealed class DefaultComparer<T, V> : IEqualityComparer<T>
{
    private readonly Func<T, V> getValue;

    public DefaultComparer(Func<T, V> getValue)
    {
        this.getValue = getValue;
    }

    public bool Equals(T x, T y)
    {
        return EqualityComparer<V>.Default.Equals(getValue(x), getValue(y));
    }

    public int GetHashCode(T obj)
    {
        var value = getValue(obj);
        if (value == null)
        {
            return 0;
        }

        return EqualityComparer<V>.Default.GetHashCode(value);
    }
}

Tikiuosi bent daliai jūsų šios klasės pravers norint aptikti pasikeitimus kolekcijose.

Rodyk draugams

Programavimo standartai

Parašė Sergejus | 2010-05-10 19:14

Kuriant programinę įrangą komandoje, atsiranda poreikis susitarti dėl programavimo standartų. Įdomu tai, kad bent jau projekto pradžioje žmonių požiūris į bendrus susitarimus yra pakankamai skeptiškas. Tai galima suprasti – kiekvienas turi savo stilių, savo ego ir nenori persimokinti. Problema atsiranda tada, kai tenka palaikyti kolegų kodą. Tokiais atvejais dažnai ir pradedama suprasti kam reikalingi susitarimai bei vieningas programavimo stilius.

Vykdant projektus, vienas pirmų dalykų kuriuos aš įvedu komandoje – vieningas programavimo standartas. Dažniausiai tai apima C# ir ASP.NET kodą. Internete galima rasti nemažai taip vadinamų „kodavimo standartų“, bet jų kokybė ir teisingumas kartais kelia didelių abejonių. Šiandien nusprendžiau pateikti sąrašą mano manymų vertų dėmesio programavimo standartų C#, ASP.NET bei JavaScript kalboms:

Iš kitų pagalbinių įrankių labai rekomenduoju Microsoft StyleCop, kuris leidžia tikrinti kodo atitikimą standartams tiesiogiai Visual Studio aplinkoje ar vykdant automatinį surinkimą Team Foundation Server pagalba. Negalima sakyti, kad su visomis jo taisyklėmis aš asmeniškai sutinku, bet komandose su daugiau nei 5 žmonėmis, mano manymu, tai yra „must have“ tipo įrankis.

Vienintelis dalykas, kurio dar nesu radęs (ir labai norėčiau jūsų pagalbos), tai išsamus T-SQL programavimo standartas. Siūlykite komentaruose!

Rodyk draugams

10 geriausių nemokamų Visual Studio 2010 papildukų (Add-ons)

Parašė Sergejus | 2010-05-04 18:39

Nuo oficialaus Visual Studio 2010 pristatymo praėjo kelios savaitės ir tikriausiai nemažai jūsų jau vienaip ar kitaip ją naudojate. Kaip aš ir minėjau, Visual Studio 2010 buvo pakeistas praplėtimo mechanizmas nauju Managed Extensibility Framework (MEF), kas leidžia paprasčiau kurti bei valdyti papildukus. Siūlau jūsų dėmesiui, mano manymu, 10 geriausių nemokamų Visual Studio 2010 papildukų.

10 vieta

StructureAdornment – vaizdžiai parodo kodo blokų ribas

clip_image002

9 vieta

Triple Click – su trečiu paspaudimu pažymi visą eilutę

clip_image003

8 vieta

Go To Defenition – atitinka F12 mygtuko paspaudimą (deja, skirtingai negu ReSharper, į interfeisų realizaciją nenueina)

clip_image004

7 vieta

Find Results Highlighter – paryškina ieškomą frazę

clip_image006

6 vieta

Format Document – atlieka kodo formatavimą (CTRL+K, CTRL+D atitikmuo) iš karto visame sprendime

clip_image007

5 vieta

Remove And Sort Using – surūšiuoja ir ištrina nereikalingus using sakinius iš karto visame sprendime

clip_image008

4 vieta

Visual Studio Color Theme Editor – WPF dėka, tapo įmanoma keisti Visual Studio spalvą

clip_image009

3 vieta

Highlight all occurrences of selected word – vaizdžiai parodo kur naudojamas pažymėtas teksto fragmentas

clip_image011

2 vieta

Hide Main Menu – paslepia meniu juosta taip, kaip tai daro Internet Explorer 8 (meniu parodomas paspaudus Alt)

clip_image012

1 vieta

Spell Checker – tikrina anglų kalbos gramatiką kode

clip_image013

 

Dar kartą noriu priminti, tai yra mano asmeninė nuomonė ir nebūtinai tai kas atrodo man aktualu, jums bus irgi. Pirmas tris vietas užimančius papildukus aš aktyviai naudoju kasdieniniame darbe ir kol kas esu visiškai jais patenkintas.

Rodyk draugams