Skip to content

Commit 95a7b4b

Browse files
committed
added bulk save logic
1 parent 29aba46 commit 95a7b4b

2 files changed

Lines changed: 216 additions & 23 deletions

File tree

src/GenericSQLEntityHandler/GenericSQLEntityHandler.cs

Lines changed: 212 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Data;
44
using System.Data.SqlClient;
55
using System.Diagnostics;
6+
using System.Linq;
67
using System.Reflection;
78
using System.Threading;
89

@@ -45,18 +46,193 @@ public GenericSQLEntityHandler(SqlConnection sqlConnection)
4546

4647
#region Save Methods
4748

48-
/// <summary>
49-
/// Saves a single entity to the database.
50-
/// </summary>
51-
/// <param name="entity">The entity to save.</param>
52-
/// <param name="table">The name of the table to save to.</param>
53-
/// <param name="identityColumns">Only used for Update and InsertOrUpdate. Must contain the columns that identify the entity (Case sensitive).</param>
54-
/// <param name="saveType">To save to or update the database, InsertOrUpdate handles a mixed list, but has a little overhead. FastInsert will not select the autogen column after an insert.</param>
55-
/// <param name="autoGenIdColumn">If an autoincrement column is in the table, on insert it sets the new id to the inserted entitys corresponding property (Case sensitive).</param>
56-
/// <returns>Returns true if the entity is saved/updated, else false.</returns>
57-
public bool SaveEntity<T>(T entity, string table, string[] identityColumns, SaveType saveType, string autoGenIdColumn) where T : class
49+
#region Bulk Save
50+
public bool BulkInsert<T>(ICollection<T> entityList, string tableName, string[] columnNames) where T : class
51+
{
52+
return BulkInsert(entityList, tableName, columnNames, null);
53+
}
54+
55+
public bool BulkInsert<T>(ICollection<T> entityList, string tableName, string[] columnNames, string identityColumn) where T : class
56+
{
57+
return BulkSave(entityList, tableName, columnNames, identityColumn, false);
58+
}
59+
60+
public bool BulkUpdate<T>(ICollection<T> entityList, string tableName, string[] columnNames, string identityColumn) where T : class
61+
{
62+
return BulkSave(entityList, tableName, columnNames, identityColumn, true);
63+
}
64+
public bool BulkSave<T>(ICollection<T> entities, string tableName, string[] columnNames, string identityColumn,
65+
bool update, int bulkCopyTimeout = 600) where T : class
66+
{
67+
bool succes = false;
68+
69+
try
70+
{
71+
string destinationTableName;
72+
bool identity = !string.IsNullOrEmpty(identityColumn);
73+
if (identity)
74+
destinationTableName = "#tmp_" + tableName;
75+
else
76+
destinationTableName = tableName;
77+
78+
Type entityType = typeof(T);
79+
List<PropertyInfo> propertyInfos = new List<PropertyInfo>();
80+
if(columnNames != null && columnNames.Length > 0)
81+
{
82+
foreach (string columnName in columnNames)
83+
{
84+
propertyInfos.Add(entityType.GetProperty(columnName));
85+
}
86+
}
87+
else
88+
{
89+
SqlCommand tempCommand = GetSqlCommand();
90+
Dictionary<string, string> tableColumns = GetTableColumns(tempCommand, tableName, entityType);
91+
tempCommand.Dispose();
92+
PropertyInfo[] allPropertyInfoss = entityType.GetProperties();
93+
foreach (PropertyInfo propertyInfo in allPropertyInfoss)
94+
{
95+
if (tableColumns.ContainsKey(propertyInfo.Name.ToLower()))
96+
{
97+
propertyInfos.Add(propertyInfo);
98+
}
99+
}
100+
tempCommand.Dispose();
101+
}
102+
103+
104+
105+
SqlCommand cmd = GetSqlCommand();
106+
SqlBulkCopy copy = new SqlBulkCopy(sqlConnection, update || identity ? SqlBulkCopyOptions.KeepIdentity : SqlBulkCopyOptions.Default, sqlTransaction);
107+
copy.BulkCopyTimeout = bulkCopyTimeout;
108+
109+
try
110+
{
111+
112+
DataTable table = new DataTable();
113+
string columnNamesForQuery = "";
114+
foreach (PropertyInfo property in propertyInfos)
115+
{
116+
if (!identity || property.Name.ToLower() != identityColumn.ToLower())
117+
{
118+
if (!update)
119+
columnNamesForQuery += "[" + property.Name + "],";
120+
else
121+
columnNamesForQuery += "[" + tableName + "].[" + property.Name + "]=" + destinationTableName + ".[" + property.Name + "],";
122+
}
123+
if (property.PropertyType == typeof(int?))
124+
{
125+
DataColumn col = new DataColumn(property.Name, typeof(int));
126+
col.AllowDBNull = true;
127+
table.Columns.Add(col);
128+
}
129+
else
130+
table.Columns.Add(property.Name, property.PropertyType);
131+
copy.ColumnMappings.Add(property.Name, property.Name);
132+
}
133+
columnNamesForQuery = columnNamesForQuery.TrimEnd(',');
134+
DataRow row;
135+
int rowNumber = 1000;
136+
foreach (T entity in entities)
137+
{
138+
row = table.NewRow();
139+
int index = 0;
140+
foreach (PropertyInfo property in propertyInfos)
141+
{
142+
if (!update && identity && property.Name.ToLower() == identityColumn.ToLower())
143+
{
144+
property.SetValue(entity, rowNumber, null);
145+
}
146+
object val = property.GetValue(entity, null);
147+
if (val == null)
148+
row[index] = DBNull.Value;
149+
else
150+
row[index] = val;
151+
index++;
152+
}
153+
table.Rows.Add(row);
154+
rowNumber++;
155+
}
156+
157+
copy.DestinationTableName = destinationTableName;
158+
copy.BatchSize = entities.Count;
159+
160+
if (identity)
161+
{
162+
//Check if the temp table exists for some reason and then just reuse it if it does otherwise create it
163+
cmd.CommandText = "IF OBJECT_ID('tempdb.." + destinationTableName + "') IS NULL BEGIN SELECT 0 END ELSE SELECT 1";
164+
if ((int)cmd.ExecuteScalar() == 0)
165+
CreateTableClone(tableName, destinationTableName, entityType);
166+
else
167+
cmd.CommandText = "TRUNCATE TABLE " + destinationTableName;
168+
169+
copy.WriteToServer(table);
170+
if (!update)
171+
{
172+
cmd.CommandText = "DECLARE @tmpIdTable TABLE (id INT) ";
173+
cmd.CommandText += "INSERT INTO [" + tableName + "] (" + columnNamesForQuery + ") ";
174+
cmd.CommandText += "OUTPUT inserted.[" + identityColumn + "] INTO @tmpIdTable ";
175+
cmd.CommandText += "SELECT " + columnNamesForQuery + " FROM " + destinationTableName;
176+
cmd.CommandText += " ORDER BY [" + identityColumn + "] ";
177+
cmd.CommandText += "SELECT [" + identityColumn + "] FROM @tmpIdTable ORDER BY [" + identityColumn + "]";
178+
using (SqlDataReader reader = cmd.ExecuteReader())
179+
{
180+
IEnumerator<T> enumerator = entities.GetEnumerator();
181+
while (reader.Read())
182+
{
183+
enumerator.MoveNext();
184+
entityType.GetProperty(identityColumn).SetValue(enumerator.Current, reader[0], null);
185+
}
186+
}
187+
}
188+
else
189+
{
190+
cmd.CommandText = "UPDATE [" + tableName + "] SET " + columnNamesForQuery + " FROM " + destinationTableName;
191+
cmd.CommandText += " WHERE " + destinationTableName + ".[" + identityColumn + "]=[" + tableName + "].[" + identityColumn + "]";
192+
succes = cmd.ExecuteNonQuery() == entities.Count;
193+
}
194+
cmd.CommandText = "DROP TABLE " + destinationTableName;
195+
cmd.ExecuteNonQuery();
196+
cmd.Dispose();
197+
}
198+
else
199+
copy.WriteToServer(table);
200+
201+
succes = true;
202+
}
203+
finally
204+
{
205+
copy.Close();
206+
}
207+
}
208+
catch (Exception exception)
209+
{
210+
string errorMessage = GenerateErrorMessage<T>(entities.Count > 0 ? entities.First().GetType() : null, tableName, (update ? SaveType.Update : SaveType.Insert), exception.Message,
211+
exception.StackTrace);
212+
213+
214+
215+
Debug.WriteLine(errorMessage);
216+
ErrorOccurred?.Invoke(exception);
217+
}
218+
219+
return succes;
220+
}
221+
222+
#endregion Bulk Save
223+
224+
/// <summary>
225+
/// Saves a single entity to the database.
226+
/// </summary>
227+
/// <param name="entity">The entity to save.</param>
228+
/// <param name="table">The name of the table to save to.</param>
229+
/// <param name="identityColumns">Only used for Update and InsertOrUpdate. Must contain the columns that identify the entity (Case sensitive).</param>
230+
/// <param name="saveType">To save to or update the database, InsertOrUpdate handles a mixed list, but has a little overhead. FastInsert will not select the autogen column after an insert.</param>
231+
/// <param name="autoGenIdColumn">If an autoincrement column is in the table, on insert it sets the new id to the inserted entitys corresponding property (Case sensitive).</param>
232+
/// <returns>Returns true if the entity is saved/updated, else false.</returns>
233+
public bool SaveEntity<T>(T entity, string tableName, string[] identityColumns, SaveType saveType, string autoGenIdColumn) where T : class
58234
{
59-
return SaveEntityList(new List<T>(new[] { entity }), table, identityColumns, saveType, autoGenIdColumn, false, null);
235+
return SaveEntityList(new List<T>(new[] { entity }), tableName, identityColumns, saveType, autoGenIdColumn, false, null);
60236
}
61237

62238
/// <summary>
@@ -68,9 +244,9 @@ public bool SaveEntity<T>(T entity, string table, string[] identityColumns, Save
68244
/// <param name="saveType">To save or update the database, InsertOrUpdate handles a mixed list, but has a little overhead. FastInsert will not select the autogen column after an insert.</param>
69245
/// <param name="autoGenIdColumn">If an autoincrement column is in the table, on insert it sets the new id to the inserted entitys corresponding property (Case sensitive).</param>
70246
/// <returns>Returns true if all entities are saved/updated, else false.</returns>
71-
public bool SaveEntities<T>(List<T> entityList, string table, string[] identityColumns, SaveType saveType, string autoGenIdColumn) where T : class
247+
public bool SaveEntities<T>(List<T> entityList, string tableName, string[] identityColumns, SaveType saveType, string autoGenIdColumn) where T : class
72248
{
73-
return SaveEntityList(entityList, table, identityColumns, saveType, autoGenIdColumn, false, null);
249+
return SaveEntityList(entityList, tableName, identityColumns, saveType, autoGenIdColumn, false, null);
74250
}
75251

76252
/// <summary>
@@ -84,12 +260,12 @@ public bool SaveEntities<T>(List<T> entityList, string table, string[] identityC
84260
/// <param name="fetchAutoGenIdColumnOnUpdate">True if the auto gen id column should be fetched on update, on insert it already does</param>
85261
/// <param name="propertiesToIgnore">Specify an array of property names, that should not be inserted/updated in the DB</param>
86262
/// <returns>Returns true if all entities are saved/updated, else false.</returns>
87-
public bool SaveEntities<T>(List<T> entityList, string table, string[] identityColumns, SaveType saveType, string autoGenIdColumn, bool fetchAutoGenIdColumnOnUpdate, string[] propertiesToIgnore) where T : class
263+
public bool SaveEntities<T>(List<T> entityList, string tableName, string[] identityColumns, SaveType saveType, string autoGenIdColumn, bool fetchAutoGenIdColumnOnUpdate, string[] propertiesToIgnore) where T : class
88264
{
89-
return SaveEntityList(entityList, table, identityColumns, saveType, autoGenIdColumn, fetchAutoGenIdColumnOnUpdate, propertiesToIgnore);
265+
return SaveEntityList(entityList, tableName, identityColumns, saveType, autoGenIdColumn, fetchAutoGenIdColumnOnUpdate, propertiesToIgnore);
90266
}
91267

92-
private bool SaveEntityList<T>(List<T> entities, string table, string[] identityColumns, SaveType saveType,
268+
private bool SaveEntityList<T>(List<T> entities, string tableName, string[] identityColumns, SaveType saveType,
93269
string autoGenIdColumn, bool fetchAutoGenIdColumnOnUpdate, string[] propertiesToIgnore) where T : class
94270
{
95271
try
@@ -121,7 +297,7 @@ private bool SaveEntityList<T>(List<T> entities, string table, string[] identity
121297
if (identityColumns.Length != 1)
122298
throw new Exception("Only one identityColumn supported when using fetchAutoGenIdColumnOnUpdate");
123299
cmd.Parameters.Clear();
124-
cmd.CommandText = "SELECT " + identityColumns[0] + ", " + autoGenIdColumn + " FROM " + table;
300+
cmd.CommandText = "SELECT " + identityColumns[0] + ", " + autoGenIdColumn + " FROM " + tableName;
125301
using (SqlDataReader reader = cmd.ExecuteReader())
126302
{
127303
while (reader.Read())
@@ -133,7 +309,7 @@ private bool SaveEntityList<T>(List<T> entities, string table, string[] identity
133309
}
134310

135311
//fetch table columns
136-
Dictionary<string, string> tableColumns = GetTableColumns(cmd, table, entityType);
312+
Dictionary<string, string> tableColumns = GetTableColumns(cmd, tableName, entityType);
137313

138314

139315
if (saveType == SaveType.Insert)
@@ -150,9 +326,9 @@ private bool SaveEntityList<T>(List<T> entities, string table, string[] identity
150326
// - generate the SQL
151327
cmd.Parameters.Clear();
152328
if (!string.IsNullOrEmpty(autoGenIdColumn))
153-
cmd.CommandText = "SELECT TOP 1 [" + autoGenIdColumn + "] FROM [" + table + "] WHERE ";
329+
cmd.CommandText = "SELECT TOP 1 [" + autoGenIdColumn + "] FROM [" + tableName + "] WHERE ";
154330
else
155-
cmd.CommandText = "SELECT TOP 1 [" + identityColumns[0] + "] FROM [" + table + "] WHERE ";
331+
cmd.CommandText = "SELECT TOP 1 [" + identityColumns[0] + "] FROM [" + tableName + "] WHERE ";
156332
foreach (string idColumn in identityColumns)
157333
{
158334
cmd.CommandText += idColumn + " = @" + idColumn + " AND ";
@@ -211,7 +387,7 @@ private bool SaveEntityList<T>(List<T> entities, string table, string[] identity
211387
bool autoGenIdIsGuid = false;
212388
List<PropertyInfo> propertiesToInsert = new List<PropertyInfo>();
213389

214-
string sqlInsertInto = "INSERT INTO [" + table + "](";
390+
string sqlInsertInto = "INSERT INTO [" + tableName + "](";
215391
string sqlParameters = "VALUES(";
216392
// find the columns to insert into
217393
PropertyInfo[] propertyInfos = entityType.GetProperties();
@@ -295,7 +471,7 @@ private bool SaveEntityList<T>(List<T> entities, string table, string[] identity
295471
if (entitiesToUpdate.Count > 0)
296472
{
297473
cmd.Parameters.Clear();
298-
string sqlUpdate = "UPDATE [" + table + "] SET ";
474+
string sqlUpdate = "UPDATE [" + tableName + "] SET ";
299475
string sqlWhere = " WHERE ";
300476
List<PropertyInfo> whereProperties = new List<PropertyInfo>();
301477
List<PropertyInfo> setProperties = new List<PropertyInfo>();
@@ -360,7 +536,7 @@ private bool SaveEntityList<T>(List<T> entities, string table, string[] identity
360536
}
361537
catch (Exception exception)
362538
{
363-
string errorMessage = GenerateErrorMessage<T>(entities.Count > 0 ? entities[0].GetType() : null, table, saveType, exception.Message,
539+
string errorMessage = GenerateErrorMessage<T>(entities.Count > 0 ? entities[0].GetType() : null, tableName, saveType, exception.Message,
364540
exception.StackTrace);
365541

366542
Debug.WriteLine(errorMessage);
@@ -462,6 +638,19 @@ private void AddParameterToCmd(object fieldObj, SqlCommand cmd, string idColumn)
462638
cmd.Parameters.AddWithValue("@" + idColumn, fieldObj ?? DBNull.Value);
463639
}
464640

641+
public bool CreateTableClone(string sourceTableName, string newName, Type entityType)
642+
{
643+
SQLTableCreator creator = new SQLTableCreator(sqlConnection, sqlTransaction);
644+
SqlCommand cmd = GetSqlCommand();
645+
646+
cmd.CommandText = "SELECT TOP 1 * FROM [" + sourceTableName + "]";
647+
SqlDataReader reader = cmd.ExecuteReader();
648+
DataTable dataTable = reader.GetSchemaTable();
649+
reader.Close();
650+
cmd.Dispose();
651+
creator.Create(dataTable, newName, entityType);
652+
return true;
653+
}
465654
#endregion Helper Methods
466655
}
467656
}

src/GenericSQLEntityHandler/SQLTableCreator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public SQLTableCreator(SqlConnection sqlConnection, SqlTransaction sqlTransactio
3838
#endregion Constructor
3939

4040
#region Methods
41+
public bool Create(DataTable schema, string destinationTableName, Type entityType)
42+
{
43+
return Create(schema, null, destinationTableName, entityType);
44+
}
4145
public bool Create(DataTable schema, int[] primaryKeys, string destinationTableName, Type entityType)
4246
{
4347
try

0 commit comments

Comments
 (0)