SqlBulkCopy using …

20. Februar 2015

Im Rahmen von stark datengetriebenen Anwendungen kann es vorkommen, dass eine Liste von Objekten in einer SQL Server Datenbank abgespeichert werden muss. Die Liste der Datenobjekte stammt dabei aus fremden Datenquellen, liegt als Liste oder Enumeration vor und die Datenmenge kann durchaus mehrere (hundert-)tausend Elemente erreichen.

Um die Datenmenge performant in der SQL Server Datenbank abzuspeichern, bietet sich hier natürlich ein BulkCopy an, welches aus C# heraus mit der Klasse SqlBulkCopy ermöglicht wird. Diese Klasse bietet die Methode WriteToServer an, wobei sie im Wesentlichen entweder eine DataTable oder ein IDataReader als Datenquelle erwartet.

In dem folgenden Beispiel wird ein Businessobjekt mit den Eigenschaften fachliche ID, Name, Preis und Datum verwendet. Die Datenbanktabelle nimmt diese 4 Attribute plus eine Identity-Column als technischen Key auf.

image

Als erstes betrachten wir die Methode mit einer DataTable als Parameter. Hierfür muss nun aus der Enumeration der Objekte eine DataTable erzeugt werden, wobei die Struktur der DataTable der Datenbanktabelle entsprechen muss. Um keine Mappings anlegen zu müssen, ist es erforderlich die Tabelle auch in der Spaltenreihenfolge identisch zu halten.

   1: private static DataTable CreateDataTable()

   2: {

   3:   var table = new DataTable(TABLENAME);

   4:  

   5:   table.Columns.Add(DC_KEY, typeof(int));

   6:   table.Columns.Add(DC_ID, typeof(int));

   7:   table.Columns.Add(DC_NAME, typeof(string));

   8:   table.Columns.Add(DC_PRICE, typeof(double));

   9:   table.Columns.Add(DC_DATE, typeof(DateTime));

  10:  

  11:   return table;

  12: }

Dabei ist auch zu beachten, dass die Identity-Column mit anzugeben ist. Ist die DataTable erzeugt, können die Daten übertragen werden und dann per WriteToServer in der Datenbank abgespeichert werden.
   1: public void SaveUsingDataTable(IEnumerable<BusinessItem> businessItems)

   2: {

   3:   var strConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["MyDB"].ConnectionString;

   4:   var insertTable = CreateDataTable();

   5:  

   6:   foreach (var businessItem in businessItems)

   7:   {

   8:     var tableRow = insertTable.NewRow();

   9:  

  10:     tableRow[DC_ID] = businessItem.ID;

  11:     tableRow[DC_NAME] = businessItem.Name;

  12:     tableRow[DC_PRICE] = businessItem.UnitPrice;

  13:     tableRow[DC_DATE] = businessItem.Date;

  14:  

  15:     insertTable.Rows.Add(tableRow);

  16:   }

  17:  

  18:   using (var tableBulkCopy = new SqlBulkCopy(strConnectionString, SqlBulkCopyOptions.UseInternalTransaction))

  19:   {

  20:     tableBulkCopy.BatchSize = insertTable.Rows.Count;

  21:     tableBulkCopy.DestinationTableName = insertTable.TableName;

  22:     tableBulkCopy.WriteToServer(insertTable);

  23:   }

  24: }

Beobachtet man den Ressourcen Verbrauch (Working Set (Memory)) und die Dauer, dann ergibt sich folgendes Bild:

Datensätze

10.000

100.000

1.000.000

Zuwachs Arbeitsspeicher (K)

5.000

22.000

150.000

Zeit (s)

0,2 – 0,3

0,8-0,9

7,5-8,0

Betrachtet man den Code so werden die Daten durch die Erzeugung der DataTable im Arbeitsspeicher „quasi verdoppelt“. Im Sinne einer Ressourcen schonenden Verarbeitung ist dies nicht der Königsweg. Daher ist eventuell die zweite Variante der WriteToServer Methode mit einem IDataReader als Parameter eine Alternative.

Ziel muss es also sein, einen DataReader für die Liste der Objekte bereitzustellen. Wie dies geht zeigen unter anderem die Beispiele hier oder hier. Nun hat die Schnittstelle IDataReader nicht wenige Methoden definiert, doch werden durch die SqlBulkCopy Methode WriteToServer nur sehr wenige genutzt, nämlich die Eigenschaft FieldCount und die Methoden Read() sowie GetValue(int i). Mit diesen Informationen lässt sich eine maßgeschneiderte Hilfsklasse implementieren:

   1: internal class BulkCopyHelper<T> : IDataReader

   2: {

   3:   private readonly IEnumerator<T> enumerator;

   4:   private readonly Func<T, object>[] getters;

   5:  

   6:   public BulkCopyHelper(IEnumerable<T> input, 

   7:     params Func<T, object>[] getters)

   8:   {

   9:     this.enumerator = input.GetEnumerator();

  10:     this.getters = getters;

  11:   }

  12:  

  13:   public int FieldCount

  14:   {

  15:     get { return this.getters.Length; }

  16:   }

  17:  

  18:   public bool Read()

  19:   {

  20:     return this.enumerator.MoveNext();

  21:   }

  22:  

  23:   public object GetValue(int i)

  24:   {

  25:     return this.getters[i](this.enumerator.Current);

  26:   }

  27:  

  28:   public void Dispose()

  29:   {

  30:     if (this.enumerator != null)

  31:     {

  32:       this.enumerator.Dispose();

  33:     }

  34:   }

  35:  

  36: ...

  37:   // sonstiges IDataReader-Methoden

  38: }

Mit einer zusätzlichen Extension-Method

   1: public static class EnumerationBulkCopyExtension

   2: {

   3:   public static IDataReader AsDataReader<T>(this IEnumerable<T> items, 

   4:     params Func<T, object>[] propertyGetters)

   5:   {

   6:     return new BulkCopyHelper<T>(items, propertyGetters);

   7:   }

   8: }

ist dann der Aufruf für den SqlBulkCopy

   1: public void SaveUsingDataReader(IEnumerable<BusinessItem> businessItems)

   2: {

   3:   var strConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings["MyDB"].ConnectionString;

   4:   var reader = businessItems.AsDataReader(

   5:     item => -1, // Identity column

   6:     item => item.ID,

   7:     item => item.Name,

   8:     item => item.UnitPrice,

   9:     item => item.Date);

  10:  

  11:   using (var readerBulkCopy = new SqlBulkCopy(strConnectionString, SqlBulkCopyOptions.UseInternalTransaction))

  12:   {

  13:     readerBulkCopy.BatchSize = 10000;

  14:     readerBulkCopy.DestinationTableName = TABLENAME;

  15:     readerBulkCopy.WriteToServer(reader);

  16:   }

  17: }

Die Funktionen, die zur Ermittlung der Daten eines Elementes dienen, müssen zum Layout der Datenbanktabelle passen. Hier ist die Identity-Column nicht zu vergessen. Beobachtet man nun den Ressourcen Verbrauch (Working Set (Memory)) und die Dauer, dann ergibt sich folgendes Bild:

Datensätze

10.000

100.000

1.000.000

Zuwachs Arbeitsspeicher (K)

5.000

5.000

6.000

Zeit (s)

0,2 – 0,3

0,6-0,7

5,5 -6,0

Fazit

Sind Massendaten aus einer .NET Anwendung heraus in eine SQL Server Datenbanktabelle einzufügen und liegen die Daten als Enumeration vor, so ist es empfehlenswert die SqlBulkCopy Klasse zu verwenden und dabei den Weg über das Interface IDataReader zu gehen. Bei großen Datenmengen benötigt sie weniger Arbeitsspeicher und ist noch etwas schneller als die DataTable Variante.