Accueil > .Net, C#, Développement, Sql Server > Copier des données d’une base à une autre

Copier des données d’une base à une autre

Récemment, j’ai eu besoin d’alimenter une base de données locale avec les données d’une autre base de données.
La contrainte est que la base de données source est un SQL Server 2008 R2, la base de données cible un SQL Server Express 2005.
Donc, on élimine de suite le backup/restore.

Après, il y a sans doute des outils ô combien puissant pour faire ça.
Mais…j’aime bien me faire les miens.

Du coup, j’ai réutilisé une couche d’accès aux données que j’avais déjà (que j’avais partiellement donné ici : [C# SqlServer] Moteur de requêtage) ainsi que le bulk insert (dont j’avais parlé ici [C#] Bulk Insert).

En modifiant le premier pour intégrer le deuxième, ça m’a donné le résultat escompté en un temps record.

namespace KR.TransfertData
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Data.SqlClient;
    using System.Linq;

    /// <summary>
    /// Classe permettant la gestion de la connexion à la base de données.
    /// <para>Dans le cas d'une connexion avec transaction, la connexion est ouverte à la création de l'objet courant. Il faut appeler la méthode 
    /// <see cref="CommitTransaction"/> pour <c>commiter</c> et fermer la connexion. Dans le cas contraire, la transaction sera <c>Rollback</c> à sa fermeture.</para>
    /// </summary>
    public sealed class ConnectionHandler : IDisposable
    {
        /// <summary>
        /// Timeout par défaut de la commande.
        /// </summary>
        private const int DefaultTimeOut = 15;

        #region Constructeur / Destructeur

        /// <summary>
        /// Initialise une nouvelle instance de la classe <see cref="ConnectionHandler" />.
        /// </summary>
        /// <param name="log">Méthode permettant de loguer toutes les requêtes.</param>
        private ConnectionHandler()
        {
        }

        /// <summary>
        /// Finalise une instance de la classe <see cref="ConnectionHandler" />.
        /// </summary>
        ~ConnectionHandler()
        {
            this.Dispose(false);
        }
        #endregion

        #region Variables privées

        /// <summary>
        /// Obtient ou définit une valeur indiquant si les objets managés sont "disposés".
        /// </summary>
        private bool _disposed { get; set; }

        /// <summary>
        /// Obtient ou définit la connexion vers la base de données.
        /// </summary>
        private SqlConnection Connexion { get; set; }

        /// <summary>
        /// Obtient ou définit une valeur indiquant si une transaction doit être prise en compte.
        /// </summary>
        private bool HasTransaction { get; set; }

        #endregion

        #region Création de connexion

        /// <summary>
        /// Permet de créer la connexion vers la base de données.
        /// <para>Utilise la chaine de connexion <c>SgbdSaga</c> dans le fichier de configuration.</para>
        /// <para>Dans le cas d'une connexion avec transaction, la connexion est ouverte à la création de l'objet courant. Il faut appeler la méthode 
        /// <see cref="CommitTransaction"/> pour <c>commiter</c> et fermer la connexion. Dans le cas contraire, la transaction sera <c>Rollback</c> à sa fermeture.</para>
        /// </summary>
        /// <param name="connectionString">Chaine de connexion à la base.</param>
        /// <returns>Retourne un objet <see cref="ConnectionHandler"/>.</returns>
        public static ConnectionHandler CreateConnexion(string connectionString)
        {
            ConnectionHandler connexion = new ConnectionHandler() { Connexion = new SqlConnection(connectionString) };

            connexion.OpenIfNeeded();
            connexion.HasTransaction = true;

            return connexion;
        }

        #endregion

        #region Méthode statique publique

        /// <summary>
        /// Copie les données des tables mentionnées d'une table vers une autre.
        /// </summary>
        /// <param name="connectionStringFrom">Chaîne de connexion à la base de données source.</param>
        /// <param name="connectionStringTo">Chaîne de connexion à la base de données cible.</param>
        /// <param name="tables">Liste des tables.</param>
        public static void CopyData(string connectionStringFrom, string connectionStringTo, List<string> tables)
        {
            DataSet data = new DataSet();
            using (ConnectionHandler connexion = ConnectionHandler.CreateConnexion(connectionStringFrom))
            {
                string query = string.Join(";", tables.Select(t => string.Format("SELECT * FROM {0}", t)));

                connexion.ExecuteReader(
                    query,
                    reader =>
                    {
                        data.Load(reader, LoadOption.OverwriteChanges, tables.ToArray());
                    });
            }

            using (ConnectionHandler connexion = ConnectionHandler.CreateConnexion(connectionStringTo))
            {
                for (int i = 0; i < data.Tables.Count; i++)
                {
                    connexion.BulkInsert(data.Tables[i]);
                }
            }
        }

        #endregion

        #region Membres IDisposable

        /// <summary>
        /// Permet de fermer (si besoin) et de libérer les ressources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Dispose de l'objet courant.
        /// </summary>
        /// <param name="disposing">Indique s'il faut faire un Dispose sur les objets managés.</param>
        private void Dispose(bool disposing)
        {
            if (this._disposed)
            {
                return;
            }

            if (disposing)
            {
                // Libérer les ressources managée qui implémentent IDisposable.
                this.CloseIfNeeded();
                this.Connexion.Dispose();
            }

            // Libérer les ressources NON managées et mettre leurs références à null.
            this._disposed = true;
        }

        #endregion

        #region Méthodes protected
        /// <summary>
        /// Permet d'ouvrir la connexion, si besoin.
        /// </summary>
        private void OpenIfNeeded()
        {
            if (this.Connexion != null)
            {
                if (this.Connexion.State != System.Data.ConnectionState.Open)
                {
                    this.Connexion.Open();
                }
            }
        }

        /// <summary>
        /// Permet de fermer la connexion, si besoin.
        /// </summary>
        private void CloseIfNeeded()
        {
            // S'il y a une transaction courante, il faut laisser la connexion ouverte.
            if (this.Connexion != null && !this.HasTransaction)
            {
                if (this.Connexion.State != System.Data.ConnectionState.Closed)
                {
                    this.Connexion.Close();
                }
            }
        }
        #endregion

        #region Méthodes privées

        /// <summary>
        /// Méthode permettant de créer une commande au format texte.
        /// </summary>
        /// <param name="commandText">Requête sous forme de string.</param>
        /// <param name="desactivateLog">Permet de forcer la désactivation de la fonctionnalité de log.</param>
        /// <param name="commandTimeout">Permet de fixer le <c>timeout</c> de la commande, par défaut : 15 secondes.</param>
        /// <returns>Retourne la commande à exécuter.</returns>
        private SqlCommand CreateCommandText(string commandText, bool desactivateLog, int commandTimeout = 15)
        {
            SqlCommand command = new SqlCommand
            {
                Connection = this.Connexion,
                CommandType = System.Data.CommandType.Text,
                CommandText = commandText,
                CommandTimeout = commandTimeout
            };

            return command;
        }

        /// <summary>
        /// Permet de récupérer les données depuis un <c>SqlDataReader</c>.
        /// </summary>
        /// <param name="reader">Reader contenant les données.</param>
        /// <param name="fillMethod">Méthode pour gérer le cas où le reader contient des données.</param>
        /// <param name="emptyResult">Méthode pour gérer le cas où le reader ne contient pas de données.</param>
        /// <returns>Indique si le reader contenait des données.</returns>
        private bool ManageDataReader(
            SqlDataReader reader,
            Action<SqlDataReader> fillMethod = null,
            Action emptyResult = null)
        {
            bool hasResult = false;
            if (reader.HasRows)
            {
                hasResult = true;
                if (fillMethod != null)
                {
                    fillMethod(reader);
                }
            }
            else
            {
                if (emptyResult != null)
                {
                    emptyResult();
                }
            }

            return hasResult;
        }

        /// <summary>
        /// Permet d'exécuter une requête retournant une ou plusieurs lignes de données (SELECT).
        /// </summary>
        /// <param name="commandText">Requête SQL.</param>
        /// <param name="fillMethod">Méthode permettant de gérer la récupération des données.</param>
        /// <returns>Indique si l'opération retourne des données.</returns>
        private bool ExecuteReader(
            string commandText,
            Action<SqlDataReader> fillMethod)
        {
            bool hasResult;
            try
            {
                SqlCommand command = this.CreateCommandText(commandText, false, DefaultTimeOut);

                this.OpenIfNeeded();
                SqlDataReader reader = command.ExecuteReader();
                hasResult = this.ManageDataReader(reader, fillMethod, null);

                // TRES IMPORTANT ! 
                // Dans le cas d'une transaction, la ligne suivante doit être présente sous peine de plantage.
                // Mais en règle générale, c'est aussi bien de l'avoir.
                reader.Close();
            }
            finally
            {
                this.CloseIfNeeded();
            }

            return hasResult;
        }

        /// <summary>
        /// Insertion en masse des données.
        /// </summary>
        /// <param name="data"><c>DataTable</c> contenant toutes les données à l'image de la table cible.</param>
        /// <returns></returns>
        private bool BulkInsert(DataTable data)
        {
            try
            {
                this.OpenIfNeeded();

                SqlBulkCopy bulkCopy = new SqlBulkCopy(
                    this.Connexion);

                bulkCopy.DestinationTableName = data.TableName;
                bulkCopy.WriteToServer(data);
            }
            finally
            {
                this.CloseIfNeeded();
            }

            return true;
        }
        #endregion
    }
}

Et pour l’utilisation, dans une application console :

namespace KR.TransfertData
{
    using System;
    using System.Configuration;
    using System.Diagnostics;
    using System.Linq;

    public class Program
    {
        public static void Main(string[] args)
        {
            if (args != null && args.Length > 0)
            {
                Stopwatch s = Stopwatch.StartNew();
                string destination = ConfigurationManager.ConnectionStrings["DESTINATION"].ConnectionString;
                string source = ConfigurationManager.ConnectionStrings["SOURCE"].ConnectionString;

                ConnectionHandler.CopyData(source, destination, args.ToList());
                s.Stop();
                Console.WriteLine(s.ElapsedMilliseconds);
            }
        }
    }
}

Il va sans dire que l’App.config est bien configuré :)
Après, j’ai juste à spécifier mes tables en paramètre, en zou, roulez jeunesse.

Sur mon besoin, ça donne : 80.000 lignes pour 9 secondes, 5.000 lignes pour 1 seconde.
C’est pas mal, je trouve.
Après, ça bouffe pas mal la RAM, c’est pas optimisé et ne permettra pas de gérer les gros volumes.
Mais dans le cadre de l’alimentation d’une base de données sur poste de développement, c’est suffisant (à mon goût).

About these ads
Catégories:.Net, C#, Développement, Sql Server
  1. 03/12/2013 à 17:39

    Pas mal, mais as tu fais une comparaison avec la fonctionnalité "Data Compare" de Visual Studio ?
    Je sais qu’on peut aussi faire un "Schema Compare" dessus pour mettre à un même niveau les schémas de BDD.

  1. 02/12/2013 à 17:10

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

Suivre

Recevez les nouvelles publications par mail.

Rejoignez 55 autres abonnés

%d bloggers like this: