web-dev-qa-db-fra.com

Amélioration des performances des insertions en bloc dans Entity Framework

Je veux insérer 20000 enregistrements dans un cadre table par entité et cela prend environ 2 min. Existe-t-il un autre moyen que d’utiliser SP pour améliorer ses performances? Ceci est mon code: 

 foreach (Employees item in sequence)
 {
   t = new Employees ();
   t.Text = item.Text;
   dataContext.Employees.AddObject(t);                  
 }
 dataContext.SaveChanges();
105
Vahid Ghadiri

Plusieurs améliorations sont possibles (si vous utilisez DbContext):

Ensemble:

yourContext.Configuration.AutoDetectChangesEnabled = false;
yourContext.Configuration.ValidateOnSaveEnabled = false;

Faites SaveChanges() dans des packages de 100 inserts ... ou essayez avec des packages de 1000 éléments et observez les changements de performances. 

Comme pendant toutes ces insertions, le contexte est le même et qu'il grossit, vous pouvez reconstruire votre objet de contexte toutes les 1000 insertions. var yourContext = new YourContext(); Je pense que c'est le gros gain.

Cette amélioration d’un processus d’importation de données m’est passée de 7 minutes à 6 secondes. 

Les chiffres réels ... ne pourraient pas être 100 ou 1000 dans votre cas ... essayez-le et Tweak it.

204
Romias

Il n'y a aucun moyen de forcer EF à améliorer les performances lorsque vous le faites de cette manière. Le problème est que EF exécute chaque insertion dans un aller-retour séparé vers la base de données. Génial n'est ce pas? Même les DataSets ont pris en charge le traitement par lots. Vérifiez cet article pour une solution de contournement. Une autre solution consiste à utiliser une procédure stockée personnalisée acceptant un paramètre de valeur table, mais vous avez besoin du fichier ADO.NET brut pour cela.

42
Ladislav Mrnka

Vous pouvez utiliser extension d'insertion en bloc

Voici un petit tableau de comparaison

EntityFramework.BulkInsert vs EF AddRange_ extrait de code _

j'espère que cela t'aides

context.BulkInsert(hugeAmountOfEntities);

hope this helps

33
maxlego

En utilisant le code ci-dessous, vous pouvez étendre la classe de contexte partielle avec une méthode qui prend une collection d'objets d'entité et les copie en bloc dans la base de données. Remplacez simplement le nom de la classe de MyEntities par le nom de votre classe d'entité et ajoutez-le à votre projet, dans l'espace de noms approprié. Ensuite, il vous suffit d'appeler la méthode BulkInsertAll pour lui remettre les objets d'entité à insérer. Ne réutilisez pas la classe de contexte, créez plutôt une nouvelle instance chaque fois que vous l'utilisez. Cela est nécessaire, du moins dans certaines versions de EF, car les données d'authentification associées au SQLConnection utilisé ici sont perdues après avoir utilisé la classe une fois. Je ne sais pas pourquoi.

Cette version est pour EF 5

public partial class MyEntities
{
    public void BulkInsertAll<T>(T[] entities) where T : class
    {
        var conn = (SqlConnection)Database.Connection;

        conn.Open();

        Type t = typeof(T);
        Set(t).ToString();
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var workspace = objectContext.MetadataWorkspace;
        var mappings = GetMappings(workspace, objectContext.DefaultContainerName, typeof(T).Name);

        var tableName = GetTableName<T>();
        var bulkCopy = new SqlBulkCopy(conn) { DestinationTableName = tableName };

        // Foreign key relations show up as virtual declared 
        // properties and we want to ignore these.
        var properties = t.GetProperties().Where(p => !p.GetGetMethod().IsVirtual).ToArray();
        var table = new DataTable();
        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;

            // Nullable properties need special treatment.
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            // Since we cannot trust the CLR type properties to be in the same order as
            // the table columns we use the SqlBulkCopy column mappings.
            table.Columns.Add(new DataColumn(property.Name, propertyType));
            var clrPropertyName = property.Name;
            var tableColumnName = mappings[property.Name];
            bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(clrPropertyName, tableColumnName));
        }

        // Add all our entities to our data table
        foreach (var entity in entities)
        {
            var e = entity;
            table.Rows.Add(properties.Select(property => GetPropertyValue(property.GetValue(e, null))).ToArray());
        }

        // send it to the server for bulk execution
        bulkCopy.BulkCopyTimeout = 5 * 60;
        bulkCopy.WriteToServer(table);

        conn.Close();
    }

    private string GetTableName<T>() where T : class
    {
        var dbSet = Set<T>();
        var sql = dbSet.ToString();
        var regex = new Regex(@"FROM (?<table>.*) AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }

    private Dictionary<string, string> GetMappings(MetadataWorkspace workspace, string containerName, string entityName)
    {
        var mappings = new Dictionary<string, string>();
        var storageMapping = workspace.GetItem<GlobalItem>(containerName, DataSpace.CSSpace);
        dynamic entitySetMaps = storageMapping.GetType().InvokeMember(
            "EntitySetMaps",
            BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance,
            null, storageMapping, null);

        foreach (var entitySetMap in entitySetMaps)
        {
            var typeMappings = GetArrayList("TypeMappings", entitySetMap);
            dynamic typeMapping = typeMappings[0];
            dynamic types = GetArrayList("Types", typeMapping);

            if (types[0].Name == entityName)
            {
                var fragments = GetArrayList("MappingFragments", typeMapping);
                var fragment = fragments[0];
                var properties = GetArrayList("AllProperties", fragment);
                foreach (var property in properties)
                {
                    var edmProperty = GetProperty("EdmProperty", property);
                    var columnProperty = GetProperty("ColumnProperty", property);
                    mappings.Add(edmProperty.Name, columnProperty.Name);
                }
            }
        }

        return mappings;
    }

    private ArrayList GetArrayList(string property, object instance)
    {
        var type = instance.GetType();
        var objects = (IEnumerable)type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance, null, instance, null);
        var list = new ArrayList();
        foreach (var o in objects)
        {
            list.Add(o);
        }
        return list;
    }

    private dynamic GetProperty(string property, object instance)
    {
        var type = instance.GetType();
        return type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance, null, instance, null);
    }
}

Cette version est pour EF 6

public partial class CMLocalEntities
{
    public void BulkInsertAll<T>(T[] entities) where T : class
    {
        var conn = (SqlConnection)Database.Connection;

        conn.Open();

        Type t = typeof(T);
        Set(t).ToString();
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var workspace = objectContext.MetadataWorkspace;
        var mappings = GetMappings(workspace, objectContext.DefaultContainerName, typeof(T).Name);

        var tableName = GetTableName<T>();
        var bulkCopy = new SqlBulkCopy(conn) { DestinationTableName = tableName };

        // Foreign key relations show up as virtual declared 
        // properties and we want to ignore these.
        var properties = t.GetProperties().Where(p => !p.GetGetMethod().IsVirtual).ToArray();
        var table = new DataTable();
        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;

            // Nullable properties need special treatment.
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            // Since we cannot trust the CLR type properties to be in the same order as
            // the table columns we use the SqlBulkCopy column mappings.
            table.Columns.Add(new DataColumn(property.Name, propertyType));
            var clrPropertyName = property.Name;
            var tableColumnName = mappings[property.Name];
            bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(clrPropertyName, tableColumnName));
        }

        // Add all our entities to our data table
        foreach (var entity in entities)
        {
            var e = entity;
            table.Rows.Add(properties.Select(property => GetPropertyValue(property.GetValue(e, null))).ToArray());
        }

        // send it to the server for bulk execution
        bulkCopy.BulkCopyTimeout = 5*60;
        bulkCopy.WriteToServer(table);

        conn.Close();
    }

    private string GetTableName<T>() where T : class
    {
        var dbSet = Set<T>();
        var sql = dbSet.ToString();
        var regex = new Regex(@"FROM (?<table>.*) AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }

    private Dictionary<string, string> GetMappings(MetadataWorkspace workspace, string containerName, string entityName)
    {
        var mappings = new Dictionary<string, string>();
        var storageMapping = workspace.GetItem<GlobalItem>(containerName, DataSpace.CSSpace);
        dynamic entitySetMaps = storageMapping.GetType().InvokeMember(
            "EntitySetMaps",
            BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance,
            null, storageMapping, null);

        foreach (var entitySetMap in entitySetMaps)
        {
            var typeMappings = GetArrayList("EntityTypeMappings", entitySetMap);
            dynamic typeMapping = typeMappings[0];
            dynamic types = GetArrayList("Types", typeMapping);

            if (types[0].Name == entityName)
            {
                var fragments = GetArrayList("MappingFragments", typeMapping);
                var fragment = fragments[0];
                var properties = GetArrayList("AllProperties", fragment);
                foreach (var property in properties)
                {
                    var edmProperty = GetProperty("EdmProperty", property);
                    var columnProperty = GetProperty("ColumnProperty", property);
                    mappings.Add(edmProperty.Name, columnProperty.Name);
                }
            }
        }

        return mappings;
    }

    private ArrayList GetArrayList(string property, object instance)
    {
        var type = instance.GetType();
        var objects = (IEnumerable)type.InvokeMember(
            property, 
            BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, instance, null);
        var list = new ArrayList();
        foreach (var o in objects)
        {
            list.Add(o);
        }
        return list;
    }

    private dynamic GetProperty(string property, object instance)
    {
        var type = instance.GetType();
        return type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, instance, null);
    }

}

Et enfin, un petit quelque chose pour vous les amoureux de Linq-To-Sql.

partial class MyDataContext
{
    partial void OnCreated()
    {
        CommandTimeout = 5 * 60;
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        entities = entities.ToArray();

        string cs = Connection.ConnectionString;
        var conn = new SqlConnection(cs);
        conn.Open();

        Type t = typeof(T);

        var tableAttribute = (TableAttribute)t.GetCustomAttributes(
            typeof(TableAttribute), false).Single();
        var bulkCopy = new SqlBulkCopy(conn) { 
            DestinationTableName = tableAttribute.Name };

        var properties = t.GetProperties().Where(EventTypeFilter).ToArray();
        var table = new DataTable();

        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 => GetPropertyValue(
              property.GetValue(entity, null))).ToArray());
        }

        bulkCopy.WriteToServer(table);
        conn.Close();
    }

    private bool EventTypeFilter(System.Reflection.PropertyInfo p)
    {
        var attribute = Attribute.GetCustomAttribute(p, 
            typeof (AssociationAttribute)) as AssociationAttribute;

        if (attribute == null) return true;
        if (attribute.IsForeignKey == false) return true; 

        return false;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }
}
25
Måns Tånneryd

Peut-être que ceci répondre ici vous aidera. On dirait que vous voulez éliminer le contexte périodiquement. En effet, le contexte devient de plus en plus grand au fur et à mesure que les entités attachées grandissent. 

8
MemeDeveloper

Une meilleure façon consiste à ignorer Entity Framework entièrement pour cette opération et à vous fier à la classe SqlBulkCopy. D'autres opérations peuvent continuer à utiliser EF comme auparavant.

Cela augmente le coût de maintenance de la solution, mais contribue néanmoins à réduire le temps requis pour insérer de grandes collections d'objets dans la base de données d'un à deux ordres de grandeur par rapport à l'utilisation de EF.

Voici un article qui compare la classe SqlBulkCopy à EF pour des objets avec une relation parent-enfant (décrit également les modifications de conception nécessaires pour implémenter une insertion en bloc): Comment insérer en bloc des objets complexes dans une base de données SQL Server

4
Zoran Horvat

Dans un environnement Azure avec un site Web de base comportant 1 instance.J'ai essayé d'insérer un lot de 1 000 enregistrements à la fois sur 25 000 enregistrements à l'aide de la boucle for, cela a pris 11,5 minutes, mais en parallèle, cela a pris moins d'une minute. (Bibliothèque parallèle de tâches).

         var count = (you collection / 1000) + 1;
         Parallel.For(0, count, x =>
        {
            ApplicationDbContext db1 = new ApplicationDbContext();
            db1.Configuration.AutoDetectChangesEnabled = false;

            var records = members.Skip(x * 1000).Take(1000).ToList();
            db1.Members.AddRange(records).AsParallel();

            db1.SaveChanges();
            db1.Dispose();
        });
4
user3571683

Actuellement, il n'y a pas de meilleur moyen, mais il peut y avoir une amélioration marginale en déplaçant SaveChanges à l'intérieur de la boucle pour probablement 10 éléments.

int i = 0;

foreach (Employees item in sequence)
{
   t = new Employees ();
   t.Text = item.Text;
   dataContext.Employees.AddObject(t);   

   // this will add max 10 items together
   if((i % 10) == 0){
       dataContext.SaveChanges();
       // show some progress to user based on
       // value of i
   }
   i++;
}
dataContext.SaveChanges();

Vous pouvez ajuster 10 pour être plus proche d'une meilleure performance. Cela n'améliorera pas beaucoup la vitesse, mais cela vous permettra de montrer quelques progrès à l'utilisateur et de le rendre plus convivial.

4
Akash Kava

Essayez d’utiliser Bulk Insert ....

http://code.msdn.Microsoft.com/LinqEntityDataReader

Si vous avez une collection d'entités, par exemple storeEntities, vous pouvez les stocker en utilisant SqlBulkCopy comme suit

        var bulkCopy = new SqlBulkCopy(connection);
        bulkCopy.DestinationTableName = TableName;
        var dataReader = storeEntities.AsDataReader();
        bulkCopy.WriteToServer(dataReader);

Il y a un gotcha avec ce code. Assurez-vous que la définition Entity Framework de l'entité est en corrélation exacte avec la définition de la table, vérifiez que les propriétés de l'entité sont dans le même ordre dans le modèle d'entité que les colonnes de la table SQL Server. Sinon, cela entraînera une exception.

3
Mick

Votre code pose deux problèmes de performances majeurs:

  • Utilisation de la méthode Add
  • Utilisation de SaveChanges

Utilisation de la méthode Add

La méthode Add devient de plus en plus lente pour chaque entité ajoutée.

Voir: http://entityframework.net/improve-ef-add-performance

Par exemple, ajouter 10 000 entités via:

  • Ajouter (prendre environ 105 000 ms)
  • AddRange (prendre ~ 120ms)

_ {Remarque: les entités n'ont pas encore été enregistrées dans la base de données!} _

Le problème est que la méthode Add tente de DetectChanges à chaque entité ajoutée, alors que AddRange le fait une fois après que toutes les entités ont été ajoutées au contexte.

Les solutions communes sont:

  • Utilisez AddRange sur Add
  • SET AutoDetectChanges sur false
  • SPLIT SaveChanges en plusieurs lots

Utilisation de SaveChanges

Entity Framework n'a pas été créé pour les opérations en bloc. Pour chaque entité que vous enregistrez, un aller-retour de base de données est effectué. 

Donc, si vous voulez insérer 20 000 enregistrements, vous effectuerez 20 000 allers-retours à la base de données, ce qui est INSANE!

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 d'Entity Framework

Faites attention lorsque vous choisissez une bibliothèque d'insertion en bloc. Seules les extensions Entity Framework prennent en charge tous les types d'associations et d'héritage, et c'est la seule qui est toujours prise en charge.


Disclaimer: Je suis le propriétaire de Entity Framework Extensions

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

  • Sauvegarde en vracChangements
  • Insert en vrac
  • Suppression en masse
  • Mise à jour en vrac
  • Fusion en vrac

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

EDIT: Répondre à la question en commentaire

Existe-t-il une taille maximale recommandée pour chaque insertion en bloc de la bibliothèque que vous avez créée?

Pas trop haut, pas trop bas. Aucune valeur particulière ne convient à tous les scénarios, car elle dépend de plusieurs facteurs tels que la taille de la ligne, l'index, le déclencheur, etc.

Il est normalement recommandé de se situer autour de 4000.

De plus, y a-t-il un moyen de lier tout cela en une transaction sans craindre de l'expiration du délai?

Vous pouvez utiliser la transaction Entity Framework. Notre bibliothèque utilise la transaction si celle-ci est démarrée. Mais soyez prudent, une transaction qui prend trop de temps s'accompagne également de problèmes tels que le verrouillage de ligne/index/table.

3
Jonathan Magnan

Bien que ce soit une réponse tardive, mais je poste la réponse parce que je souffre de la même douleur . J'ai créé un nouveau projet GitHub rien que pour cela, il prend désormais en charge l'insertion/la mise à jour/la suppression en bloc du serveur SQL de manière transparente. en utilisant SqlBulkCopy.

https://github.com/MHanafy/EntityExtensions

Il y a aussi d'autres bonnes choses, et j'espère que cela sera étendu pour en faire plus sur la piste.

Son utilisation est aussi simple que

var insertsAndupdates = new List<object>();
var deletes = new List<object>();
context.BulkUpdate(insertsAndupdates, deletes);

J'espère que ça aide!

0
Mahmoud Hanafy