Accueil > .Net, Développement > [C#] Entity Framework et Bulk Insert

[C#] Entity Framework et Bulk Insert

Aujourd’hui, j’ai eu un problème avec Entity Framework sur mes tests de charges : je dois insérer 50.000 lignes sur trois tables différentes. VDM.

Ça résume plutôt bien le postulat de base.
En pratique, je travaille avec Entity Framework.
J’ai en input un certain volume de données (dont j’ignore à priori le volume en question), après les avoir transformer, je mets tout en base de données.
Pour le moment, l’objectif est de traiter 500 lignes. Donc, pour les tests de charges, on a prit un facteur 100, 50.000 lignes, donc.
Du coup, je me retrouve à la fin du traitement avec des tables où insérer entre 1 et 20 lignes, mais trois tables où insérer 50.000 lignes.
Le tout au sein d’une transaction pour assurer la cohérence des données.

J’ai bien pensé à faire deux transactions, mais ce n’est réellement pas un choix pertinent car en cas de plantage de la deuxième, je dois faire une passe sur la base pour supprimer les données de la première transaction… Et rien n’assure qu’il ne pourrait pas y avoir de plantage lors du nettoyage suite au plantage…

 
Et là, c’est quand même un peu le drame.
Car si Entity Framework est vraiment bien pour gérer le tout venant, pour gérer des données en masse, il est un peu largué.

J’ai donc pensé à mon grand copain le Bulk Insert (BI pour la suite, fainéant que je suis).

Mais il y a deux problèmes majeurs.
Le premier est la transaction : il faut que mes traitements EF et le BI soient dans la même transaction.
Hors, j’utilise la transaction d’EF et non un TransactionScope (qui, lui, utilise MSDTC; d’ailleurs je ne suis pas persuadé que le TransactionScope fonctionnerait avec le BI, mais je n’ai pas tenté).

Pour créer une transaction, je procède comme ceci :

using(var context = new EFEntities())
{
    context.Connection.Open();
    using(var transaction = context.Connection.BeginTransaction())
    {
        //...
    }
}

Ici, l’objet transaction est de type abstrait System.Data.Common.DbTransaction et de type concret System.Data.EntityClient.EntityTransaction.
Hors, le BI attend un System.Data.SqlClient.SqlTransaction qui hérite aussi de System.Data.Common.DbTransaction.

Il faut donc transformer l’objet transaction de System.Data.EntityClient.EntityTransaction vers System.Data.SqlClient.SqlTransaction.
Simple, non ?

Mais il y a une autre chose à prendre en compte. Une même transaction (de ce type) ne peut pas être partagée entre plusieurs connexions à la base de données (fussent-elles sur la même base).
Donc, il faut aussi utiliser la même connexion qu’EF.
Même problème, on va devoir transformer une System.Data.EntityClient.EntityConnection en une System.Data.SqlClient.SqlConnection, tous les deux héritant de System.Data.Common.DbConnection.
Simple aussi, non ?

En réalité, ce n’est pas si compliqué que ça.
Il y a une part de recherche sur notre grand ami qui sait tout, à savoir…Bing (bah ouais, quand on bosse avec les technos Microsoft, y a un minimum de cohérence à avoir :P).

Bref, au final, ça donne cette classe :

    using System;
    using System.Collections.Generic;
    using System.Data.Common;
    using System.Data.EntityClient;
    using System.Data.Objects.DataClasses;
    using System.Data.SqlClient;
    using System.Reflection;
    using Microsoft.Samples.EntityDataReader;

    /// <summary>
    /// Classe d'aide permettant de gérer le <c>Bulk Insert</c>.
    /// </summary>
    public class BulkInsertHelper
    {
        private readonly SqlConnection _connection;
        private readonly SqlTransaction _transaction;

        /// <summary>
        /// Initialise une nouvelle instance de la classe BulkInsertHelper avec un message d'erreur spécifié.
        /// </summary>
        /// <param name="connection">Connexion à la base de données.</param>
        public BulkInsertHelper(DbConnection connection)
        {
            if(connection == null)
            {
                throw new ArgumentNullException("connection");
            }

            var tempConnection = connection;

            var entityConnection = connection as EntityConnection;
            if (entityConnection != null)
            {
                tempConnection = entityConnection.StoreConnection;
            }

            _connection = (SqlConnection) tempConnection;
        }

        /// <summary>
        /// Initialise une nouvelle instance de la classe BulkInsertHelper avec un message d'erreur spécifié.
        /// </summary>
        /// <param name="connection">Connexion à la base de données.</param>
        /// <param name="transaction">Transaction dans laquelle s'inscrire.</param>
        public BulkInsertHelper(DbConnection connection, DbTransaction transaction)
            : this(connection)
        {
            var entityTransaction = transaction as EntityTransaction;
            if(entityTransaction != null)
            {
                _transaction = (SqlTransaction)entityTransaction.GetType()
                                                  .InvokeMember("StoreTransaction",
                                                                BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.InvokeMethod
                                                                | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.NonPublic, null, entityTransaction, new object[0]);
            }
            else
            {
                _transaction = (SqlTransaction)transaction;
            }
        }

        /// <summary>
        /// Permet d'envoyer au serveur un lot de données.
        /// <para>Attention: les clefs primaires des objets ne seront pas mises à jour.</para>
        /// </summary>
        /// <typeparam name="T">Type d'entité en provenance de <c>Entity Framework</c></typeparam>
        /// <param name="table">Nom de la table dans laquelle insérer les données.</param>
        /// <param name="options">Options à utiliser pour la copie.</param>
        /// <param name="entities">Données à insérer.</param>
        public void WriteToServer<T>(string table, IList<T> entities, SqlBulkCopyOptions options = SqlBulkCopyOptions.Default)
            where T : EntityObject
        {
            if (entities == null || entities.Count <= 0) return;

            var bulkCopy = _transaction != null
                               ? new SqlBulkCopy(_connection, options, _transaction)
                               : new SqlBulkCopy(_connection);

            bulkCopy.DestinationTableName = table;

            bulkCopy.WriteToServer(entities.AsDataReader());
        }
    }

Il y a quand même un truc à voir, ici, c’est la méthode AsDataReader.
Elle se situe dans le namespace Microsoft.Samples.EntityDataReader.
Mais pour l’avoir, il faut rajouter du code : LINQ Entity Data Reader.

Avec tout ceci, on peut enfin utiliser la classe BulkInsertHelper :

using(var context = new EFEntities())
{
    context.Connection.Open();
    using(var transaction = context.Connection.BeginTransaction())
    {
        try
        {
            //...
            var bulkInsert = new BulkInsertHelper(context.Connection, transaction);
            bulkInsert.WriteToServer("MA_TABLE1", ma_collection1);
            bulkInsert.WriteToServer("MA_TABLE2", ma_collection2);
            bulkInsert.WriteToServer("MA_TABLE3", ma_collection3);
            //...
        }
    }

Et voilà, c’est tout.
J’espère que ça pourra aider.
Moi, je garde ce code précieusement !

Catégories :.Net, Développement

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :