Faire du Bulk Insert/Update avec MySQL en .NET

Tags: MySQL

Si vous êtes développeur .NET, il y a de fortes chances pour que vous utilisiez une base de données de type Microsoft SQL Server (ou l’une de ses dérivées). Mais ce n’est pas le cas pour tout le monde et l’une des alternatives (gratuite) est bien entendu l’excellente MySQL.

Heureusement, il existe un connecteur ADO .NET pour MySQL qui vous permet d’accéder à votre base de données (en version 6.5.4 à ce jour, 21 Mars 2012)

Je ne vous ferais pas l’affront de vous expliquer comment cela marche avec ce connecteur, puisque c’est exactement le même principe que l’accès à une base SQL Server (avec les ExecuteReader, ExecuteNonQuery & co).

Mais il y a bien entendu des spécificités, et l’une d’entre elle est la possibilité de faire du Bulk Insert/Update.

Bulk Insert/Update : Kézako ?

Imaginons que vous ayez beaucoup de ligne à insérer dans votre base de données. La première solution (la plus catastrophique), est de faire une série d’insert. On peut voir cela de plusieurs façons :

Une boucle qui :

  • appelle une méthode qui ouvre la connexion,
  • effectue un ExecuteNonQuery avec comme requête, un INSERT INTO,
  • ferme la connexion,
  • passe à la ligne suivante.

Catastrophe au niveau performance.

Seconde solution, créer dynamiquement une requête SQL avec autant de ligne INSERT INTO que de ligne à insérer :

INSERT INTO maTable (Nom, Prenom) VALUES (‘Clark’, ‘Richard’)
INSERT INTO maTable (Nom, Prenom) VALUES (‘Clark’, ‘Sabine’)
INSERT INTO maTable (Nom, Prenom) VALUES (‘Gates’, ‘Bill’)

Vous pouvez optimiser votre script en verrouillant la table comme le montre l’article sur MySQL. Inconvénients de cette méthode : les performances ne sont tout de même pas du feu de Dieu, et l’on doit verrouiller la table.

Donc l’idéal est de faire du Bulk Insert, ou plus exactement d’utiliser la syntaxe LOAD DATA INFILE de MySQL.

Load Data Infile

Cette méthode permet d’uploader un fichier type csv dans MySQL et le moteur se charge de faire l’insertion. Cette méthode est jusqu’à 20 fois plus rapide que celles évoquées plus haut. L’inconvénient par rapport à un Bulk Insert pour SQL Server, c’est que vous êtes obligé de passer par un fichier (avec SQL Server, vous pouvez le faire “in-memory”).

Donc regardons comment procéder.

NB: nous allons insérer dans une table Persons des données représentées par la classe Person suivante :

public class Person
{
    public int Id { get; set; }
    public string Nom { get; set; }
    public string Prenom { get; set; }
    public decimal SalaireMensuel { get; set; }
}

 

La table Persons définie l’Id comme un INT(11) qui est auto incrémentiel.

MySqlBulkLoader est la classe fournis par le connecteur MySql qui va nous permettre de faire ce que l’on veut. Il est très simple d’utilisation. Parmi les propriétés importantes qu’il faut lui préciser, il y a :

  • Le caractère séparateur de champs,
  • Le caractère de fin de ligne,
  • Le nom de la table MySql,
  • Les colonnes à considérer,
  • Le nombre de ligne dans le fichier csv à ne pas prendre en compte au début du fichier csv

Une fois que vous avez déterminé tout cela, il ne vous restera plus qu’à créer dynamiquement le fichier.

Donc première étape, créer notre MySqlBulkLoader :

public int BulkInsertPerson(IEnumerable<Person> persons)
{
    using (var connection = new MySqlConnection(ConnectionString))
    {
        var bl = new MySqlBulkLoader(connection);
        bl.TableName = "Persons";
        bl.FieldTerminator = "\t";
        bl.LineTerminator = "\r\n";
        bl.FileName = CreatePersonsFile(persons);
        bl.Columns.AddRange(new[]
                                {
                                    "Nom",
                                    "Prenom",
                                    "SalaireMensuel"
                                });

        bl.NumberOfLinesToSkip = 0;

        var inserted = bl.Load();
        return inserted;
    }
}

Vraiment rien de bien compliqué. Je n’ai juste que la méthode CreatePersonsFile à écrire qui va créer dynamiquement mon fichier csv. Pour ne pas avoir à gérer ce fichier, je vais utiliser le dossier de fichiers temporaires de Windows (c’est lui qui fera le ménage Clignement d'œil) :

 

private static string CreatePersonsFile(IEnumerable<Person> persons)
{
    var fileName = Path.GetTempFileName();
    using (var writer = new StreamWriter(fileName))
    {
        foreach (var ligne in persons)
        {
            writer.WriteLine(
                string.Join("\t", new[]
                                      {
                                          ligne.Nom,
                                          ligne.Prenom,
                                          ligne.SalaireMensuel.ToString(CultureInfo.InvariantCulture)
                                      }));
        }
    }
    return fileName;
}

Notez l’utilisation du InvariantCulture pour le decimal.

Bulk Insert/Update

Vous allez me dire : et si je veux faire de l’UPDATE et de l’INSERT en même temps ? C’est tout à fait possible.

Tout d’abord, vous avez vu que dans les colonnes, je n’avais pas spécifié la colonne Id de ma table puisque c’est une colonne de type auto incrémentiel. Et bien si vous voulez faire de l’INSERT/UPDATE, il va falloir l’ajouter et de plus, il va falloir indiquer à mySQL comment gérer les conflits. Pour cela, MySqlBulkLoader possède une propriété ConflictOption : si MySql trouve une ligne avec le même Id, que doit-il faire ? Si conflictOption est à Ignore, il ne fera rien, en revanche si il est à Replace, il supprimera la ligne et la remplacera par les nouvelles données.

Attention, dans ce cas, le nombre retournée par la méthode est différente du nombre de ligne véritablement affectées. Un UPDATE compte pour 2 (un DELETE et un INSERT), alors qu’un INSERT compte pour 1. Donc si vous envoyez x lignes et qu’il vous retourne y :

  • Nombre UPDATE = Nombre retourné – Nombre de ligne envoyées
  • Nombre INSERT = 2*Nombre de ligne envoyées – Nombre retourné

 

Pour en revenir à notre code :

public int BulkInsertUpdatePerson(IEnumerable<Person> persons)
{
    using (var connection = new MySqlConnection(ConnectionString))
    {
        var bl = new MySqlBulkLoader(connection);
        bl.TableName = "Persons";
        bl.FieldTerminator = "\t";
        bl.LineTerminator = "\r\n";
        bl.FileName = CreatePersonsFile(persons);
        bl.Columns.AddRange(new[]
                                {
                                    "id",
                                    "Nom",
                                    "Prenom",
                                    "SalaireMensuel"
                                });

        bl.NumberOfLinesToSkip = 0;

        bl.ConflictOption = MySqlBulkLoaderConflictOption.Replace;
        var inserted = bl.Load();
        return inserted;
    }
}

 

Et pour la création du fichier, on prend comme convention que si notre Id est égal à 0, c’est un INSERT qu’il faut faire. Dans on écrit un null dans la colonne correspondante dans le fichier :

private static string CreatePersonsFile(IEnumerable<Person> persons)
{
    var fileName = Path.GetTempFileName();
    using (var writer = new StreamWriter(fileName))
    {
        foreach (var ligne in persons)
        {
            writer.WriteLine(
                string.Join("\t", new[]
                                      {
                                          ligne.Id == 0 ? null : ligne.Id.ToString(CultureInfo.InvariantCulture),
                                          ligne.Nom,
                                          ligne.Prenom,
                                          ligne.SalaireMensuel.ToString(CultureInfo.InvariantCulture)
                                      }));
        }
    }
    return fileName;
}

 

Et voilà, en une seule méthode, vous pouvez faire des INSERT/UPDATE de masse dans votre base de données MySQL.

blog comments powered by Disqus