2016-03-16 3 views
0

Мое приложение использует Entity Framework. В большинстве дней я чувствую себя хорошо, но у меня проблемы, когда мне нужно делать массовые вставки. Мне еще предстоит найти способ для EF сделать это быстро. Таким образом, мне нужно решение, которое выходит за рамки EF, чтобы делать обновления.Как навалом вставить общий исходный массив в произвольную таблицу SQL

Мне нужен метод, который принимает строку соединения, имя таблицы назначения и общий массив исходных данных и выполняет массовые вставки. Более того, я хотел бы, чтобы он сопоставлял свойства исходных данных с конкретными полями таблицы, в идеале, не требуя атрибутов в исходном объекте для обозначения поля таблицы.

Таким образом, для данного объекта:

public class Customer 
{ 
    //property populated & used only in memory 
    public string TempProperty { get; set; } 

    //properties saved in the database 
    public string Name { get; set; } 
    public string Address { get; set; } 
    public int Age { get; set; } 
    public string Comments { get; set; } 

    //various methods, constructors, etc. 
} 

я должен быть в состоянии предоставить имя таблицы Data.Customer, и метод должен отобразить Customer.Name -> Data.Customers.Name, Customer.Address -> Data.Customers.Address и т.д.

ответ

0

Я был удивлен, что я не мог найти код для чего-то подобного в любом месте. Может быть, это не лучший подход к проблеме? Несмотря на эту возможность, вот решение, которое я придумал.

Мой исходный план был что-то вроде этого:

  1. Используйте таблицу назначения и исходный тип объекта для создания объекта отображения с помощью отражения.
  2. Итерировать через исходные объекты, применяя картограф, для генерации данных вставки.
  3. Вставьте данные в таблицу назначения, используя объект System.Data.SqlClient.SqlBulkCopy.

Когда я вижу схему, как это, я пытаюсь перефразировать в терминах типов данных, потому что на самом деле все, что я делаю перевод мой вход (T[]) в то, что SqlBulkCopy будет принимать. Но для этого мне нужно решить, как отображать поля.

Вот что я остановился на:

var mapper = new List<Tuple<string, Func<object, string, object>>>(); 

Я выбрал это, чтобы представить таблицу, которая выглядит следующим образом:

+------------+----------------------------------------------+ 
| Field Name |    Mapping Function    | 
+------------+----------------------------------------------+ 
| Name  | Customer.Name -> Data.Customers.Name   | 
| Address | Customer.Address -> Data.Customers.Address | 
| Age  | Customer.Age -> Data.Customers.Age   | 
| Comments | Customer.Comments -> Data.Customers.Comments | 
+------------+----------------------------------------------+ 

Тот факт, что это список представляет строки. Это оставляет нам Tuple<string, Func<object, string, object>>. Это создает своего рода словарь (но индексированный), где заданная строка (имя поля) сопоставляется с функцией, которая при заданном объекте-источнике T и исходном поле (например, Address) будет получать соответствующее значение. Если для поля таблицы не найдено соответствующего свойства, мы просто вернем null.

После проверки того, что входные значения действительны (соединение действительное, таблица существует и т. Д.), Мы создадим отображение объекта:

//get all the column names for the table to build mapping object 
SqlCommand command = new SqlCommand($"SELECT TOP 1 * FROM {foundTableName}", conn); 
SqlDataReader reader = command.ExecuteReader(); 

//build mapping object by iterating through rows and verifying that there is a match in the table 
var mapper = new List<Tuple<string, Func<object, string, object>>>(); 
foreach (DataRow col in reader.GetSchemaTable().Rows) 
{ 
    //get column information 
    string columnName = col.Field<string>("ColumnName"); 
    PropertyInfo property = typeof(T).GetProperty(columnName); 
    Func<object, string, object> map; 
    if (property == null) 
    { 
     //check if it's nullable and exit if not 
     bool nullable = col.Field<bool>("Is_Nullable"); 
     if (!nullable) 
      return $"No corresponding property found for Non-nullable field '{columnName}'."; 

     //if it's nullable, create mapping function 
     map = new Func<object, string, object>((a, b) => null); 
    } 
    else 
     map = new Func<object, string, object>((src, fld) => typeof(T).GetProperty(fld).GetValue(src)); 

    //add mapping object 
    mapper.Add(new Tuple<string, Func<object, string, object>>(columnName, map)); 
} 

Объект SqlBulkCopy принимает DataRow[] как вход, так что здесь мы можем просто создать шаблон DataRow объекты из таблицы назначения и заполнить их из нашего картографа:

//get all the data 
int dataCount = sourceData.Count(); 
var rows = new DataRow[dataCount]; 
DataTable destTableDT = new DataTable(); 
destTableDT.Load(reader); 
for (int x = 0; x < dataCount; x++) 
{ 
    var dataRow = destTableDT.NewRow(); 
    dataRow.ItemArray = mapper.Select(m => m.Item2.Invoke(sourceData[x], m.Item1)) 
           .ToArray(); 
    rows[x] = dataRow; 
} 

Затем закончить его, написав данные:

//set up the bulk copy connection 
    SqlBulkCopy sbc = new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock | 
              SqlBulkCopyOptions.UseInternalTransaction, null); 
    sbc.DestinationTableName = foundTableName; 
    sbc.BatchSize = BATCH_SIZE; 
    sbc.WriteToServer(rows); 

И это все! Работает как прелесть, и слишком быстро побежал за мной, чтобы побеспокоить бенчмаркинг (EF занимал несколько минут, чтобы запустить этот импорт).

Следует отметить, что я оставил много кода проверки ошибок и, вероятно, добавлю больше. Однако, если вы хотели бы видеть мой класс в целом, здесь:

using System; 
    using System.Collections.Generic; 
    using System.Data; 
    using System.Data.Common; 
    using System.Data.SqlClient; 
    using System.Linq; 
    using System.Reflection; 

    namespace DatabaseUtilities 
    { 
     public static class RapidDataTools 
     { 
      const int SCHEMA_SCHEMA_NAME = 1; 
      const int SCHEMA_TABLE_NAME = 2; 
      const int BATCH_SIZE = 1000; 

      /// <summary> 
      /// Imports an array of data into a specified table. It does so by mapping object properties 
      /// to table columns. Only properties with the same name as the column name will be copied; 
      /// other columns will be left null. Non-nullable columns with no corresponding property will 
      /// throw an error. 
      /// </summary> 
      /// <param name="connectionString"></param> 
      /// <param name="destTableName">Qualified table name (e.g. Admin.Table)</param> 
      /// <param name="sourceData"></param> 
      /// <returns></returns> 
      public static string Import<T>(string connectionString, string destTableName, T[] sourceData) 
      { 
       //get destination table qualified name 
       string[] tableParts = destTableName.Split('.'); 
       if (tableParts.Count() != 2) return $"Invalid or unqualified destination table name: {destTableName}."; 
       string destSchema = tableParts[0]; 
       string destTable = tableParts[1]; 

       //create the database connection 
       SqlConnection conn = GetConnection(connectionString); 
       if (conn == null) return "Invalid connection string."; 

       //establish connection 
       try { conn.Open(); } 
       catch { return "Could not connect to database using provided connection string."; } 

       //make sure the requested table exists 
       string foundTableName = string.Empty; 
       foreach (DataRow row in conn.GetSchema("Tables").Rows) 
        if (row[SCHEMA_SCHEMA_NAME].ToString().Equals(destSchema, StringComparison.CurrentCultureIgnoreCase) && 
         row[SCHEMA_TABLE_NAME].ToString().Equals(destTable, StringComparison.CurrentCultureIgnoreCase)) 
        { 
         foundTableName = $"{row[SCHEMA_SCHEMA_NAME]}.{row[SCHEMA_TABLE_NAME]}"; 
         break; 
        } 
       if (foundTableName == string.Empty) return $"Specified table '{destTableName}' could not be found in table."; 

       //get all the column names for the table to build mapping object 
       SqlCommand command = new SqlCommand($"SELECT TOP 1 * FROM {foundTableName}", conn); 
       SqlDataReader reader = command.ExecuteReader(); 

       //build mapping object by iterating through rows and verifying that there is a match in the table 
       var mapper = new List<Tuple<string, Func<object, string, object>>>(); 
       foreach (DataRow col in reader.GetSchemaTable().Rows) 
       { 
        //get column information 
        string columnName = col.Field<string>("ColumnName"); 
        PropertyInfo property = typeof(T).GetProperty(columnName); 
        Func<object, string, object> map; 
        if (property == null) 
        { 
         //check if it's nullable and exit if not 
         bool nullable = col.Field<bool>("Is_Nullable"); 
         if (!nullable) 
          return $"No corresponding property found for Non-nullable field '{columnName}'."; 

         //if it's nullable, create mapping function 
         map = new Func<object, string, object>((a, b) => null); 
        } 
        else 
         map = new Func<object, string, object>((src, fld) => typeof(T).GetProperty(fld).GetValue(src)); 

        //add mapping object 
        mapper.Add(new Tuple<string, Func<object, string, object>>(columnName, map)); 
       } 

       //get all the data 
       int dataCount = sourceData.Count(); 
       var rows = new DataRow[dataCount]; 
       DataTable destTableDT = new DataTable(); 
       destTableDT.Load(reader); 
       for (int x = 0; x < dataCount; x++) 
       { 
        var dataRow = destTableDT.NewRow(); 
        dataRow.ItemArray = mapper.Select(m => m.Item2.Invoke(sourceData[x], m.Item1)).ToArray(); 
        rows[x] = dataRow; 
       } 

       //close the old connection 
       conn.Close(); 

       //set up the bulk copy connection 
       SqlBulkCopy sbc = new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.UseInternalTransaction, null); 
       sbc.DestinationTableName = foundTableName; 
       sbc.BatchSize = BATCH_SIZE; 

       //establish connection 
       try { conn.Open(); } 
       catch { return "Failed to re-established connection to the database after reading data."; } 

       //write data 
       try { sbc.WriteToServer(rows); } 
       catch (Exception ex) { return $"Batch write failed. Details: {ex.Message} - {ex.StackTrace}"; } 

       //if we got here, everything worked! 
       return string.Empty; 
      } 

      private static SqlConnection GetConnection(string connectionString) 
      { 
       DbConnectionStringBuilder csb = new DbConnectionStringBuilder(); 
       try { csb.ConnectionString = connectionString; } 
       catch { return null; } 
       return new SqlConnection(csb.ConnectionString); 
      } 
     } 
    } 
0

Существует в основном три подхода для выполнения массовой вставки в Entity Framework

  • Entity Framework Расширения (рекомендуется)
  • EntityFramework.BulkInsert (Limited и неподдерживаемый)
  • SqlBulkCopy (комплекс, чтобы сделать его работу)

Первая 2 библиотека добавить методы расширения к DbContext

using (var ctx = new EntitiesContext()) 
{ 
    ctx.BulkInsert(list); 
} 

Вы можете узнать больше о трех методах в этой article

Отказ от ответственности: Я владелец проекта Entity Framework Extensions

+0

Очень круто! Я видел это во время своего поиска, но не любил продавать оплачиваемое продление моему работодателю. – Daniel

0

Посмотрите на пакет NuGet «EntityFramework.Utilities», вы можете добавить его в свой проект. У этого есть особенности для этого. Вам нужно будет немного перекодировки, но это, вероятно, принесет пользу вам в конце.

Ссылки:

Смежные вопросы