Méthode d'insertion la plus rapide dans Entity Framework


682

Je cherche le moyen le plus rapide d'insérer dans Entity Framework.

Je pose cette question en raison du scénario dans lequel vous avez un TransactionScope actif et l'insertion est énorme (4000+). Cela peut potentiellement durer plus de 10 minutes (délai d'expiration par défaut des transactions), ce qui entraînera une transaction incomplète.


1
Comment le faites-vous actuellement?
Dustin Laine

Création du TransactionScope, instanciation du DBContext, ouverture de la connexion et dans une instruction for-each faisant les insertions et SavingChanges (pour chaque enregistrement), REMARQUE: TransactionScope et DBContext utilisent des instructions et je ferme la connexion dans un enfin bloc
Bongo Sharp

Une autre réponse pour référence: stackoverflow.com/questions/5798646/…
Ladislav Mrnka

2
L'insertion la plus rapide dans une base de données SQL n'implique pas EF. AFAIK Son BCP puis TVP + Fusionner / insérer.
StingyJack

1
Pour ceux qui liront les commentaires: La réponse la plus pertinente et moderne est ici.
Tanveer Badar

Réponses:


986

À votre remarque dans les commentaires à votre question:

"... SavingChanges ( pour chaque enregistrement ) ..."

C'est la pire chose que vous puissiez faire! L'appel SaveChanges()pour chaque enregistrement ralentit considérablement les insertions en masse. Je ferais quelques tests simples qui amélioreront très probablement les performances:

  • Appelez SaveChanges()une fois après TOUS les enregistrements.
  • Appelez SaveChanges()par exemple 100 enregistrements.
  • Appelez SaveChanges()par exemple 100 enregistrements et supprimez le contexte et créez-en un nouveau.
  • Désactiver la détection des modifications

Pour les insertions en masse, je travaille et expérimente avec un modèle comme celui-ci:

using (TransactionScope scope = new TransactionScope())
{
    MyDbContext context = null;
    try
    {
        context = new MyDbContext();
        context.Configuration.AutoDetectChangesEnabled = false;

        int count = 0;            
        foreach (var entityToInsert in someCollectionOfEntitiesToInsert)
        {
            ++count;
            context = AddToContext(context, entityToInsert, count, 100, true);
        }

        context.SaveChanges();
    }
    finally
    {
        if (context != null)
            context.Dispose();
    }

    scope.Complete();
}

private MyDbContext AddToContext(MyDbContext context,
    Entity entity, int count, int commitCount, bool recreateContext)
{
    context.Set<Entity>().Add(entity);

    if (count % commitCount == 0)
    {
        context.SaveChanges();
        if (recreateContext)
        {
            context.Dispose();
            context = new MyDbContext();
            context.Configuration.AutoDetectChangesEnabled = false;
        }
    }

    return context;
}

J'ai un programme de test qui insère 560 000 entités (9 propriétés scalaires, pas de propriétés de navigation) dans la base de données. Avec ce code, il fonctionne en moins de 3 minutes.

Pour les performances, il est important d'appeler SaveChanges()après "plusieurs" enregistrements ("plusieurs" autour de 100 ou 1000). Il améliore également les performances pour éliminer le contexte après SaveChanges et en créer un nouveau. Cela efface le contexte de toutes les entités, SaveChangesne fait pas cela, les entités sont toujours attachées au contexte dans l'état Unchanged. C'est la taille croissante des entités attachées dans le contexte qui ralentit l'insertion pas à pas. Il est donc utile de l'effacer après un certain temps.

Voici quelques mesures pour mes 560000 entités:

  • commitCount = 1, recreateContext = false: plusieurs heures (c'est votre procédure actuelle)
  • commitCount = 100, recreateContext = false: plus de 20 minutes
  • commitCount = 1000, recreateContext = false: 242 sec
  • commitCount = 10000, recréerContexte = false: 202 sec
  • commitCount = 100000, recreateContext = false: 199 sec
  • commitCount = 1000000, recreateContext = false: exception de mémoire insuffisante
  • commitCount = 1, recreateContext = true: plus de 10 minutes
  • commitCount = 10, recreateContext = true: 241 sec
  • commitCount = 100, recreateContext = true: 164 sec
  • commitCount = 1000, recreateContext = true: 191 sec

Le comportement dans le premier test ci-dessus est que les performances sont très non linéaires et diminuent extrêmement au fil du temps. ("Plusieurs heures" est une estimation, je n'ai jamais terminé ce test, je me suis arrêté à 50 000 entités après 20 minutes.) Ce comportement non linéaire n'est pas si significatif dans tous les autres tests.


89
@Bongo Sharp: N'oubliez pas de régler AutoDetectChangesEnabled = false;sur le DbContext. Il a également un gros effet supplémentaire sur les performances: stackoverflow.com/questions/5943394/…
Slauma

6
Ouais, le problème est que j'utilise Entity Framework 4, et AutoDetectChangesEnabled fait partie du 4.1, néanmoins, j'ai fait le test de performance et j'ai eu des RÉSULTATS AMAZING, il est passé de 00:12:00 à 00:00:22 SavinChanges sur chaque entité faisait l'olverload ... MERCI beaucoup pour votre réponse! c'est ce que je cherchais
Bongo Sharp

10
Merci pour le context.Configuration.AutoDetectChangesEnabled = false; astuce, cela fait une énorme différence.
douglaz

1
@ dahacker89: Utilisez-vous la bonne version EF> = 4.1 et DbContextNON ObjectContext?
Slauma

3
@ dahacker89: Je vous suggère de créer une question distincte pour votre problème avec peut-être plus de détails. Je ne suis pas en mesure de comprendre ici ce qui ne va pas.
Slauma

176

Cette combinaison augmente assez bien la vitesse.

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;

46
Ne désactivez pas aveuglément ValidateOnSaveEnabled, vous pouvez dépendre de ce comportement et ne le réalisez pas avant qu'il ne soit trop tard. Là encore, vous pouvez effectuer une validation ailleurs dans le code et faire valider EF une fois de plus est complètement inutile.
Jeremy Cook

1
Lors de mon test, la sauvegarde de 20 000 lignes est passée de 101 secondes à 88 secondes. Pas beaucoup et quelles sont les implications.
AH.

27
@JeremyCook Je pense que ce que vous essayez d'obtenir, c'est que cette réponse serait bien meilleure si elle expliquait les implications possibles du changement de ces propriétés par rapport à leurs valeurs par défaut (à part l'amélioration des performances). Je suis d'accord.
pseudocoder

1
Cela a fonctionné pour moi, bien que si vous mettez à jour des enregistrements dans le contexte, vous devrez appeler explicitement DetectChanges ()
hillstuk

2
Ceux-ci peuvent être désactivés puis réactivés
yellavon

98

Le moyen le plus rapide serait d'utiliser l' extension d'insertion en vrac , que j'ai développée

note: ceci est un produit commercial, non gratuit

Il utilise SqlBulkCopy et un datareader personnalisé pour obtenir des performances maximales. En conséquence, il est plus de 20 fois plus rapide que l'utilisation de l'insertion régulière ou AddRange EntityFramework.BulkInsert vs EF AddRange

l'utilisation est extrêmement simple

context.BulkInsert(hugeAmountOfEntities);

10
Rapide mais ne fait que la couche supérieure d'une hiérarchie.
CAD bloke

66
Ce n'est pas gratuit.
Amir Saniyan

72
Les publicités deviennent plus intelligentes ... c'est un produit payant et très cher pour un indépendant. Être averti!
JulioQc

35
600 USD pour l'assistance et les mises à niveau d'un an? Es-tu fou?
Camilo Terevinto

7
Je

83

Vous devriez envisager d'utiliser le System.Data.SqlClient.SqlBulkCopypour cela. Voici la documentation , et bien sûr, il existe de nombreux tutoriels en ligne.

Désolé, je sais que vous cherchiez une réponse simple pour que EF fasse ce que vous voulez, mais les opérations en bloc ne sont pas vraiment ce à quoi les ORM sont destinés.


1
J'ai rencontré le SqlBulkCopy plusieurs fois lors de mes recherches, mais il semble être plus orienté vers les insertions table à table, malheureusement je ne m'attendais pas à des solutions faciles, mais plutôt à des conseils de performance, comme par exemple la gestion de l'état de la connexion manuelle, au lieu de laisser EF le faire pour vous
Bongo Sharp

7
J'ai utilisé SqlBulkCopy pour insérer de grandes quantités de données directement depuis mon application. Vous avez essentiellement de créer un DataTable, le remplir, puis passer que pour BulkCopy. Il y a quelques problèmes lors de la configuration de votre DataTable (dont la plupart ont malheureusement été oubliés), mais cela devrait très bien fonctionner
Adam Rackis

2
J'ai fait la preuve de concept, et comme promis, cela fonctionne très rapidement, mais l'une des raisons pour lesquelles j'utilise EF est que l'insertion de données relationnelles est plus facile, par exemple, si j'insère une entité qui contient déjà des données relationnelles , il sera également inséré, avez-vous déjà été dans ce scénario? Merci!
Bongo Sharp

2
Malheureusement, l'insertion d'une toile d'objets dans un SGBD n'est pas vraiment quelque chose que BulkCopy fera. C'est l'avantage d'un ORM comme EF, le coût étant qu'il ne s'adaptera pas pour faire efficacement des centaines de graphiques d'objets similaires.
Adam Rackis

2
SqlBulkCopy est certainement la voie à suivre si vous avez besoin d'une vitesse brute ou si vous allez relancer cet insert. J'ai déjà inséré plusieurs millions d'enregistrements auparavant et c'est extrêmement rapide. Cela dit, à moins que vous n'ayez besoin de relancer cet insert, il pourrait être plus facile d'utiliser simplement EF.
Neil

49

Je suis d'accord avec Adam Rackis. SqlBulkCopyest le moyen le plus rapide de transférer des enregistrements en vrac d'une source de données à une autre. Je l'ai utilisé pour copier 20 000 enregistrements et cela a pris moins de 3 secondes. Jetez un œil à l'exemple ci-dessous.

public static void InsertIntoMembers(DataTable dataTable)
{           
    using (var connection = new SqlConnection(@"data source=;persist security info=True;user id=;password=;initial catalog=;MultipleActiveResultSets=True;App=EntityFramework"))
    {
        SqlTransaction transaction = null;
        connection.Open();
        try
        {
            transaction = connection.BeginTransaction();
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = "Members";
                sqlBulkCopy.ColumnMappings.Add("Firstname", "Firstname");
                sqlBulkCopy.ColumnMappings.Add("Lastname", "Lastname");
                sqlBulkCopy.ColumnMappings.Add("DOB", "DOB");
                sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
                sqlBulkCopy.ColumnMappings.Add("Email", "Email");

                sqlBulkCopy.ColumnMappings.Add("Address1", "Address1");
                sqlBulkCopy.ColumnMappings.Add("Address2", "Address2");
                sqlBulkCopy.ColumnMappings.Add("Address3", "Address3");
                sqlBulkCopy.ColumnMappings.Add("Address4", "Address4");
                sqlBulkCopy.ColumnMappings.Add("Postcode", "Postcode");

                sqlBulkCopy.ColumnMappings.Add("MobileNumber", "MobileNumber");
                sqlBulkCopy.ColumnMappings.Add("TelephoneNumber", "TelephoneNumber");

                sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted");

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
        }

    }
}

1
J'ai essayé la plupart des solutions fournies dans ce post et SqlBulkCopy était de loin le plus rapide. Pure EF a pris 15 minutes, mais avec un mélange de la solution et de SqlBulkCopy j'ai pu descendre à 1,5 min! C'était avec 2 millions d'enregistrements! Sans aucune optimisation d'index DB.
jonas

La liste est plus simple que DataTable. Il existe une AsDataReader()méthode d'extension, expliquée dans cette réponse: stackoverflow.com/a/36817205/1507899
RJB

Mais ce n'est que pour l'entité supérieure, pas relationnelle
Zahid Mustafa

1
@ZahidMustafa: oui. Il s'agit de BulkInsert, et non Bulk-Analysis-And-Relation-Tracing-On-Object-Graphs. nécessaire, et vous obtiendrez une solution personnalisée rapide. Ou, vous pouvez compter sur EF pour ce faire, aucun travail de votre côté, mais plus lent à l'exécution.
quetzalcoatl

23

Je recommanderais cet article sur la façon de faire des insertions en masse à l'aide d'EF.

Entity Framework et INSERTS en vrac lents

Il explore ces domaines et compare les performances:

  1. EF par défaut (57 minutes pour terminer l'ajout de 30 000 enregistrements)
  2. Remplacement par le code ADO.NET (25 secondes pour ces mêmes 30 000)
  3. Contournement du contexte - Gardez le graphique de contexte actif petit en utilisant un nouveau contexte pour chaque unité de travail (les mêmes 30 000 insertions prennent 33 secondes)
  4. Grandes listes - Désactivez AutoDetectChangesEnabled (réduit le temps à environ 20 secondes)
  5. Batching (jusqu'à 16 secondes)
  6. DbTable.AddRange () - (les performances sont de l'ordre de 12)

21

comme cela n'a jamais été mentionné ici, je veux recommander EFCore.BulkExtensions ici

context.BulkInsert(entitiesList);                 context.BulkInsertAsync(entitiesList);
context.BulkUpdate(entitiesList);                 context.BulkUpdateAsync(entitiesList);
context.BulkDelete(entitiesList);                 context.BulkDeleteAsync(entitiesList);
context.BulkInsertOrUpdate(entitiesList);         context.BulkInsertOrUpdateAsync(entitiesList);         // Upsert
context.BulkInsertOrUpdateOrDelete(entitiesList); context.BulkInsertOrUpdateOrDeleteAsync(entitiesList); // Sync
context.BulkRead(entitiesList);                   context.BulkReadAsync(entitiesList);

1
J'appuie cette suggestion. Après avoir essayé de nombreuses solutions homebrew, cela a réduit mon insert à 1 seconde au lieu de plus de 50 secondes. Et, c'est une licence MIT si facile à intégrer.
SouthShoreAK

est-ce disponible pour ef 6.x
Alok

c'est seulement plus performant que d'utiliser AddRange si c'est plus de 10 entités
Jackal

5
10 000 inserts sont passés de 9 minutes à 12 secondes. Cela mérite plus d'attention!
callisto

2
S'il existe un moyen de modifier les réponses acceptées, cela devrait être la réponse acceptée moderne maintenant. Et je souhaite que l'équipe EF fournisse cela hors de la boîte.
Tanveer Badar

18

J'ai enquêté sur la réponse de Slauma (ce qui est génial, merci pour l'idée homme), et j'ai réduit la taille des lots jusqu'à ce que j'atteigne la vitesse optimale. En regardant les résultats de Slauma:

  • commitCount = 1, recreateContext = true: plus de 10 minutes
  • commitCount = 10, recreateContext = true: 241 sec
  • commitCount = 100, recreateContext = true: 164 sec
  • commitCount = 1000, recreateContext = true: 191 sec

Il est visible qu'il y a une augmentation de la vitesse lors du passage de 1 à 10 et de 10 à 100, mais de 100 à 1000, la vitesse d'insertion diminue à nouveau.

Je me suis donc concentré sur ce qui se passe lorsque vous réduisez la taille du lot à une valeur comprise entre 10 et 100, et voici mes résultats (j'utilise différents contenus de ligne, donc mes temps ont une valeur différente):

Quantity    | Batch size    | Interval
1000    1   3
10000   1   34
100000  1   368

1000    5   1
10000   5   12
100000  5   133

1000    10  1
10000   10  11
100000  10  101

1000    20  1
10000   20  9
100000  20  92

1000    27  0
10000   27  9
100000  27  92

1000    30  0
10000   30  9
100000  30  92

1000    35  1
10000   35  9
100000  35  94

1000    50  1
10000   50  10
100000  50  106

1000    100 1
10000   100 14
100000  100 141

Sur la base de mes résultats, l'optimum réel est d'environ 30 pour la taille du lot. C'est moins que 10 et 100. Le problème, c'est que je n'ai aucune idée de pourquoi 30 est optimal, et je n'aurais pas pu trouver d'explication logique à cela.


2
J'ai trouvé la même chose avec Postrges et SQL pur (cela dépend de SQL et non d'EF) que 30 est optimal.
Kamil Gareev

D'après mon expérience, l'optimum diffère en fonction de la vitesse de connexion et de la taille de la ligne. Pour une connexion rapide et de petites rangées, l'optimum peut être égal à> 200 rangées.
jing

18

Comme d'autres l'ont dit, SqlBulkCopy est le moyen de le faire si vous voulez de très bonnes performances d'insertion.

C'est un peu lourd à mettre en œuvre mais il existe des bibliothèques qui peuvent vous aider. Il y en a quelques-uns mais je débrancherai sans vergogne ma propre bibliothèque cette fois: https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

Le seul code dont vous auriez besoin est:

 using (var db = new YourDbContext())
 {
     EFBatchOperation.For(db, db.BlogPosts).InsertAll(list);
 }

Alors, c'est beaucoup plus rapide? Très difficile à dire parce que cela dépend de nombreux facteurs, la performance des ordinateurs, réseau, la taille des objets etc etc Les tests de performance que j'ai fait suggère 25k entités peuvent être insérées à environ 10s la manière standard sur localhost Si vous optimisez votre configuration EF comme mentionné dans les autres réponses. Avec EFUtilities, cela prend environ 300 ms. Encore plus intéressant, j'ai enregistré environ 3 millions d'entités en moins de 15 secondes en utilisant cette méthode, en moyenne environ 200 000 entités par seconde.

Le seul problème est bien sûr si vous devez insérer des données recréées. Cela peut être fait efficacement sur le serveur SQL à l'aide de la méthode ci-dessus, mais cela nécessite que vous ayez une stratégie de génération d'ID qui vous permette de générer des ID dans le code d'application pour le parent afin que vous puissiez définir les clés étrangères. Cela peut être fait en utilisant des GUID ou quelque chose comme la génération d'ID HiLo.


Fonctionne bien. La syntaxe est cependant un peu bavarde. Pensez qu'il serait préférable d' EFBatchOperationavoir un constructeur que vous transmettez DbContextà plutôt que de passer à chaque méthode statique. Des versions génériques de InsertAllet UpdateAllqui trouvent automatiquement la collection, similaires à DbContext.Set<T>, seraient également bonnes.
kjbartel

Juste un petit commentaire pour dire merci! Ce code m'a permis de sauvegarder 170k enregistrements en 1,5 seconde! Souffle complètement toute autre méthode que j'ai essayée hors de l'eau.
Tom Glenn

@Mikael Un problème concerne les champs d'identité. Avez-vous encore un moyen d'activer l'insertion d'identité?
Joe Phillips

1
Contrairement à EntityFramework.BulkInsert, cette bibliothèque est restée gratuite. +1
Rudey

14

Dispose()le contexte crée des problèmes si les entités Add()sur lesquelles vous comptez sur d'autres entités préchargées (par exemple, les propriétés de navigation) dans le contexte

J'utilise un concept similaire pour garder mon contexte petit pour obtenir les mêmes performances

Mais au lieu du Dispose()contexte et recréer, je détache simplement les entités qui déjàSaveChanges()

public void AddAndSave<TEntity>(List<TEntity> entities) where TEntity : class {

const int CommitCount = 1000; //set your own best performance number here
int currentCount = 0;

while (currentCount < entities.Count())
{
    //make sure it don't commit more than the entities you have
    int commitCount = CommitCount;
    if ((entities.Count - currentCount) < commitCount)
        commitCount = entities.Count - currentCount;

    //e.g. Add entities [ i = 0 to 999, 1000 to 1999, ... , n to n+999... ] to conext
    for (int i = currentCount; i < (currentCount + commitCount); i++)        
        _context.Entry(entities[i]).State = System.Data.EntityState.Added;
        //same as calling _context.Set<TEntity>().Add(entities[i]);       

    //commit entities[n to n+999] to database
    _context.SaveChanges();

    //detach all entities in the context that committed to database
    //so it won't overload the context
    for (int i = currentCount; i < (currentCount + commitCount); i++)
        _context.Entry(entities[i]).State = System.Data.EntityState.Detached;

    currentCount += commitCount;
} }

envelopper avec try catch et TrasactionScope()si vous en avez besoin, ne pas les montrer ici pour garder le code propre


1
Cela a ralenti l'insertion (AddRange) à l'aide d'Entity Framework 6.0. L'insertion de 20 000 lignes est passée d'environ 101 secondes à 118 secondes.
AH.

1
@Stephen Ho: J'essaie également d'éviter de disposer de mon contexte. Je peux comprendre que c'est plus lent que de recréer le contexte, mais je veux savoir si vous l'avez trouvé assez rapidement plutôt que de ne pas recréer le contexte mais avec un ensemble commitCount.
Apprenant

@Learner: Je pense que c'était plus rapide que de recréer le contexte. Mais je ne me souviens plus vraiment maintenant parce que je suis enfin passé à SqlBulkCopy.
Stephen Ho

J'ai fini par devoir utiliser cette technique parce que, pour une raison étrange, il y avait un suivi restant lors du deuxième passage dans la boucle while, même si j'avais tout enveloppé dans une instruction using et même appelé Dispose () sur le DbContext . Lorsque j'ajouterais au contexte (lors de la 2e passe), le nombre de jeux de contexte passerait à 6 au lieu d'un seul. Les autres éléments qui ont été ajoutés arbitrairement avaient déjà été insérés lors du premier passage dans la boucle while, de sorte que l'appel à SaveChanges échouerait lors du deuxième passage (pour des raisons évidentes).
Hallmanac

9

Je sais que c'est une très vieille question, mais un gars ici a dit avoir développé une méthode d'extension pour utiliser l'insertion en vrac avec EF, et quand j'ai vérifié, j'ai découvert que la bibliothèque coûte 599 $ aujourd'hui (pour un développeur). Peut-être que cela a du sens pour toute la bibliothèque, mais pour l'insertion en bloc, c'est trop.

Voici une méthode d'extension très simple que j'ai faite. Je l'utilise d'abord sur une paire avec une base de données (ne testez pas d'abord avec le code, mais je pense que cela fonctionne de la même manière). Changez YourEntitiesavec le nom de votre contexte:

public partial class YourEntities : DbContext
{
    public async Task BulkInsertAllAsync<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            await conn.OpenAsync();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            await bulkCopy.WriteToServerAsync(table);
        }
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            conn.Open();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            foreach (var property in properties)
            {
                Type propertyType = property.PropertyType;
                if (propertyType.IsGenericType &&
                    propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    propertyType = Nullable.GetUnderlyingType(propertyType);
                }

                table.Columns.Add(new DataColumn(property.Name, propertyType));
            }

            foreach (var entity in entities)
            {
                table.Rows.Add(
                    properties.Select(property => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            bulkCopy.WriteToServer(table);
        }
    }

    public string GetTableName(Type type)
    {
        var metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));

        var entityType = metadata
                .GetItems<EntityType>(DataSpace.OSpace)
                .Single(e => objectItemCollection.GetClrType(e) == type);

        var entitySet = metadata
            .GetItems<EntityContainer>(DataSpace.CSpace)
            .Single()
            .EntitySets
            .Single(s => s.ElementType.Name == entityType.Name);

        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                .Single()
                .EntitySetMappings
                .Single(s => s.EntitySet == entitySet);

        var table = mapping
            .EntityTypeMappings.Single()
            .Fragments.Single()
            .StoreEntitySet;

        return (string)table.MetadataProperties["Table"].Value ?? table.Name;
    }
}

Vous pouvez l'utiliser contre n'importe quelle collection qui hérite de IEnumerable, comme ça:

await context.BulkInsertAllAsync(items);

veuillez compléter votre exemple de code. where is bulkCopy
Seabizkit

1
C'est déjà là:await bulkCopy.WriteToServerAsync(table);
Guilherme

Peut-être que je n'étais pas clair, dans votre écriture, vous suggérez que vous avez fait une extension ... ce que j'ai supposé signifier qu'aucune bibliothèque de 3e partie n'était nécessaire, alors qu'en fait dans les deux méthodes, utilisez SqlBulkCopy lib. Cela repose entièrement sur SqlBulkCopy, quand j'ai demandé d'où vient bulkCopy, c'est une extension lib dont vous avez écrit une extension lib par-dessus. Serait tout simplement plus logique de dire ici comment j'ai utilisé la lib SqlBulkCopy.
Seabizkit

devrait utiliser conn.OpenAsync dans la version async
Robert

6

Essayez d'utiliser une procédure stockée qui obtiendra un XML des données que vous souhaitez insérer.


9
La transmission de données au format XML n'est pas nécessaire si vous ne souhaitez pas les stocker au format XML. Dans SQL 2008, vous pouvez utiliser un paramètre de valeur de table.
Ladislav Mrnka

je n'ai pas clarifié cela, mais je dois également prendre en charge SQL 2005
Bongo Sharp

4

J'ai fait une extension générique de l'exemple de @Slauma ci-dessus;

public static class DataExtensions
{
    public static DbContext AddToContext<T>(this DbContext context, object entity, int count, int commitCount, bool recreateContext, Func<DbContext> contextCreator)
    {
        context.Set(typeof(T)).Add((T)entity);

        if (count % commitCount == 0)
        {
            context.SaveChanges();
            if (recreateContext)
            {
                context.Dispose();
                context = contextCreator.Invoke();
                context.Configuration.AutoDetectChangesEnabled = false;
            }
        }
        return context;
    }
}

Usage:

public void AddEntities(List<YourEntity> entities)
{
    using (var transactionScope = new TransactionScope())
    {
        DbContext context = new YourContext();
        int count = 0;
        foreach (var entity in entities)
        {
            ++count;
            context = context.AddToContext<TenancyNote>(entity, count, 100, true,
                () => new YourContext());
        }
        context.SaveChanges();
        transactionScope.Complete();
    }
}

4

Je cherche le moyen le plus rapide d'insérer dans Entity Framework

Certaines bibliothèques tierces prenant en charge l'insertion en bloc sont disponibles:

  • Z.EntityFramework.Extensions ( recommandé )
  • EFUtilities
  • EntityFramework.BulkInsert

Voir: Bibliothèque d'insertion en bloc Entity Framework

Soyez prudent lorsque vous choisissez une bibliothèque d'insertion en bloc. Seules les extensions Entity Framework prennent en charge toutes sortes d'associations et d'héritages et c'est la seule encore prise en charge.


Avis de non - responsabilité : je suis le propriétaire des extensions d' entité Framework

Cette bibliothèque vous permet d'effectuer toutes les opérations en bloc dont vous avez besoin pour vos scénarios:

  • Sauvegardes groupées
  • Insert en vrac
  • Suppression groupée
  • Mise à jour en masse
  • Fusion en masse

Exemple

// Easy to use
context.BulkSaveChanges();

// Easy to customize
context.BulkSaveChanges(bulk => bulk.BatchSize = 100);

// Perform Bulk Operations
context.BulkDelete(customers);
context.BulkInsert(customers);
context.BulkUpdate(customers);

// Customize Primary Key
context.BulkMerge(customers, operation => {
   operation.ColumnPrimaryKeyExpression = 
        customer => customer.Code;
});

19
c'est une grande extension mais pas gratuite .
Okan Kocyigit

2
Cette réponse est assez bonne et EntityFramework.BulkInsert effectue une insertion en masse de 15K lignes en 1,5 seconde, fonctionne plutôt bien pour un processus interne comme un service Windows.
Pastor Cortes

4
Oui, 600 $ pour l'encart en vrac. Ça vaut vraiment le coup.
eocron

1
@eocron Yeat ça vaut le coup si vous l'utilisez commercialement. Je ne vois aucun problème avec 600 $ pour quelque chose que je n'ai pas à passer des heures à le construire moi-même, ce qui me coûtera beaucoup plus de 600 $. Oui ça coûte de l'argent mais en regardant mon taux horaire c'est de l'argent bien dépensé!
Jordy van Eijk

3

Utilisation SqlBulkCopy:

void BulkInsert(GpsReceiverTrack[] gpsReceiverTracks)
{
    if (gpsReceiverTracks == null)
    {
        throw new ArgumentNullException(nameof(gpsReceiverTracks));
    }

    DataTable dataTable = new DataTable("GpsReceiverTracks");
    dataTable.Columns.Add("ID", typeof(int));
    dataTable.Columns.Add("DownloadedTrackID", typeof(int));
    dataTable.Columns.Add("Time", typeof(TimeSpan));
    dataTable.Columns.Add("Latitude", typeof(double));
    dataTable.Columns.Add("Longitude", typeof(double));
    dataTable.Columns.Add("Altitude", typeof(double));

    for (int i = 0; i < gpsReceiverTracks.Length; i++)
    {
        dataTable.Rows.Add
        (
            new object[]
            {
                    gpsReceiverTracks[i].ID,
                    gpsReceiverTracks[i].DownloadedTrackID,
                    gpsReceiverTracks[i].Time,
                    gpsReceiverTracks[i].Latitude,
                    gpsReceiverTracks[i].Longitude,
                    gpsReceiverTracks[i].Altitude
            }
        );
    }

    string connectionString = (new TeamTrackerEntities()).Database.Connection.ConnectionString;
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns)
                {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
    }

    return;
}

3

L'un des moyens les plus rapides pour enregistrer une liste, vous devez appliquer le code suivant

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;

AutoDetectChangesEnabled = false

Add, AddRange & SaveChanges: ne détecte pas les changements.

ValidateOnSaveEnabled = false;

Ne détecte pas le suivi des modifications

Vous devez ajouter nuget

Install-Package Z.EntityFramework.Extensions

Vous pouvez maintenant utiliser le code suivant

var context = new MyContext();

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;

context.BulkInsert(list);
context.BulkSaveChanges();

puis-je utiliser votre exemple de code pour la mise à jour en masse?
AminGolmahalle

4
La bibliothèque Z n'est pas gratuite
SHADOW.NET

3

SqlBulkCopy est super rapide

Voici ma mise en œuvre:

// at some point in my calling code, I will call:
var myDataTable = CreateMyDataTable();
myDataTable.Rows.Add(Guid.NewGuid,tableHeaderId,theName,theValue); // e.g. - need this call for each row to insert

var efConnectionString = ConfigurationManager.ConnectionStrings["MyWebConfigEfConnection"].ConnectionString;
var efConnectionStringBuilder = new EntityConnectionStringBuilder(efConnectionString);
var connectionString = efConnectionStringBuilder.ProviderConnectionString;
BulkInsert(connectionString, myDataTable);

private DataTable CreateMyDataTable()
{
    var myDataTable = new DataTable { TableName = "MyTable"};
// this table has an identity column - don't need to specify that
    myDataTable.Columns.Add("MyTableRecordGuid", typeof(Guid));
    myDataTable.Columns.Add("MyTableHeaderId", typeof(int));
    myDataTable.Columns.Add("ColumnName", typeof(string));
    myDataTable.Columns.Add("ColumnValue", typeof(string));
    return myDataTable;
}

private void BulkInsert(string connectionString, DataTable dataTable)
{
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlTransaction transaction = null;
        try
        {
            transaction = connection.BeginTransaction();

            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns) {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction?.Rollback();
            throw;
        }
    }
}

3

[Mise à jour 2019] EF Core 3.1

Suivant ce qui a été dit ci-dessus, la désactivation d'AutoDetectChangesEnabled dans EF Core a parfaitement fonctionné: le temps d'insertion a été divisé par 100 (de plusieurs minutes à quelques secondes, 10 000 enregistrements avec des relations de tables croisées)

Le code mis à jour est:

  context.ChangeTracker.AutoDetectChangesEnabled = false;
            foreach (IRecord record in records) {
               //Add records to your database        
            }
            context.ChangeTracker.DetectChanges();
            context.SaveChanges();
            context.ChangeTracker.AutoDetectChangesEnabled = true; //do not forget to re-enable

2

Voici une comparaison des performances entre l'utilisation d'Entity Framework et l'utilisation de la classe SqlBulkCopy sur un exemple réaliste: Comment insérer en bloc des objets complexes dans la base de données SQL Server

Comme d'autres l'ont déjà souligné, les ORM ne sont pas destinés à être utilisés dans des opérations en vrac. Ils offrent flexibilité, séparation des préoccupations et autres avantages, mais les opérations en bloc (sauf la lecture en bloc) n'en font pas partie.


2

Une autre option consiste à utiliser SqlBulkTools disponible auprès de Nuget. Il est très facile à utiliser et possède des fonctionnalités puissantes.

Exemple:

var bulk = new BulkOperations();
var books = GetBooks();

using (TransactionScope trans = new TransactionScope())
{
    using (SqlConnection conn = new SqlConnection(ConfigurationManager
    .ConnectionStrings["SqlBulkToolsTest"].ConnectionString))
    {
        bulk.Setup<Book>()
            .ForCollection(books)
            .WithTable("Books") 
            .AddAllColumns()
            .BulkInsert()
            .Commit(conn);
    }

    trans.Complete();
}

Voir la documentation pour plus d'exemples et une utilisation avancée. Avertissement: je suis l'auteur de cette bibliothèque et toutes les opinions sont de mon avis.


2
Ce projet a été supprimé de NuGet et GitHub.
0xced

1

Selon ma connaissance , il est no BulkInserten EntityFrameworkaugmenter les performances des inserts énormes.

Dans ce scénario , vous pouvez aller avec SqlBulkCopy dans ADO.netpour résoudre votre problème


Je jetais un œil à cette classe, mais elle semble être plus orientée vers les insertions table à table, n'est-ce pas?
Bongo Sharp

Vous ne savez pas ce que vous voulez dire, il y a une surcharge WriteToServerqui prend un DataTable.
Blindy

non, vous pouvez également insérer des objets .Net dans SQL. Que recherchez-vous?
anishMarokey

Un moyen d'insérer potentiellement des milliers d'enregistrements dans la base de données dans un bloc TransactionScope
Bongo Sharp

vous pouvez utiliser .Net TransactionScope technet.microsoft.com/en-us/library/bb896149.aspx
anishMarokey

1

Avez-vous déjà essayé d'insérer un travail ou une tâche en arrière-plan?

Dans mon cas, j'insère 7760 registres, répartis dans 182 tables différentes avec des relations de clé étrangère (par NavigationProperties).

Sans la tâche, cela a pris 2 minutes et demie. Dans une tâche ( Task.Factory.StartNew(...)), cela a pris 15 secondes.

Je ne fais SaveChanges()qu'après avoir ajouté toutes les entités au contexte. (pour assurer l'intégrité des données)


2
Je suis presque sûr que le contexte n'est pas sûr pour les threads. Avez-vous des tests pour vous assurer que toutes les entités ont été enregistrées?
Danny Varod

Je sais que le cadre d'entité entier n'est pas du tout sûr pour les threads, mais je viens d'ajouter les objets au contexte et d'enregistrer à la fin ... Son fonctionne parfaitement ici.
Rafael AMS du

Donc, vous appelez DbContext.SaveChanges () dans le thread principal, mais l'ajout d'entités au contexte est effectué dans le thread d'arrière-plan, non?
Prokurors

1
Oui, ajoutez des données à l'intérieur des threads; attendez que tout se termine; et enregistrer les modifications dans le fil principal
Rafael AMS

Bien que je pense que cette voie est dangereuse et sujette aux erreurs, je la trouve très intéressante.
Apprenant le

1

Toutes les solutions écrites ici n'aident pas car lorsque vous effectuez SaveChanges (), les instructions d'insertion sont envoyées à la base de données une par une, c'est ainsi que fonctionne Entity.

Et si votre trajet vers la base de données et retour est de 50 ms par exemple, le temps nécessaire pour l'insertion est le nombre d'enregistrements x 50 ms.

Vous devez utiliser BulkInsert, voici le lien: https://efbulkinsert.codeplex.com/

J'ai obtenu un temps d'insertion réduit de 5-6 minutes à 10-12 secondes en l'utilisant.



1

[NOUVELLE SOLUTION POUR POSTGRESQL] Hé, je sais que c'est un article assez ancien, mais j'ai récemment rencontré un problème similaire, mais nous utilisions Postgresql. Je voulais utiliser un bulkinsert efficace, ce qui s'est avéré assez difficile. Je n'ai trouvé aucune bibliothèque gratuite appropriée pour le faire sur cette base de données. Je n'ai trouvé que cette aide: https://bytefish.de/blog/postgresql_bulk_insert/ qui est également sur Nuget. J'ai écrit un petit mappeur, qui mappe automatiquement les propriétés comme Entity Framework:

public static PostgreSQLCopyHelper<T> CreateHelper<T>(string schemaName, string tableName)
        {
            var helper = new PostgreSQLCopyHelper<T>("dbo", "\"" + tableName + "\"");
            var properties = typeof(T).GetProperties();
            foreach(var prop in properties)
            {
                var type = prop.PropertyType;
                if (Attribute.IsDefined(prop, typeof(KeyAttribute)) || Attribute.IsDefined(prop, typeof(ForeignKeyAttribute)))
                    continue;
                switch (type)
                {
                    case Type intType when intType == typeof(int) || intType == typeof(int?):
                        {
                            helper = helper.MapInteger("\"" + prop.Name + "\"",  x => (int?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type stringType when stringType == typeof(string):
                        {
                            helper = helper.MapText("\"" + prop.Name + "\"", x => (string)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type dateType when dateType == typeof(DateTime) || dateType == typeof(DateTime?):
                        {
                            helper = helper.MapTimeStamp("\"" + prop.Name + "\"", x => (DateTime?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type decimalType when decimalType == typeof(decimal) || decimalType == typeof(decimal?):
                        {
                            helper = helper.MapMoney("\"" + prop.Name + "\"", x => (decimal?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type doubleType when doubleType == typeof(double) || doubleType == typeof(double?):
                        {
                            helper = helper.MapDouble("\"" + prop.Name + "\"", x => (double?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type floatType when floatType == typeof(float) || floatType == typeof(float?):
                        {
                            helper = helper.MapReal("\"" + prop.Name + "\"", x => (float?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type guidType when guidType == typeof(Guid):
                        {
                            helper = helper.MapUUID("\"" + prop.Name + "\"", x => (Guid)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                }
            }
            return helper;
        }

Je l'utilise de la manière suivante (j'avais une entité nommée Entreprise):

var undertakingHelper = BulkMapper.CreateHelper<Model.Undertaking>("dbo", nameof(Model.Undertaking));
undertakingHelper.SaveAll(transaction.UnderlyingTransaction.Connection as Npgsql.NpgsqlConnection, undertakingsToAdd));

J'ai montré un exemple de transaction, mais cela peut aussi être fait avec une connexion normale récupérée du contexte. enterprisesToAdd est dénombrable des enregistrements d'entité normaux, que je veux insérer en masse dans DB.

Cette solution, à laquelle je suis arrivé après quelques heures de recherche et d'essais, est comme vous pouvez vous attendre beaucoup plus rapide et enfin facile à utiliser et gratuite! Je vous conseille vraiment d'utiliser cette solution, non seulement pour les raisons mentionnées ci-dessus, mais aussi parce que c'est la seule avec laquelle je n'ai eu aucun problème avec Postgresql lui-même, de nombreuses autres solutions fonctionnent parfaitement, par exemple avec SqlServer.


0

Le secret est d'insérer dans une table de transfert vierge identique. Les inserts s'éclaircissent rapidement. Exécutez ensuite un seul insert à partir de cela dans votre grande table principale. Tronquez ensuite la table intermédiaire prête pour le prochain lot.

c'est à dire.

insert into some_staging_table using Entity Framework.

-- Single insert into main table (this could be a tiny stored proc call)
insert into some_main_already_large_table (columns...)
   select (columns...) from some_staging_table
truncate table some_staging_table

À l'aide d'EF, ajoutez tous vos enregistrements à une table intermédiaire vide. Utilisez ensuite SQL pour insérer dans la table principale (grande et lente) dans une seule instruction SQL. Videz ensuite votre table de scène. C'est un moyen très rapide d'insérer beaucoup de données dans une table déjà volumineuse.
Simon Hughes

13
Lorsque vous dites utiliser EF, ajoutez les enregistrements à la table intermédiaire, avez-vous réellement essayé cela avec EF? Puisque EF émet un appel distinct à la base de données avec chaque insert, je pense que vous allez voir le même coup de perf que l'OP essaie d'éviter. Comment la table intermédiaire évite-t-elle ce problème?
Jim Wooley

-1

Mais, pour plus de (+4000) insertions, je recommande d'utiliser la procédure stockée. joint le temps écoulé. Je l'ai inséré 11,788 lignes en 20 "entrez la description de l'image ici

c'est le code

 public void InsertDataBase(MyEntity entity)
    {
        repository.Database.ExecuteSqlCommand("sp_mystored " +
                "@param1, @param2"
                 new SqlParameter("@param1", entity.property1),
                 new SqlParameter("@param2", entity.property2));
    }

-1

Utilisez une procédure stockée qui prend les données d'entrée sous forme de XML pour insérer des données.

À partir de votre code c #, insérez des données au format xml.

par exemple en c #, la syntaxe serait comme ceci:

object id_application = db.ExecuteScalar("procSaveApplication", xml)

-7

Utilisez cette technique pour augmenter la vitesse d'insertion des enregistrements dans Entity Framework. Ici, j'utilise une procédure stockée simple pour insérer les enregistrements. Et pour exécuter cette procédure stockée, j'utilise la méthode .FromSql () d'Entity Framework qui exécute le SQL brut.

Le code de procédure stockée:

CREATE PROCEDURE TestProc
@FirstParam VARCHAR(50),
@SecondParam VARCHAR(50)

AS
  Insert into SomeTable(Name, Address) values(@FirstParam, @SecondParam) 
GO

Ensuite, parcourez tous vos 4000 enregistrements et ajoutez le code Entity Framework qui exécute le stocké

la procédure se répète toutes les 100 boucles.

Pour cela, je crée une requête de chaîne pour exécuter cette procédure, continuez à lui ajouter tous les enregistrements.

Vérifiez ensuite que la boucle s'exécute par multiples de 100 et, dans ce cas, exécutez-la en utilisant .FromSql().

Donc, pour 4000 enregistrements, je n'ai à exécuter la procédure que pour 4000/100 = 40 fois .

Vérifiez le code ci-dessous:

string execQuery = "";
var context = new MyContext();
for (int i = 0; i < 4000; i++)
{
    execQuery += "EXEC TestProc @FirstParam = 'First'" + i + "'', @SecondParam = 'Second'" + i + "''";

    if (i % 100 == 0)
    {
        context.Student.FromSql(execQuery);
        execQuery = "";
    }
}

Cela peut être efficace mais équivalent à NE PAS utiliser le framework d'entité. La question du PO était de savoir comment maximiser l'efficacité dans le contexte d'Entity Framework
kall2sollies
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.