Designet bag Linq to Sql er tænkt som et Unit of Work pattern.  Som navnet måske antyder, skal ens DataContext klasse betragtes som en context.  Det betyder, at tanken er, at man holder sin DataContext i live under hele sin session eller for WinForms eventuelt i hele applikationens levetid.  Hvis man åbner og lukker DataContexts i flæng, kan man bl.a. løbe ind i Attach problemer ved updates.  Det betyder også, at de genererede entityklasser skal betragtes som egentlige domæneklasser.

I det følgende vil jeg give et eksempel på, hvorfor jeg ikke synes, at designet for Linq to Sql holder.

Lad os betragte en simpel databasemodel med to tabeller, der har en en-til-mange relation:

database

Lad os sige, at jeg ønsker at trække en bestemt Parent ud, og lidt efter ønsker jeg at trække et tilhørende Child ud med navnet "Child1".  Min første naive tilgang kan se ud som følgende:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    db.Log = Console.Out;

    var p = (from parents in db.Parents
             where parents.ParentId == 1
             select parents).Single();

    // Noget kode...
   
    var c = from children in p.Childs
            where children.Name == "Child1"
            select children;
    Console.WriteLine(c.Count());
}

 

Følgende viser output:

output1

Som det fremgår, er det ikke en god metode.  I den genererede SQL bliver min where-clause fuldstændig ignoreret, og det er først på klienten, at Linq to Sql filtrerer.  Eftersom jeg ikke filterer på primærnøglen, svarer det logisk til en tablescan - dvs. en for-løkke på mit EntitySet, hvor man tester et objekt ad gangen.  Det er ikke smart, hvis jeg har 8 mio. rækker i min Child tabel, som i øvrigt også først skal hives henover netværket.

Den anbefalede løsning fra eksperterne er at bruge en LEFT OUTER JOIN på Child tabellen vha. LoadWith funktionen:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    db.Log = Console.Out;

    DataLoadOptions options = new DataLoadOptions();
    options.LoadWith<Parent>(parent => parent.Childs);
    db.LoadOptions = options;
    db.DeferredLoadingEnabled = false;

    var p = (from parents in db.Parents
                 where parents.ParentId == 1
                 select parents).Single();

    // Noget kode.

    var c = from children in p.Childs
             where children.Name == "Child1"
             select children;
    Console.WriteLine(c.Count());
}

 

Dette giver følgende output:

output2

LoadWith giver en mere effektiv SQL, bortset fra en SELECT COUNT(*) på Child.  Jeg har svært ved at gennemskue, hvorfor den er nødvendig.

Problemet med denne løsning er, at LoadOptions sættes på det "globale" datacontext objekt.  Hvis vi på et senere tidspunkt ønsker at benytte deferred loading på samme DataContext, får vi en InvalidOperationException, for man må ikke ændre LoadOptions efter første load på en DataContext.

Man kan også bruge DataLoadOptions.AssociateWith i stedet for en where-clause på Child:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    db.Log = Console.Out;

    DataLoadOptions options = new DataLoadOptions();
    options.AssociateWith>Parent>(parent => parent.Childs.Where(child => child.Name == "Child1"));
    db.LoadOptions = options;

    var p = (from parents in db.Parents
                 where parents.ParentId == 1
                 select parents).Single();

    // Noget kode.

    var c = from children in p.Childs
             select children;
    Console.WriteLine(c.Count());
}

 

Resultatet er en noget mærkelig SQL med en (unødvendig) nested SQL til at finde parentid, og fordi DataContext.LoadOptions ikke kan ændres, er vi nu til "evig" tid bundet op på én where-clause:

output3

Konklusionen er, at man skal passe meget på, hvordan man bruger Linq to Sql.  Faktisk tør jeg ikke rigtig bruge det efter hensigten, men kun til små lukkede funktionskald, hvor jeg kan tillade mig at smide DataContext væk efter brug.  Men nu er der selvfølgelig også noget, der tyder på, at Linq to Sql vil lide en stille død.

Kommentarer

Janus Denmark siger:

2. februar 2009 18:34

Som med alle teknologier skal man overveje hvordan kagen skal skaeres. Jeg har snart set mange sjove forsoeg paa at holde DataContext i live og maa nok sige at flg. "Det betyder, at tanken er, at man holder sin DataContext i live under hele sin session"... er en smule forkert opfattet. DataContext skal oprettes naar den skal bruges og lukkes igen ved endt anvendelse.

MSFT har sandelig ikke taget fejl paa det plan, at de saa har givet mulighed for relation building med tilhoerende eksekvering kan dog undrer.

Brug SPs istedet for at indlejret SQL! Det er nemmere at debugge, nemmere at rette og nemmere at genbruge og sidst og meget vigtigt - query optimizeren kan optimere sin plan.
Skal du endelig lave et join, saa goer det efter data er hentet!

Janus Denmark siger:

3. februar 2009 00:19

LOL, har du fjernet min post. Jamen det er da også forfærdeligt sådan at blive modsagt med gode argumenter ;)

t4rzsan Denmark siger:

4. februar 2009 09:45

Undskyld, havde ikke opdaget, der var kommet en kommentar, så jeg havde ikke fået den godkendt Smile

Jeg synes, Entity Framework er kommet lidt bedre omkring det med at lukke sin context, fordi deres Attach/Detach model fungerer bedre.

I øvrigt kan SQL Server sagtens optimere parametriseret SQL og ikke kun SPs.  Det har den vist været i stand til siden SQL Server 2000, såvidt jeg husker Smile

Kommentarerne er lukkede