2013-08-07 15 views
17

Sto utilizzando ASP.NET MVC4 con Entity Framework Code First. Ho una tabella chiamata "utenti", con la chiave primaria "UserId". Questa tabella potrebbe contenere oltre 200.000 voci.Ignora inserimento chiave duplicato con Entity Framework

Ho bisogno di inserire altri 50 utenti. Potrei fare questo come

foreach(User user in NewUsers){ 
    context.Add(user); 
} 
dbcontext.SaveChanges(); 

Il problema è che uno o più di quei nuovi utenti potrebbero già esistere nel DB. Se li aggiungo e poi provo a salvare, genera un errore e nessuno di quelli validi viene aggiunto. Potrei modificare il codice per fare questo:

foreach(User user in NewUsers){ 
    if(dbcontext.Users.FirstOrDefault(u => u.UserId) == null) 
    { 
     dbcontext.Users.Add(user); 
    } 
} 
dbcontext.SaveChanges(); 

che funzionerebbe. Il problema è, quindi deve eseguire una query 50 volte su una tabella di immissione 200.000+. Quindi la mia domanda è: qual è il metodo più efficiente di inserire questi utenti, ignorando eventuali duplicati?

+2

forse 'context.AddOrUpdate (utente);' è quello che si vuole –

+1

@OO Non è in realtà gli utenti, è dati diversi che vengono estratti da un'API. L'API può o non può fornire gli stessi dati in più chiamate sequenziali. Ho appena usato "Utenti" perché era il primo esempio a cui pensavo. – Jordan

+1

Ho avuto lo stesso problema e non ho trovato una soluzione adeguata. Esistono scenari validi per questo, ad esempio quando si effettua un'importazione di massa da CSV in cui l'indirizzo di posta elettronica deve essere univoco in un database esistente. Leggere tutte le chiavi esistenti in memoria non sembra buono per le prestazioni, né aggiungere ciascuna voce separatamente. Sembra che ciò che è necessario sia qualcosa come INSERTO IGNORA. – acarlon

risposta

6

Si può fare questo:

var newUserIDs = NewUsers.Select(u => u.UserId).Distinct().ToArray(); 
var usersInDb = dbcontext.Users.Where(u => newUserIDs.Contains(u.UserId)) 
           .Select(u => u.UserId).ToArray(); 
var usersNotInDb = NewUsers.Where(u => !usersInDb.Contains(u.UserId)); 
foreach(User user in usersNotInDb){ 
    context.Add(user); 
} 

dbcontext.SaveChanges(); 

Questo eseguirà una singola query nel database per trovare gli utenti che già esistono, poi filtrare fuori della vostra NewUsers set.

+0

Per le raccolte di grandi dimensioni, sarebbe meglio utilizzare HashSet anziché .ToArray()? – tbmsu

+1

@tbmsu Per le chiamate al framework di entità, probabilmente non avrebbe alcun effetto sulle prestazioni perché il 'Contains' viene effettivamente tradotto in una clausola' IN' di SQL in modo che dipenda realmente dalle prestazioni del DB più di ogni altra cosa. Nota anche che per * grandi * set di dati, questo probabilmente non funzionerebbe affatto perché c'è un limite al numero di valori che puoi usare in una clausola 'IN' ([ref] (http://stackoverflow.com/questions/1069415/limite-on-the-dove-col-in-condizione)). –

2

Poiché questa è la chiave principale, le opzioni sono limitate. Se questa non era la chiave primaria e solo un indice univoco, supponendo SQL Server, è possibile impostare la chiave univoca per ignorare i duplicati.

Quello che potrei suggerire è semplicemente racchiudere un tentativo/aggirare l'Add e mangiare l'eccezione se l'eccezione è un errore di chiave duplicato.

Si potrebbe anche vedere se il proprio oggetto supporta il metodo AddOrUpdate(). So che questo è supportato nelle implementazioni Code First. Credo che in questo caso farà un add on a new or update se la riga esiste. Tuttavia, questo potrebbe comportare ancora un viaggio nel DB per vedere se l'utente esiste già per sapere se fare un add o un aggiornamento. E, in alcuni casi, potresti non voler eseguire effettivamente un aggiornamento.

Penso che se fossi in me, andrei sulla rotta Try/Catch.

+0

Quindi stai dicendo di salvare SaveChanges() con un try/catch attorno al ciclo for. Questo non aggiungerebbe un sovraccarico di 50 chiamate SaveChanges() diverse e le loro transazioni e operazioni di scrittura corrispondenti? – Jordan

+0

@Jordan - Per quanto riguarda le transazioni, dovrebbe esserci una sola transazione. Non dovresti avere più transazioni che si verificano qui. Per quanto riguarda l'overhead, sì, ce ne saranno alcuni. Ma sarà più grande che fare 50 (o qualsiasi altra) chiamata separata per vedere se ogni utente esiste prima? Il modo migliore per scoprirlo è testarlo e vedere. Personalmente, penso che preferirei prendere il sovraccarico in memoria dell'uso di Try/Catch piuttosto che effettuare chiamate DB ripetute. Ma dipenderà dal numero di utenti in genere inseriti contemporaneamente. –

3

è possibile filtrare gli utenti esistenti con una query

foreach(User user in NewUsers.Where(us => !dbcontext.Users.Any(u => u.userId == us.userId))) 
{ 
    dbcontext.Users.Add(user); 
} 
dbcontext.SaveChanges(); 

EDIT:

Come sottolineato nei commenti alla proposta di cui sopra si tradurrà in una chiamata SQL per ogni elemento della collezione newusers. Potrei confermarlo con SQL Server Profiler.

Un risultato intresting del profiling è la SQL un po 'strano generati da EF per ogni elemento (i nomi dei modelli sono diverso da quello nel PO, ma la domanda è la stessa):

exec sp_executesql N'SELECT 
CASE WHEN (EXISTS (SELECT 
    1 AS [C1] 
    FROM [dbo].[EventGroup] AS [Extent1] 
    WHERE [Extent1].[EventGroupID] = @p__linq__0 
)) THEN cast(1 as bit) WHEN (NOT EXISTS (SELECT 
    1 AS [C1] 
    FROM [dbo].[EventGroup] AS [Extent2] 
    WHERE [Extent2].[EventGroupID] = @p__linq__0 
)) THEN cast(0 as bit) END AS [C1] 
FROM (SELECT 1 AS X) AS [SingleRowTable1]',N'@p__linq__0 int',@p__linq__0=10 

Piuttosto un bel pezzo di codice per fare il lavoro di un semplice one-liner.

Il mio punto di vista è che scrivere codice dichiarativo piacevole e leggibile e lasciare che il compilatore e l'ottimizzatore facciano il lavoro sporco è una grande attitudine. Questo è uno dei casi in cui il risultato di un tale stile è sorprendente e devi sporcarti.

+0

Funziona perfettamente e deve essere eseguito una volta sola (la query where viene eseguita solo una volta sull'intera tabella, quindi per scorrere i risultati). Mi piace. – Jordan

+0

@ p.s.w.g Bella cattura, ho modificato la risposta per correggere gli errori di sintassi che ho notato. – Hari

+0

@Jordan No non funziona se 'NewUsers' è una raccolta in memoria. In tal caso deve scorrere ogni elemento e valutare "Qualsiasi" su ciascuno. Questo non migliora veramente il tuo codice originale, ma lo riorganizza in un modo più esteticamente gradevole. –

0

Il seguente metodo di estensione vi permetterà di inserire i record di qualsiasi tipo, ignorando i duplicati:

public static void AddRangeIgnore(this DbSet dbSet, IEnumerable<object> entities) 
    { 
     var entitiesList = entities.ToList(); 
     var firstEntity = entitiesList.FirstOrDefault(); 

     if (firstEntity == null || !firstEntity.HasKey() || firstEntity.HasIdentityKey()) 
     { 
      dbSet.AddRange(entitiesList); 
      return; 
     } 

     var uniqueEntities = new List<object>(); 

     using (var dbContext = _dataService.CreateDbContext()) 
     { 
      var uniqueDbSet = dbContext.Set(entitiesList.First().GetType()); 

      foreach (object entity in entitiesList) 
      { 
       var keyValues = entity.GetKeyValues(); 
       var existingEntity = uniqueDbSet.Find(keyValues); 

       if (existingEntity == null) 
       { 
        uniqueEntities.Add(entity); 
        uniqueDbSet.Attach(entity); 
       } 
      } 
     } 

     dbSet.AddRange(uniqueEntities); 
    } 

    public static object[] GetKeyValues(this object entity) 
    { 
     using (var dbContext = _dataService.CreateDbContext()) 
     { 
      var entityType = entity.GetType(); 
      dbContext.Set(entityType).Attach(entity); 
      var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity); 
      var value = objectStateEntry.EntityKey 
             .EntityKeyValues 
             .Select(kv => kv.Value) 
             .ToArray(); 
      return value; 
     } 
    } 

    public static bool HasKey(this object entity) 
    { 
     using (var dbContext = _dataService.CreateDbContext()) 
     { 
      var entityType = entity.GetType(); 
      dbContext.Set(entityType).Attach(entity); 
      var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity); 
      return objectStateEntry.EntityKey != null; 
     } 
    } 

    public static bool HasIdentityKey(this object entity) 
    { 
     using (var dbContext = _dataService.CreateDbContext()) 
     { 
      var entityType = entity.GetType(); 
      dbContext.Set(entityType).Attach(entity); 
      var objectStateEntry = ((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.GetObjectStateEntry(entity); 
      var keyPropertyName = objectStateEntry.EntityKey 
             .EntityKeyValues 
             .Select(kv => kv.Key) 
             .FirstOrDefault(); 

      if (keyPropertyName == null) 
      { 
       return false; 
      } 

      var keyProperty = entityType.GetProperty(keyPropertyName); 
      var attribute = (DatabaseGeneratedAttribute)Attribute.GetCustomAttribute(keyProperty, typeof(DatabaseGeneratedAttribute)); 
      return attribute != null && attribute.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity; 
     } 
    } 
+0

Che cos'è _dataService? – iivel

Problemi correlati