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.

Jeg har kæmpet lidt med at få UPDATE vha. Table<Entity>.Attach til at virke med LINQ to SQL.  Lad os tage udgangspunkt i en databasetabel User.  Vha. LINQ to SQL har vi fået genereret en User klasse, som logisk set har følgende udseende:

public class User
{
    public int Id { get; set; }
    public bool IsMale { get; set; }
    public int Age { get; set; }
    public string Name { get; set; }
}

Vi vil nu gerne opdatere en række i databasen, som har Id = 1.  Det data, der skal gemmes, har en bruger indtastet i en form på en ASP.NET side.  Så vi forsøger os med følgende:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    User lt = new User()
    {
        Id = 1
    };

    db.Users.Attach(lt);

    lt.IsMale = true;
    lt.Age = 42;
    lt.Name = "!";
    db.SubmitChanges();
}

Vores update går fint igennem og alt er godt.

En dag kommer der en rigtig provokerende bruger forbi vores website.  Da han skal redigere sine eksisterende brugeroplysninger, sætter han alder til 0 og udfylder ikke sit navn.  Vi får følgende værdier:

using (DataClassesDataContext db = new DataClassesDataContext())
{
    User lt = new User()
    {
        Id = 1
    };

    db.Users.Attach(lt);

    lt.IsMale = true;
    lt.Age = 0;
    lt.Name = null;
    db.SubmitChanges();
}

Til vores skræk bliver alder og navn ikke opdateret i databasen, og den dag en kvinde finder på ikke at udfylde alder og navn går det helt galt: Intet bliver opdateret.  Årsagen er, at en property på klassen User ikke bliver ændret og at diverse property changed events ikke bliver  kaldt, hvis den nye værdi ikke er anderledes end den eksisterende.  Når man som ovenfor opretter en instans af User, bliver alle properties sat til deres default værdier: False for bools, 0 for integers, null for strings etc.  Derfor mener LINQ to SQL ikke, at det er nødvendigt at sende en UPDATE til databasen, hvis man sætter de forskellige properties til deres default værdier.  Bemærk i øvrigt, at dette har intet at gøre med "Update Check" i .dbml filer.

På nettet er der diverse "løsninger" til problemet: Indlæs først den pågældende entity fra databasen via samme data context og undgå dermed Attach, gem alle entities i Session eller gem alle entities i ViewState.  Jeg er ikke begejstret for nogen af løsningerne.  ViewState løsningen er slet ikke mulig i ASP.NET MVC.

En anden mulig løsning er at sætte alle properties til noget snedigt før man laver sin Attach.  Det er ikke kønt, så jeg er modtagelig overfor andre forslag:

public static void Update(int id, bool isMale, int age, string name)
{
    using (DataClassesDataContext db = new DataClassesDataContext())
    {
        // Grimt - men det virker
        User lt = new User()
        {
            Id = id,
            IsMale = !isMale,
            Age = age == 0 ? 1 : age,
            Name = name == null ? "" : name
        };

        db.Users.Attach(lt);

        lt.IsMale = isMale;
        lt.Age = age;
        lt.Name = name;
        db.SubmitChanges();
    }
}