2013-08-07 16 views
17

Ich benutze ASP.NET MVC4 mit Entity Framework Code zuerst. Ich habe eine Tabelle namens "Benutzer" mit Primärschlüssel "UserId". Diese Tabelle kann 200.000 Einträge enthalten.Ignorieren Sie doppelte Schlüssel einfügen mit Entity Framework

Ich muss noch 50 Benutzer einfügen. Ich könnte dies wie

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

Das Problem ist, einer oder mehrere dieser neuen Benutzer möglicherweise bereits in der DB vorhanden. Wenn ich sie hinzufüge und dann versuche zu speichern, wird ein Fehler ausgegeben und keine der gültigen hinzugefügt. Ich könnte den Code ändern, um dies zu tun:

was funktionieren würde. Das Problem ist, dass es 50 Mal eine Abfrage in einer 200.000+ Eintragstabelle ausführen muss. Also meine Frage ist, was ist die leistungsfähigste Methode zum Einfügen dieser Benutzer, ignorieren alle Duplikate?

+2

'context.AddOrUpdate (Benutzer);' ist, was Sie –

+1

@OO wollen Benutzer Es ist nicht wirklich, es ist verschiedene Daten, die von einer API abgerufen werden. Die API kann dieselben Daten in mehreren sequenziellen Aufrufen bereitstellen oder nicht. Ich habe nur "Benutzer" verwendet, weil es das erste Beispiel war, an das ich dachte. – Jordan

+1

Ich hatte das gleiche Problem und fand keine angemessene Lösung. Hierfür gibt es gültige Szenarios, z. B. wenn Sie einen Massenimport aus CSV durchführen, bei dem die E-Mail-Adresse in einer vorhandenen Datenbank eindeutig sein muss. Das Lesen aller vorhandenen Schlüssel in den Speicher scheint für die Leistung nicht gut zu sein, und auch nicht jeden Eintrag separat hinzuzufügen. Es scheint, dass etwas wie INSERT IGNORE benötigt wird. – acarlon

Antwort

6

Sie können dies tun:

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(); 

Dies wird eine einzelne Abfrage in der Datenbank ausführen Benutzer zu finden, die bereits vorhanden ist, dann filtert sie aus Ihrem NewUsers Set.

+0

Wäre es für große Sammlungen besser, HashSets anstelle von .ToArray() zu verwenden? – tbmsu

+1

@tbmsu Für Entity-Framework-Aufrufe würde es wahrscheinlich keinen Performance-Effekt haben, da das 'Contains' tatsächlich in eine SQL' IN'-Klausel übersetzt wird, so dass es mehr als alles andere von der DB-Performance abhängig ist. Beachten Sie auch, dass dies für * große * Datensätze wahrscheinlich überhaupt nicht funktioniert, da die Anzahl der Werte, die Sie in einer 'IN'-Klausel verwenden können, begrenzt ist ([ref] (http://stackoverflow.com/questions)/1069415/Limit-auf-dem-wo-Col-in-Bedingung)). –

2

Da dies Ihr Primärschlüssel ist, sind Ihre Möglichkeiten begrenzt. Wenn dies nicht Ihr primärer Schlüssel und nur ein eindeutiger Index wäre, könnten Sie unter der Annahme von SQL Server Ihren eindeutigen Schlüssel einrichten, um Duplikate zu ignorieren.

Was ich vorschlagen könnte ist, einfach ein try/catch um das Hinzufügen und essen die Ausnahme, wenn die Ausnahme eine doppelte Schlüsselfehler ist.

Sie können auch sehen, ob Ihr Objekt die AddOrUpdate() Methode unterstützt. Ich weiß, dass dies in Code First-Implementierungen unterstützt wird. Ich glaube in diesem Fall wird es ein neues hinzufügen oder aktualisieren, wenn die Zeile existiert. Dies kann jedoch immer noch eine Reise in die DB beinhalten, um festzustellen, ob der Benutzer bereits existiert, um zu wissen, ob ein Hinzufügen oder Aktualisieren durchgeführt werden soll. In einigen Fällen möchten Sie möglicherweise kein Update durchführen.

Ich denke, wenn ich ich wäre, würde ich die Try/Catch-Route gehen.

+0

Sie sagen also, die SaveChanges() mit einem try/catch um es in der for-Schleife. Wäre das nicht viel Aufwand von 50 verschiedenen SaveChanges() - Aufrufen und ihren entsprechenden Transaktionen und Schreibvorgängen? – Jordan

+0

@ Jordan - Bezüglich der Transaktionen sollte es nur eine Transaktion geben. Sie sollten hier nicht mehrere Transaktionen durchführen. In Bezug auf den Overhead, ja, es wird einige geben. Aber wird es größer sein als 50 (oder was auch immer) separate Aufrufe zu machen, um zu sehen, ob jeder Benutzer zuerst existiert? Der beste Weg, um herauszufinden, ist es zu testen und zu sehen. Persönlich denke ich, dass ich lieber den Overhead des Speicherns mit Try/Catch nehmen würde, als wiederholte DB-Anrufe. Aber es hängt davon ab, wie viele Benutzer Sie gleichzeitig eingeben. –

3

Sie können

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

EDIT die bestehenden Benutzer mit einer Abfrage filtern:

Wie in den Kommentaren über den Vorschlag darauf hingewiesen wird für jedes Element in der newusers Sammlung in einem SQL-Aufruf kommen. Ich konnte das mit SQL Server Profiler bestätigen.

Ein intresting Ergebnis der Profilierung ist die etwas wierd SQL erzeugt von EF für jedes Element (Modellnamen sind anders als in dem OP, aber die Abfrage ist gleich):

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 

ein recht schönes Stück von Code, um die Arbeit eines einfachen Einzeilers zu erledigen.

Meine Sicht ist, dass das Schreiben von schönen und lesbaren deklarativen Code und lassen Sie den Compiler und Optimierer machen den schmutzigen Job ist eine gute Einstellung. Dies ist einer der Fälle, wenn das Ergebnis eines solchen Stils überraschend ist und Sie schmutzig gehen müssen.

+0

Funktioniert perfekt und muss nur einmal ausgeführt werden (die Abfrage, bei der die Abfrage nur einmal für die gesamte Tabelle ausgeführt wird, und anschließend die Ergebnisse durchläuft). Ich mag das. – Jordan

+0

@ p.s.w.g Netter Haken, ich habe die Antwort bearbeitet, um die Syntaxfehler zu beheben, die ich bemerkt habe. – Hari

+0

@Jordan Nein, wenn 'NewUsers' eine In-Memory-Sammlung ist. In diesem Fall muss es jedes Element durchlaufen und die Any bewerten. Dies verbessert Ihren ursprünglichen Code nicht wirklich, sondern arrangiert ihn nur auf ästhetisch ansprechende Weise. –

0

Die folgende Erweiterung Methode, die Sie Aufzeichnungen jeder Art einfügen können, während Duplikate zu ignorieren:

vielleicht
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

Was ist _dataService? – iivel

Verwandte Themen