Skip to content

Commit 743740e

Browse files
committed
add upsert/merge feature.
1 parent a5abc50 commit 743740e

File tree

11 files changed

+319
-14
lines changed

11 files changed

+319
-14
lines changed
Binary file not shown.
128 KB
Binary file not shown.

EntityFramework.Utilities/EntityFramework.Utilities/ColumnMapping.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ public class ColumnMapping
1212
public string DataType { get; set; }
1313

1414
public bool IsPrimaryKey { get; set; }
15+
public bool IsStoreGeneratedIdentity { get; set; }
1516
}
1617
}

EntityFramework.Utilities/EntityFramework.Utilities/EFBatchOperation.cs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ public interface IEFBatchOperationBase<TContext, T> where T : class
3434
/// <param name="connection">The DbConnection to use for the insert. Only needed when for example a profiler wraps the connection. Then you need to provide a connection of the type the provider use.</param>
3535
/// <param name="batchSize">The size of each batch. Default depends on the provider. SqlProvider uses 15000 as default</param>
3636
void UpdateAll<TEntity>(IEnumerable<TEntity> items, Action<UpdateSpecification<TEntity>> updateSpecification, DbConnection connection = null, int? batchSize = null) where TEntity : class, T;
37+
38+
/// <summary>
39+
/// provider batch upsert operation
40+
/// SQL:
41+
/// merge into [(the table of source entity)] as Target
42+
/// using (tempTable) as Source
43+
/// on <paramref name="identitySpecification"/>
44+
/// when matched then
45+
/// update set <paramref name="whenMatchedUpdateSpecification"/>
46+
/// when not matched then
47+
/// insert ...;
48+
/// </summary>
49+
/// <typeparam name="TEntity"></typeparam>
50+
/// <param name="items"></param>
51+
/// <param name="identitySpecification">match identity specification. if parameter is null, use primary key as default</param>
52+
/// <param name="whenMatchedUpdateSpecification">update specification when matched by <paramref name="identitySpecification"/>. if parameter is null, update all columns except primary key</param>
53+
/// <param name="connection"></param>
54+
/// <param name="batchSize"></param>
55+
void MergeAll<TEntity>(IEnumerable<TEntity> items, Action<IdentitySpecification<TEntity>> identitySpecification = null,
56+
Action<UpdateSpecification<TEntity>> whenMatchedUpdateSpecification = null, DbConnection connection = null, int? batchSize = null) where TEntity : class, T;
3757
}
3858

3959
public class UpdateSpecification<T>
@@ -52,6 +72,22 @@ public UpdateSpecification<T> ColumnsToUpdate(params Expression<Func<T, object>>
5272
public Expression<Func<T, object>>[] Properties { get; set; }
5373
}
5474

75+
public class IdentitySpecification<T>
76+
{
77+
/// <summary>
78+
/// Set each column you use to identity.
79+
/// </summary>
80+
/// <param name="properties"></param>
81+
/// <returns></returns>
82+
public IdentitySpecification<T> ColumnsToIdentity(params Expression<Func<T, object>>[] properties)
83+
{
84+
Properties = properties;
85+
return this;
86+
}
87+
88+
public Expression<Func<T, object>>[] Properties { get; set; }
89+
}
90+
5591
public interface IEFBatchOperationFiltered<TContext, T>
5692
{
5793
int Delete();
@@ -161,6 +197,7 @@ public void UpdateAll<TEntity>(IEnumerable<TEntity> items, Action<UpdateSpecific
161197
NameInDatabase = p.ColumnName,
162198
NameOnObject = p.PropertyName,
163199
DataType = p.DataTypeFull,
200+
IsStoreGeneratedIdentity = p.IsStoreGeneratedIdentity,
164201
IsPrimaryKey = p.IsPrimaryKey
165202
}).ToList();
166203

@@ -175,6 +212,70 @@ public void UpdateAll<TEntity>(IEnumerable<TEntity> items, Action<UpdateSpecific
175212
}
176213
}
177214

215+
public void MergeAll<TEntity>(IEnumerable<TEntity> items, Action<IdentitySpecification<TEntity>> identitySpecification, Action<UpdateSpecification<TEntity>> updateSpecification, DbConnection connection, int? batchSize)
216+
where TEntity : class, T
217+
{
218+
var con = context.Connection as EntityConnection;
219+
if (con == null && connection == null)
220+
{
221+
Configuration.Log("No provider could be found because the Connection didn't implement System.Data.EntityClient.EntityConnection");
222+
throw new InvalidOperationException("No provider supporting the upsert operation");
223+
}
224+
225+
var connectionToUse = connection ?? con.StoreConnection;
226+
var currentType = typeof(TEntity);
227+
var provider = Configuration.Providers.FirstOrDefault(p => p.CanHandle(connectionToUse));
228+
if (provider != null && provider.CanBulkUpdate)
229+
{
230+
231+
var mapping = EfMappingFactory.GetMappingsForContext(this.dbContext);
232+
var typeMapping = mapping.TypeMappings[typeof(T)];
233+
var tableMapping = typeMapping.TableMappings.First();
234+
235+
var properties = tableMapping.PropertyMappings
236+
.Where(p => currentType.IsSubclassOf(p.ForEntityType) || p.ForEntityType == currentType)
237+
.Select(p => new ColumnMapping
238+
{
239+
NameInDatabase = p.ColumnName,
240+
NameOnObject = p.PropertyName,
241+
DataType = p.DataTypeFull,
242+
IsPrimaryKey = p.IsPrimaryKey,
243+
IsStoreGeneratedIdentity = p.IsStoreGeneratedIdentity,
244+
}).ToList();
245+
246+
HashSet<string> columnsToMatch;
247+
if (identitySpecification != null)
248+
{
249+
var identity = new IdentitySpecification<TEntity>();
250+
identitySpecification(identity);
251+
columnsToMatch = new HashSet<string>(identity.Properties.Select(p => p.GetPropertyName()));
252+
}
253+
else
254+
{
255+
columnsToMatch = new HashSet<string>(properties.Where(p => p.IsPrimaryKey).Select(p => p.NameOnObject));
256+
}
257+
258+
HashSet<string> columnsToUpdate;
259+
if (updateSpecification != null)
260+
{
261+
var spec = new UpdateSpecification<TEntity>();
262+
updateSpecification(spec);
263+
columnsToUpdate = new HashSet<string>(spec.Properties.Select(p => p.GetPropertyName()));
264+
}
265+
else
266+
{
267+
columnsToUpdate = new HashSet<string>(properties.Where(p => !p.IsPrimaryKey).Select(p => p.NameOnObject));
268+
}
269+
270+
provider.UpsertImtes(items, tableMapping.Schema, tableMapping.TableName, properties, connectionToUse, batchSize, columnsToMatch, columnsToUpdate);
271+
}
272+
else
273+
{
274+
Configuration.Log("Found provider: " + (provider == null ? "[]" : provider.GetType().Name) + " for " + connectionToUse.GetType().Name);
275+
throw new InvalidOperationException("No provider supporting the upsert operation");
276+
}
277+
}
278+
178279
public IEFBatchOperationFiltered<TContext, T> Where(Expression<Func<T, bool>> predicate)
179280
{
180281
this.predicate = predicate;
@@ -247,6 +348,6 @@ public int Update<TP>(Expression<Func<T, TP>> prop, Expression<Func<T, TP>> modi
247348
}
248349

249350

250-
351+
251352
}
252353
}

EntityFramework.Utilities/EntityFramework.Utilities/IQueryProvider.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Data.Common;
4-
using System.Linq;
5-
using System.Text;
63

74
namespace EntityFramework.Utilities
85
{
@@ -17,6 +14,7 @@ public interface IQueryProvider
1714
string GetUpdateQuery(QueryInformation predicateQueryInfo, QueryInformation modificationQueryInfo);
1815
void InsertItems<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize);
1916
void UpdateItems<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize, UpdateSpecification<T> updateSpecification);
17+
void UpsertImtes<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize, HashSet<string> identitySpecification, HashSet<string> updateSpecification);
2018

2119
bool CanHandle(DbConnection storeConnection);
2220

EntityFramework.Utilities/EntityFramework.Utilities/MappingHelper.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
using System.Data.Entity.Infrastructure;
1111
using System.IO;
1212
using System.Linq;
13+
using System.Reflection;
1314
using System.Xml;
1415
using System.Xml.Linq;
15-
using System.Reflection;
1616

1717
namespace EntityFramework.Utilities
1818
{
@@ -94,6 +94,7 @@ public class PropertyMapping
9494
public bool IsPrimaryKey { get; set; }
9595

9696
public string DataTypeFull { get; set; }
97+
public bool IsStoreGeneratedIdentity { get; set; }
9798
}
9899

99100
/// <summary>
@@ -122,7 +123,7 @@ public EfMapping(DbContext db)
122123
var conceptualContainer = metadata.GetItems<EntityContainer>(DataSpace.CSpace).Single();
123124

124125
// Storage part of the model has info about the shape of our tables
125-
var storeContainer = metadata.GetItems<EntityContainer>(DataSpace.SSpace).Single();
126+
var storeContainer = metadata.GetItems(DataSpace.SSpace).OfType<EntityType>();
126127

127128
// Object part of the model that contains info about the actual CLR types
128129
var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));
@@ -220,6 +221,11 @@ public EfMapping(DbContext db)
220221
if ((mappingToLookAt.EntityType ?? mappingToLookAt.IsOfEntityTypes[0]).KeyProperties.Any(p => p.Name == item.PropertyName))
221222
{
222223
item.IsPrimaryKey = true;
224+
item.IsStoreGeneratedIdentity = storeContainer.FirstOrDefault(t => t.Name == item.ForEntityType.Name)
225+
?.Properties
226+
?.FirstOrDefault(p => p.Name == item.ColumnName)
227+
?.IsStoreGeneratedIdentity
228+
?? false;
223229
}
224230
}
225231
}

EntityFramework.Utilities/EntityFramework.Utilities/SqlQueryProvider.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Data.Common;
44
using System.Data.SqlClient;
55
using System.Linq;
6-
using System.Text;
76
using System.Text.RegularExpressions;
87

98
namespace EntityFramework.Utilities
@@ -83,7 +82,7 @@ public void InsertItems<T>(IEnumerable<T> items, string schema, string tableName
8382

8483
public void UpdateItems<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize, UpdateSpecification<T> updateSpecification)
8584
{
86-
var tempTableName = "temp_" + tableName + "_" + DateTime.Now.Ticks;
85+
var tempTableName = "#temp_" + tableName + "_" + DateTime.Now.Ticks;
8786
var columnsToUpdate = updateSpecification.Properties.Select(p => p.GetPropertyName()).ToDictionary(x => x);
8887
var filtered = properties.Where(p => columnsToUpdate.ContainsKey(p.NameOnObject) || p.IsPrimaryKey).ToList();
8988
var columns = filtered.Select(c => "[" + c.NameInDatabase + "] " + c.DataType);
@@ -110,9 +109,14 @@ INNER JOIN
110109
ON
111110
{2}", tableName, tempTableName, filter, setters);
112111

112+
var dropCommand = $@"IF Object_id('tempdb..{tempTableName}') IS NOT NULL
113+
BEGIN DROP TABLE {tempTableName} END
114+
ELSE
115+
BEGIN THROW 51000,'Drop temp table {tempTableName} fail.',1; END";
116+
113117
using (var createCommand = new SqlCommand(str, con))
114118
using (var mCommand = new SqlCommand(mergeCommand, con))
115-
using (var dCommand = new SqlCommand(string.Format("DROP table {0}.[{1}]", schema, tempTableName), con))
119+
using (var dCommand = new SqlCommand(dropCommand, con))
116120
{
117121
createCommand.ExecuteNonQuery();
118122
InsertItems(items, schema, tempTableName, filtered, storeConnection, batchSize);
@@ -123,6 +127,56 @@ INNER JOIN
123127

124128
}
125129

130+
public void UpsertImtes<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize, HashSet<string> columnsToIdentity, HashSet<string> columnsToUpdate)
131+
{
132+
var tempTableName = "#temp_" + tableName + "_" + DateTime.Now.Ticks;
133+
134+
var str = $@"CREATE TABLE {schema}.[{tempTableName}] (
135+
{string.Join(", ", properties.Select(c => "[" + c.NameInDatabase + "] " + c.DataType))},
136+
PRIMARY KEY ({string.Join(", ", properties.Where(p => p.IsPrimaryKey).Select(c => "[" + c.NameInDatabase + "]"))})
137+
)";
138+
139+
var con = storeConnection as SqlConnection;
140+
if (con.State != System.Data.ConnectionState.Open)
141+
{
142+
con.Open();
143+
}
144+
145+
var insertProperties = properties.Where(p => !p.IsStoreGeneratedIdentity).Select(p => p.NameInDatabase).ToArray();
146+
string mergeCommand =
147+
$@"merge into [{tableName}] as Target
148+
using {tempTableName} as Source
149+
on {string.Join(" and ", properties
150+
.Where(p => columnsToIdentity.Contains(p.NameOnObject))
151+
.Select(p => $"Target.{p.NameInDatabase}=Source.{p.NameInDatabase}"))}
152+
when matched then
153+
update set {string.Join(",", properties
154+
.Where(p => columnsToUpdate.Contains(p.NameOnObject) && !p.IsPrimaryKey)
155+
.Select(p => "Target.[" + p.NameInDatabase + "] = Source.[" + p.NameInDatabase + "]"))}
156+
when not matched then
157+
insert (
158+
{string.Join(",", insertProperties)}
159+
) values (
160+
{string.Join(",", insertProperties.Select(p=>$"Source.{p}"))}
161+
);";
162+
163+
164+
var dropCommand = $@"IF Object_id('tempdb..{tempTableName}') IS NOT NULL
165+
BEGIN DROP TABLE {tempTableName} END
166+
ELSE
167+
BEGIN THROW 51000,'Drop temp table {tempTableName} fail.',1; END";
168+
169+
using (var createCommand = new SqlCommand(str, con))
170+
using (var mCommand = new SqlCommand(mergeCommand, con))
171+
using (var dCommand = new SqlCommand(dropCommand, con))
172+
{
173+
createCommand.ExecuteNonQuery();
174+
InsertItems(items, schema, tempTableName, properties, storeConnection, batchSize);
175+
mCommand.ExecuteNonQuery();
176+
dCommand.ExecuteNonQuery();
177+
}
178+
}
179+
126180

127181
public bool CanHandle(System.Data.Common.DbConnection storeConnection)
128182
{

EntityFramework.Utilities/Tests/InsertTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.VisualStudio.TestTools.UnitTesting;
55
using Tests.FakeDomain;
66
using Tests.FakeDomain.Models;
7-
using System;
87

98
namespace Tests
109
{

0 commit comments

Comments
 (0)