2014-05-13 13 views
9

Recentemente ho modificato parte della mia applicazione che stava funzionando molto lentamente disabilitando il rilevamento automatico delle modifiche (Context.Configuration.AutoDetectChangesEnabled = false) prima di fare un'eliminazione di massa, quindi riabilitarla e salvare le modifiche.Quali sono i bug che possono essere causati in EF disabilitando il rilevamento automatico delle modifiche?

ho letto un paio diverse fonti spiegando che, in sostanza, ogni volta che io chiamo metodi come .Add() o .Remove() su un DbSet, il DetectChanges() è chiamata, e che si può ottenere costoso quando abbiamo a che fare con un sacco di entità. OK.

Ora voglio richiamare l'attenzione su questi articoli, in particolare:

Entity Framework Automatic Detect Changes (MSDN)

un'alternativa alla disattivazione e la riattivazione è quello di lasciare la rilevazione automatica delle modifiche disattivato in ogni momento e sia chiamata context.ChangeTracker.DetectChanges esplicitamente o utilizzare diligentemente i proxy di rilevamento delle modifiche. Entrambe queste opzioni sono avanzate e possono facilmente introdurre bug sottili nella tua applicazione, quindi usali con cura.

Secrets of Detect Changes: Part 3

Non spegnere DetectChanges automatiche a meno che non si ha realmente bisogno di; ti causerà solo dolore

Forse è di fronte a me, ma partendo dal presupposto che, per esempio, ho avvolto .SaveChanges() in un metodo che ha sempre chiamato DetectChanges() primo, quello che i bug ho potuto iniziare incontrando che non avrei normalmente? Tutti gli avvertimenti che posso vedere suggeriscono vagamente che le cose brutte possono accadere senza entrare in quello che sono.

+0

err, nvm. Sto pensando che le modifiche dello schema non cambiano elemento. [redacted] –

+0

Le migrazioni sono totalmente irrilevanti alla mia domanda.Sto parlando del tracciamento delle modifiche di runtime che EF utilizza per decidere quali comandi SQL emettere dopo aver modificato le proprietà POCO. – Casey

risposta

14

Supponiamo di avere il seguente modello di BankAccount s e Deposit s - una semplice relazione uno-a-molti: BankAccount ha una collezione di Deposit s ed una Deposit appartiene ad un singolo BankAccount:

public class BankAccount 
{ 
    public int Id { get; set; } 
    public int AccountNumber { get; set; } 
    public string Owner { get; set; } 
    public ICollection<Deposit> Deposits { get; set; } 
} 

public class Deposit 
{ 
    public int Id { get; set; } 
    public decimal Value { get; set; } 

    public int BankAccountId { get; set; } 
    public BankAccount BankAccount { get; set; } 
} 

E un semplice contesto di database:

public class MyContext : DbContext 
{ 
    public DbSet<BankAccount> BankAccounts { get; set; } 
    public DbSet<Deposit> Deposits { get; set; } 
} 

Mr. John Smith vuole avere due conti presso la nostra banca e paga un deposito di 1.000.000 $ per il suo primo conto. programmatore della nostra banca svolge questo compito in questo modo:

using (var ctx = new MyContext()) 
{ 
    var bankAccount123 = new BankAccount 
    { 
     AccountNumber = 123, 
     Owner = "John Smith", 
     Deposits = new List<Deposit> { new Deposit { Value = 1000000m } } 
    }; 
    var bankAccount456 = new BankAccount 
    { 
     AccountNumber = 456, 
     Owner = "John Smith" 
    }; 

    ctx.BankAccounts.Add(bankAccount123); 
    ctx.BankAccounts.Add(bankAccount456); 

    ctx.SaveChanges(); 
} 

e funziona come previsto:

DetectChanges 1

Un giorno dopo, il signor Smith chiama la banca:. "Ho cambiato la mia mente ho don Voglio quei due account, solo uno, quello con il numero di conto 456, mi piace di più questo numero: sul mio conto 123 sono 1 milione di dollari, per favore spostali sull'account 456 e poi elimina il mio account 123! "

Il nostro programmatore aveva sentito dire che eliminare è una cosa pericolosa e ha deciso di copiare il database nell'ambiente di test e testare prima una nuova routine che ora scrive per seguire Mr.Richiesta di Smith:

using (var ctx = new MyContext()) 
{ 
    var bankAccount123 = ctx.BankAccounts.Include(b => b.Deposits) 
     .Single(b => b.AccountNumber == 123); 
    var bankAccount456 = ctx.BankAccounts 
     .Single(b => b.AccountNumber == 456); 
    var deposit = bankAccount123.Deposits.Single(); 

    // here our programmer moves the deposit to account 456 by changing 
    // the deposit's account foreign key 
    deposit.BankAccountId = bankAccount456.Id; 

    // account 123 is now empty and can be deleted safely, he thinks! 
    ctx.BankAccounts.Remove(bankAccount123); 

    ctx.SaveChanges(); 
} 

Corre il test e funziona:

DetectChanges 2

Prima di spostare il codice in produzione decide di aggiungere un po 'di miglioramento delle prestazioni, ma - ovviamente - non cambiare la logica testato per spostare il deposito e per eliminare l'account:

using (var ctx = new MyContext()) 
{ 
    // he added this well-known line to get better performance! 
    ctx.Configuration.AutoDetectChangesEnabled = false; 

    var bankAccount123 = ctx.BankAccounts.Include(b => b.Deposits) 
     .Single(b => b.AccountNumber == 123); 
    var bankAccount456 = ctx.BankAccounts 
     .Single(b => b.AccountNumber == 456); 
    var deposit = bankAccount123.Deposits.Single(); 

    deposit.BankAccountId = bankAccount456.Id; 

    ctx.BankAccounts.Remove(bankAccount123); 

    // he heard this line would be required when AutoDetectChanges is disabled! 
    ctx.ChangeTracker.DetectChanges(); 
    ctx.SaveChanges(); 
} 

corre il codice in produzione prima che finisce il suo wor quotidiano K.

Il giorno seguente, il signor Smith chiama la banca: "Ho bisogno di mezzo milione dal mio conto 456!" L'addetto al servizio clienti dice: "Mi dispiace, signore, ma non ci sono soldi sul suo conto 456". Mr. Smith: "Ah OK, non hanno ancora trasferito i soldi, quindi, per favore, prendi i soldi dal mio account 123!" "Mi spiace, signore, ma non hai un account 123!" Mr. Smith: "COSA ???" Servizio clienti: "Sono in grado di vedere tutti i tuoi conti e depositi nel mio strumento bancario e non c'è nulla sul vostro conto singola 456:"

DetectChanges 3


Cosa è andato storto quando il nostro programmatore ha aggiunto il suo piccolo miglioramento delle prestazioni e ha fatto del signor Smith un povero uomo?

La linea importante che si comporta in modo diverso dopo aver impostato AutoDetectChangesEnabled a false è ctx.BankAccounts.Remove(bankAccount123);. Questa linea ora non chiama più internamente DetectChanges. Il risultato è che EF non viene a conoscenza della modifica della chiave esterna BankAccountId nell'entità deposit (avvenuta prima della chiamata a Remove).

Con il rilevamento cambiamento abilitato Remove avrebbe regolato l'intero grafo oggetto secondo la chiave esterna cambiato ("relazione fixup"), cioè deposit.BankAccount sarebbe stato impostato bankAccount456, il deposit sarebbe stato rimosso dalla raccolta bankAccount123.Deposits e aggiunto alla collezione bankAccount456.Deposits.

Perché questo non è accaduto Remove segnato il genitore bankAccount123 come Deleted e mettere il deposit - che è ancora un bambino nella collezione bankAccount123.Deposits - in stato di Deleted pure. Quando viene chiamato il numero SaveChanges, entrambi vengono eliminati dal database.

Anche se questo esempio sembra un po 'artificiale, ricordo che ho avuto "bug" simili dopo aver disabilitato il rilevamento delle modifiche in codice reale che impiegava del tempo per trovare e capire. Il problema principale è che il codice che funziona e viene testato con il rilevamento delle modifiche probabilmente non funziona più e deve essere testato di nuovo dopo che il rilevamento delle modifiche è disabilitato anche se non è stato modificato nulla con quel codice. E forse il codice deve essere modificato per farlo funzionare di nuovo correttamente. (Nel nostro esempio il programmatore ha dovuto aggiungere ctx.ChangeTracker.DetectChanges();prima la linea Remove per correggere il bug.)

Questa è una delle possibili "bug sottili" nella pagina di MSDN sta parlando. Probabilmente ce ne sono molti altri.

+0

Risposta davvero fantastica. Mentre i miglioramenti delle prestazioni possono essere davvero sorprendenti ora so perché dovrò fare attenzione. – Casey

+0

@Slauma: ho capito bene che DetectChanges sta controllando solo per gli aggiornamenti? Nel tuo esempio era la linea: deposit.BankAccountId = bankAccount456.Id (un aggiornamento di proprietà) che ha causato questo comportamento. Se apporto alcune modifiche in un insieme di entità (aggiungere o rimuovere) non dovrebbe avere un comportamento simile, vero? Sto pianificando alcune operazioni di massa con entità nidificate e mi chiedevo – Learner

+1

@Learner: non so davvero se sarebbe sicuro quando il rilevamento automatico delle modifiche è disabilitato. 'DetectChanges' controlla anche i cambiamenti nelle collezioni. Di sicuro deve essere chiamato da qualche parte. Puoi provare a chiamarlo solo una volta prima di "SaveChanges". Fondamentalmente se imposti 'AutoDetectChangesEnabled' su' false' devi testare tutto attentamente. – Slauma

Problemi correlati