Quando dovrei chiamare SaveChanges () durante la creazione di migliaia di oggetti Entity Framework? (come durante un’importazione)

Sto eseguendo un’importazione che avrà 1000 di record per ogni analisi. Sto solo cercando alcune conferme sulle mie ipotesi:

Quale di questi ha più senso:

  1. Esegui SaveChanges() ogni chiamata AddToClassName() .
  2. Eseguire SaveChanges() ogni n numero di chiamate AddToClassName() .
  3. Esegui SaveChanges() dopo tutte le chiamate AddToClassName() .

La prima opzione è probabilmente lenta, giusto? Dal momento che dovrà analizzare gli oggetti EF in memoria, generare SQL, ecc.

Suppongo che la seconda opzione sia la migliore di entrambi i mondi, dal momento che possiamo eseguire un tentativo di aggirare la chiamata a SaveChanges() , e perdere solo n numero di record alla volta, se uno di essi fallisce. Forse memorizzare ogni partita in un elenco . Se la chiamata SaveChanges() positivo, eliminare l’elenco. Se fallisce, registra gli articoli.

L’ultima opzione probabilmente finirà per essere anche molto lenta, dal momento che ogni singolo object EF dovrebbe essere in memoria fino a quando non si chiama SaveChanges() . E se il salvataggio fallisse, non verrebbe commesso nulla, giusto?

Lo testerei per primo per essere sicuro. Le prestazioni non devono essere così male.

Se è necessario inserire tutte le righe in una transazione, chiamarla dopo tutta la class AddToClassName. Se le righe possono essere inserite indipendentemente, salvare le modifiche dopo ogni riga. La consistenza del database è importante.

Seconda opzione che non mi piace. Sarebbe fonte di confusione per me (dal punto di vista dell’utente finale) se avessi importato nel sistema e avrebbe declinato 10 righe su 1000, solo perché 1 è negativo. Puoi provare a importare 10 e se fallisce, prova uno per uno e poi loggati.

Verifica se richiede molto tempo. Non scrivere “propably”. Non lo sai ancora. Solo quando è effettivamente un problema, pensa ad un’altra soluzione (marc_s).

MODIFICARE

Ho fatto alcuni test (tempo in millisecondi):

10000 righe:

SaveChanges () dopo 1 riga: 18510,534
SaveChanges () dopo 100 righe: 4350,3075
SaveChanges () dopo 10000 righe: 5233,0635

50000 righe:

SaveChanges () dopo 1 riga: 78496,929
SaveChanges () dopo 500 righe: 22302,2835
SaveChanges () dopo 50000 righe: 24022,8765

Quindi è in realtà più veloce da impegnare dopo n file che dopo tutto.

La mia raccomandazione è di:

  • SaveChanges () dopo n righe.
  • Se un commit fallisce, provalo uno ad uno per trovare una riga errata.

Classi di prova:

TAVOLO:

 CREATE TABLE [dbo].[TestTable]( [ID] [int] IDENTITY(1,1) NOT NULL, [SomeInt] [int] NOT NULL, [SomeVarchar] [varchar](100) NOT NULL, [SomeOtherVarchar] [varchar](50) NOT NULL, [SomeOtherInt] [int] NULL, CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED ( [ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] 

Classe:

 public class TestController : Controller { // // GET: /Test/ private readonly Random _rng = new Random(); private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; private string RandomString(int size) { var randomSize = _rng.Next(size); char[] buffer = new char[randomSize]; for (int i = 0; i < randomSize; i++) { buffer[i] = _chars[_rng.Next(_chars.Length)]; } return new string(buffer); } public ActionResult EFPerformance() { string result = ""; TruncateTable(); result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "
"; TruncateTable(); result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "
"; TruncateTable(); result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "
"; TruncateTable(); result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "
"; TruncateTable(); result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "
"; TruncateTable(); result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "
"; TruncateTable(); return Content(result); } private void TruncateTable() { using (var context = new CamelTrapEntities()) { var connection = ((EntityConnection)context.Connection).StoreConnection; connection.Open(); var command = connection.CreateCommand(); command.CommandText = @"TRUNCATE TABLE TestTable"; command.ExecuteNonQuery(); } } private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows) { var startDate = DateTime.Now; using (var context = new CamelTrapEntities()) { for (int i = 1; i <= noOfRows; ++i) { var testItem = new TestTable(); testItem.SomeVarchar = RandomString(100); testItem.SomeOtherVarchar = RandomString(50); testItem.SomeInt = _rng.Next(10000); testItem.SomeOtherInt = _rng.Next(200000); context.AddToTestTable(testItem); if (i % commitAfterRows == 0) context.SaveChanges(); } } var endDate = DateTime.Now; return endDate.Subtract(startDate); } }

Ho appena ottimizzato un problema molto simile nel mio codice e vorrei sottolineare un’ottimizzazione che ha funzionato per me.

Ho scoperto che gran parte del tempo nell’elaborazione di SaveChanges, sia che elabori 100 o 1000 record contemporaneamente, è legato alla CPU. Quindi, elaborando i contesti con un pattern producer / consumer (implementato con BlockingCollection), sono stato in grado di fare un uso migliore dei core della CPU e ottenuto da un totale di 4000 cambiamenti / secondo (come riportato dal valore di ritorno di SaveChanges) a oltre 14.000 modifiche / secondo. L’utilizzo della CPU è passato da circa il 13% (ho 8 core) a circa il 60%. Anche utilizzando più thread di consumo, ho a malapena addebitato il (molto veloce) sistema di I / O del disco e l’utilizzo della CPU di SQL Server non era superiore al 15%.

Scaricando il salvataggio su più thread, è ansible regolare sia il numero di record prima del commit sia il numero di thread che eseguono le operazioni di commit.

Ho trovato che creando 1 thread di produzione e (# di core CPU) -1 thread di consumo mi permettevano di ottimizzare il numero di record commessi per batch in modo tale che il conteggio degli elementi in BlockingCollection oscillava tra 0 e 1 (dopo che un thread consumatore ne prendeva uno articolo). In questo modo, c’era abbastanza lavoro per far funzionare i thread consumati in modo ottimale.

Questo scenario richiede ovviamente la creazione di un nuovo contesto per ogni batch, che trovo più veloce anche in uno scenario a thread singolo per il mio caso d’uso.

Se devi importare migliaia di record, utilizzerei qualcosa come SqlBulkCopy e non il Entity Framework per quello.

  • Documenti MSDN su SqlBulkCopy
  • Utilizzare SqlBulkCopy per caricare rapidamente i dati dal client a SQL Server
  • Trasferimento di dati tramite SqlBulkCopy

Utilizzare una stored procedure.

  1. Creare un tipo di dati definito dall’utente nel server Sql.
  2. Crea e popola un array di questo tipo nel tuo codice (molto veloce).
  3. Passa la matrice alla procedura memorizzata con una chiamata (molto veloce).

Credo che questo sarebbe il modo più semplice e veloce per farlo.

Scusa, so che questa discussione è vecchia, ma penso che questo potrebbe aiutare le altre persone con questo problema.

Ho avuto lo stesso problema, ma c’è la possibilità di convalidare le modifiche prima di eseguirle. Il mio codice appare come questo e sta funzionando bene. Con chUser.LastUpdated controllo se si tratta di una nuova voce o di una modifica. Perché non è ansible ricaricare una voce che non è ancora nel database.

 // Validate Changes var invalidChanges = _userDatabase.GetValidationErrors(); foreach (var ch in invalidChanges) { // Delete invalid User or Change var chUser = (db_User) ch.Entry.Entity; if (chUser.LastUpdated == null) { // Invalid, new User _userDatabase.db_User.Remove(chUser); Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey); } else { // Invalid Change of an Entry _userDatabase.Entry(chUser).Reload(); Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey); } } _userDatabase.SaveChanges();