web-dev-qa-db-fra.com

Écrire un grand nombre d'enregistrements (insertion en bloc) dans Access en .NET/C #

Quelle est la meilleure façon de réaliser des insertions en bloc dans une base de données MS Access à partir de .NET? Avec ADO.NET, il faut plus d’une heure pour écrire un jeu de données volumineux.

Notez que mon message original, avant que je "refactored", avait la question et la réponse dans la partie question. J'ai pris la suggestion d'Igor Turman et l'ai réécrite en deux parties - la question ci-dessus et suivie de ma réponse.

47
Marc Meketon

J'ai constaté que l'utilisation de DAO d'une manière spécifique est environ 30 fois plus rapide que l'utilisation de ADO.NET. Je partage le code et entraîne cette réponse. En guise de contexte, le test ci-dessous consiste à écrire 100 000 enregistrements d’une table à 20 colonnes.

Un résumé de la technique et des temps - du meilleur au pire:

  1. 02.8 secondes: Utilisez DAO, utilisez DAO.Field 'pour faire référence aux colonnes du tableau 
  2. 02.8 secondes: Écrire dans un fichier texte, utiliser Automation pour importer le texte dans Access
  3. 11.0 secondes: Utilisez DAO, utilisez l'index de colonne pour faire référence aux colonnes de la table.
  4. 17.0 secondes: Utilisez DAO, reportez-vous à la colonne par son nom
  5. 79.0 secondes: Utilisez ADO.NET, générez des instructions INSERT pour chaque ligne.
  6. 86.0 secondes: Utilisez ADO.NET, utilisez DataTable vers un DataAdapter pour une insertion "batch"

À titre d’arrière-plan, j’ai parfois besoin d’analyser de grandes quantités de données et j’aperçois qu’Access est la meilleure plateforme. L'analyse implique de nombreuses requêtes et souvent beaucoup de code VBA.

Pour diverses raisons, je voulais utiliser C # au lieu de VBA. La méthode typique consiste à utiliser OleDB pour se connecter à Access. J'ai utilisé un OleDbDataReader pour récupérer des millions de disques, et cela a très bien fonctionné. Mais pour afficher les résultats dans une table, cela a pris beaucoup de temps. Plus d'une heure.

Commençons par examiner les deux méthodes classiques pour écrire des enregistrements dans Access à partir de C #. Les deux méthodes impliquent OleDB et ADO.NET. La première consiste à générer les instructions INSERT une à la fois et à les exécuter en prenant 79 secondes pour les 100 000 enregistrements. Le code est:

public static double TestADONET_Insert_TransferToAccess()
{
  StringBuilder names = new StringBuilder();
  for (int k = 0; k < 20; k++)
  {
    string fieldName = "Field" + (k + 1).ToString();
    if (k > 0)
    {
      names.Append(",");
    }
    names.Append(fieldName);
  }

  DateTime start = DateTime.Now;
  using (OleDbConnection conn = new OleDbConnection(Properties.Settings.Default.AccessDB))
  {
    conn.Open();
    OleDbCommand cmd = new OleDbCommand();
    cmd.Connection = conn;

    cmd.CommandText = "DELETE FROM TEMP";
    int numRowsDeleted = cmd.ExecuteNonQuery();
    Console.WriteLine("Deleted {0} rows from TEMP", numRowsDeleted);

    for (int i = 0; i < 100000; i++)
    {
      StringBuilder insertSQL = new StringBuilder("INSERT INTO TEMP (")
        .Append(names)
        .Append(") VALUES (");

      for (int k = 0; k < 19; k++)
      {
        insertSQL.Append(i + k).Append(",");
      }
      insertSQL.Append(i + 19).Append(")");
      cmd.CommandText = insertSQL.ToString();
      cmd.ExecuteNonQuery();
    }
    cmd.Dispose();
  }
  double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds;
  Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds);
  return elapsedTimeInSeconds;
}

Notez que je n'ai trouvé aucune méthode dans Access qui autorise une insertion en bloc.

J'avais alors pensé que l'utilisation d'une table de données avec un adaptateur de données serait peut-être utile. Surtout que je pensais pouvoir réaliser des insertions par lots à l'aide de la propriété UpdateBatchSize d'un adaptateur de données. Cependant, apparemment, seuls SQL Server et Oracle le prennent en charge, contrairement à Access. Et cela a pris le plus long temps de 86 secondes. Le code que j'ai utilisé était:

public static double TestADONET_DataTable_TransferToAccess()
{
  StringBuilder names = new StringBuilder();
  StringBuilder values = new StringBuilder();
  DataTable dt = new DataTable("TEMP");
  for (int k = 0; k < 20; k++)
  {
    string fieldName = "Field" + (k + 1).ToString();
    dt.Columns.Add(fieldName, typeof(int));
    if (k > 0)
    {
      names.Append(",");
      values.Append(",");
    }
    names.Append(fieldName);
    values.Append("@" + fieldName);
  }

  DateTime start = DateTime.Now;
  OleDbConnection conn = new OleDbConnection(Properties.Settings.Default.AccessDB);
  conn.Open();
  OleDbCommand cmd = new OleDbCommand();
  cmd.Connection = conn;

  cmd.CommandText = "DELETE FROM TEMP";
  int numRowsDeleted = cmd.ExecuteNonQuery();
  Console.WriteLine("Deleted {0} rows from TEMP", numRowsDeleted);

  OleDbDataAdapter da = new OleDbDataAdapter("SELECT * FROM TEMP", conn);

  da.InsertCommand = new OleDbCommand("INSERT INTO TEMP (" + names.ToString() + ") VALUES (" + values.ToString() + ")");
  for (int k = 0; k < 20; k++)
  {
    string fieldName = "Field" + (k + 1).ToString();
    da.InsertCommand.Parameters.Add("@" + fieldName, OleDbType.Integer, 4, fieldName);
  }
  da.InsertCommand.UpdatedRowSource = UpdateRowSource.None;
  da.InsertCommand.Connection = conn;
  //da.UpdateBatchSize = 0;

  for (int i = 0; i < 100000; i++)
  {
    DataRow dr = dt.NewRow();
    for (int k = 0; k < 20; k++)
    {
      dr["Field" + (k + 1).ToString()] = i + k;
    }
    dt.Rows.Add(dr);
  }
  da.Update(dt);
  conn.Close();

  double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds;
  Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds);
  return elapsedTimeInSeconds;
}

Ensuite, j'ai essayé des méthodes non standard. Tout d'abord, j'ai écrit dans un fichier texte, puis utilisé Automation pour l'importer. C'était très rapide - 2,8 secondes - et à égalité pour la première place. Mais je considère cela comme fragile pour un certain nombre de raisons: La sortie des champs de date est délicate. Je devais les formater spécialement (someDate.ToString("yyyy-MM-dd HH:mm")), puis mettre en place une "spécification d'importation" spéciale qui code dans ce format. La spécification d'importation devait également avoir le délimiteur "quote" défini correctement. Dans l'exemple ci-dessous, avec uniquement des champs entiers, une spécification d'importation n'était pas nécessaire.

Les fichiers texte sont également fragiles pour "l'internationalisation" où il existe une utilisation de virgules pour les séparateurs décimaux, différents formats de date, l'utilisation possible de l'unicode.

Notez que le premier enregistrement contient les noms de champ afin que l'ordre des colonnes ne soit pas dépendant de la table et que nous avons utilisé Automation pour effectuer l'importation réelle du fichier texte.

public static double TestTextTransferToAccess()
{
  StringBuilder names = new StringBuilder();
  for (int k = 0; k < 20; k++)
  {
    string fieldName = "Field" + (k + 1).ToString();
    if (k > 0)
    {
      names.Append(",");
    }
    names.Append(fieldName);
  }

  DateTime start = DateTime.Now;
  StreamWriter sw = new StreamWriter(Properties.Settings.Default.TEMPPathLocation);

  sw.WriteLine(names);
  for (int i = 0; i < 100000; i++)
  {
    for (int k = 0; k < 19; k++)
    {
      sw.Write(i + k);
      sw.Write(",");
    }
    sw.WriteLine(i + 19);
  }
  sw.Close();

  ACCESS.Application accApplication = new ACCESS.Application();
  string databaseName = Properties.Settings.Default.AccessDB
    .Split(new char[] { ';' }).First(s => s.StartsWith("Data Source=")).Substring(12);

  accApplication.OpenCurrentDatabase(databaseName, false, "");
  accApplication.DoCmd.RunSQL("DELETE FROM TEMP");
  accApplication.DoCmd.TransferText(TransferType: ACCESS.AcTextTransferType.acImportDelim,
  TableName: "TEMP",
  FileName: Properties.Settings.Default.TEMPPathLocation,
  HasFieldNames: true);
  accApplication.CloseCurrentDatabase();
  accApplication.Quit();
  accApplication = null;

  double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds;
  Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds);
  return elapsedTimeInSeconds;
}

Enfin, j'ai essayé DAO. Beaucoup de sites donnent des avertissements énormes sur l'utilisation de DAO. Cependant, il s'avère que c'est simplement le meilleur moyen d'interagir entre Access et .NET, en particulier lorsque vous devez écrire un grand nombre d'enregistrements. En outre, il donne accès à toutes les propriétés d'une table. J'ai lu quelque part qu'il est plus facile de programmer des transactions en utilisant DAO au lieu de ADO.NET. 

Notez qu'il y a plusieurs lignes de code qui sont commentées. Ils seront expliqués bientôt.

public static double TestDAOTransferToAccess()
{

  string databaseName = Properties.Settings.Default.AccessDB
    .Split(new char[] { ';' }).First(s => s.StartsWith("Data Source=")).Substring(12);

  DateTime start = DateTime.Now;
  DAO.DBEngine dbEngine = new DAO.DBEngine();
  DAO.Database db = dbEngine.OpenDatabase(databaseName);

  db.Execute("DELETE FROM TEMP");

  DAO.Recordset rs = db.OpenRecordset("TEMP");

  DAO.Field[] myFields = new DAO.Field[20];
  for (int k = 0; k < 20; k++) myFields[k] = rs.Fields["Field" + (k + 1).ToString()];

  //dbEngine.BeginTrans();
  for (int i = 0; i < 100000; i++)
  {
    rs.AddNew();
    for (int k = 0; k < 20; k++)
    {
      //rs.Fields[k].Value = i + k;
      myFields[k].Value = i + k;
      //rs.Fields["Field" + (k + 1).ToString()].Value = i + k;
    }
    rs.Update();
    //if (0 == i % 5000)
    //{
      //dbEngine.CommitTrans();
      //dbEngine.BeginTrans();
    //}
  }
  //dbEngine.CommitTrans();
  rs.Close();
  db.Close();

  double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds;
  Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds);
  return elapsedTimeInSeconds;
}

Dans ce code, nous avons créé des variables DAO.Field pour chaque colonne (myFields[k]), puis les avons utilisées. Cela a pris 2,8 secondes. Vous pouvez également accéder directement à ces champs, comme indiqué dans la ligne commentée rs.Fields["Field" + (k + 1).ToString()].Value = i + k;, ce qui augmente le temps à 17 secondes. Enveloppant le code dans une transaction (voir les lignes commentées), cela a chuté à 14 secondes. L’utilisation d’un index entier rs.Fields[k].Value = i + k; a réduit ce délai à 11 secondes. L'utilisation de DAO.Field (myFields[k]) et d'une transaction a pris plus de temps, augmentant le temps à 3,1 secondes.

Enfin, par souci d'exhaustivité, tout ce code était dans une classe statique simple et les instructions using sont les suivantes:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ACCESS = Microsoft.Office.Interop.Access; // USED ONLY FOR THE TEXT FILE METHOD
using DAO = Microsoft.Office.Interop.Access.Dao; // USED ONLY FOR THE DAO METHOD
using System.Data; // USED ONLY FOR THE ADO.NET/DataTable METHOD
using System.Data.OleDb; // USED FOR BOTH ADO.NET METHODS
using System.IO;  // USED ONLY FOR THE TEXT FILE METHOD
68
Marc Meketon

Merci Marc , pour te voter, j'ai créé un compte sur StackOverFlow ...

Ci-dessous se trouve la méthode réutilisable [Testé sur C # avec 64 bits - Windows 7, Windows 2008 R2, Vista, XP plates-formes]

Détails de performance: Exporte 120 000 lignes en 4 secondes.

Copiez le code ci-dessous et transmettez les paramètres ... et observez les performances.

  • Passez simplement votre datatable avec le même schéma, en tant que cible Access Db Table. 
  • DBPath = chemin d'accès complet à la base de données 
  • TableNm = Nom de la table cible d'accès Db.

Le code:

public void BulkExportToAccess(DataTable dtOutData, String DBPath, String TableNm) 
{
    DAO.DBEngine dbEngine = new DAO.DBEngine();
    Boolean CheckFl = false;

    try
    {
        DAO.Database db = dbEngine.OpenDatabase(DBPath);
        DAO.Recordset AccesssRecordset = db.OpenRecordset(TableNm);
        DAO.Field[] AccesssFields = new DAO.Field[dtOutData.Columns.Count];

        //Loop on each row of dtOutData
        for (Int32 rowCounter = 0; rowCounter < dtOutData.Rows.Count; rowCounter++)
        {
            AccesssRecordset.AddNew();
            //Loop on column
            for (Int32 colCounter = 0; colCounter < dtOutData.Columns.Count; colCounter++)
            {
                // for the first time... setup the field name.
                if (!CheckFl)
                    AccesssFields[colCounter] = AccesssRecordset.Fields[dtOutData.Columns[colCounter].ColumnName];
                AccesssFields[colCounter].Value = dtOutData.Rows[rowCounter][colCounter];
            }

            AccesssRecordset.Update();
            CheckFl = true;
        }

        AccesssRecordset.Close();
        db.Close();
    }
    finally
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(dbEngine);
        dbEngine = null;
    }
}
10
Prasoon Pathak

Vous pouvez utiliser un mappeur de relations d'objets KORM permettant des opérations en bloc sur MsAccess.

database
  .Query<Movie>()
  .AsDbSet()
  .BulkInsert(_data);

ou si vous avez un lecteur source, vous pouvez directement utiliser la classe MsAccessBulkInsert:

using (var bulkInsert = new MsAccessBulkInsert("connection string"))
{
   bulkInsert.Insert(sourceReader);
}

KORM est disponible à partir de nuget Kros.KORM.MsAccess et il est opensource sur GitHub

2
Mino

Merci Marc pour les exemples.
Sur mon système, les performances de DAO ne sont pas aussi bonnes que celles suggérées ici:

TestADONET_Insert_TransferToAccess (): 68 secondes
TestDAOTransferToAccess (): 29 secondes

Étant donné que sur mon système, l'utilisation de bibliothèques Office Interop n'est pas une option, j'ai essayé une nouvelle méthode impliquant l'écriture d'un fichier CSV, puis son importation via ADO:

    public static double TestADONET_Insert_FromCsv()
    {
        StringBuilder names = new StringBuilder();
        for (int k = 0; k < 20; k++)
        {
            string fieldName = "Field" + (k + 1).ToString();
            if (k > 0)
            {
                names.Append(",");
            }
            names.Append(fieldName);
        }

        DateTime start = DateTime.Now;
        StreamWriter sw = new StreamWriter("tmpdata.csv");

        sw.WriteLine(names);
        for (int i = 0; i < 100000; i++)
        {
            for (int k = 0; k < 19; k++)
            {
                sw.Write(i + k);
                sw.Write(",");
            }
            sw.WriteLine(i + 19);
        }
        sw.Close();

        using (OleDbConnection conn = new OleDbConnection(Properties.Settings.Default.AccessDB))
        {
            conn.Open();
            OleDbCommand cmd = new OleDbCommand();
            cmd.Connection = conn;

            cmd.CommandText = "DELETE FROM TEMP";
            int numRowsDeleted = cmd.ExecuteNonQuery();
            Console.WriteLine("Deleted {0} rows from TEMP", numRowsDeleted);

            StringBuilder insertSQL = new StringBuilder("INSERT INTO TEMP (")
                .Append(names)
                .Append(") SELECT ")
                .Append(names)
                .Append(@" FROM [Text;Database=.;HDR=yes].[tmpdata.csv]");
            cmd.CommandText = insertSQL.ToString();
            cmd.ExecuteNonQuery();

            cmd.Dispose();
        }

        double elapsedTimeInSeconds = DateTime.Now.Subtract(start).TotalSeconds;
        Console.WriteLine("Append took {0} seconds", elapsedTimeInSeconds);
        return elapsedTimeInSeconds;
    }

Analyse performace de TestADONET_Insert_FromCsv (): 1,9 seconde

Semblable à l'exemple de Marc, TestTextTransferToAccess (), cette méthode est également fragile pour plusieurs raisons liées à l'utilisation de fichiers CSV.

J'espère que cela t'aides.
Lorenzo

1
LorenzoB

Tout d’abord, assurez-vous que les colonnes de la table d’accès ont le même nom de colonne et le même type. Ensuite, vous pouvez utiliser cette fonction qui, je pense, est très rapide et élégante.

public void AccessBulkCopy(DataTable table)
{
    foreach (DataRow r in table.Rows)
        r.SetAdded();

    var myAdapter = new OleDbDataAdapter("SELECT * FROM " + table.TableName, _myAccessConn);

    var cbr = new OleDbCommandBuilder(myAdapter);
    cbr.QuotePrefix = "[";
    cbr.QuoteSuffix = "]";
    cbr.GetInsertCommand(true);

    myAdapter.Update(table);
}
0
0014

Une autre méthode à envisager consiste à relier des tables via DAO ou ADOX, puis à exécuter des instructions telles que:

SELECT * INTO Table1 FROM _LINKED_Table1

S'il vous plaît voir ma réponse complète ici:
Mise à jour par lots de MS Access via ADO.Net et interopérabilité COM

0
Ruutsa

Pour ajouter à la réponse de Marc:

Notez que l'attribut [STAThread] se situe au-dessus de votre méthode Main. permettra à votre programme de communiquer facilement avec les objets COM, augmentant ainsi la vitesse. Je sais que ce n'est pas pour toutes les applications, mais si vous dépendez fortement de DAO, je le recommanderais.

De plus, en utilisant la méthode d’insertion DAO. Si vous avez une colonne non requise et que vous souhaitez insérer une valeur null, ne définissez même pas sa valeur. La définition de la valeur coûtait du temps, même s'il était nul.

0
Bart de Bever