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.