Skip to content

Commit 6dfaaeb

Browse files
Bulk updates Issue MikaelEliasson#39
1 parent c22ccee commit 6dfaaeb

File tree

12 files changed

+206
-144
lines changed

12 files changed

+206
-144
lines changed

EntityFramework.Utilities/EntityFramework.Utilities/ColumnMapping.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@ public class ColumnMapping
88
public string NameOnObject { get; set; }
99
public string StaticValue { get; set; }
1010
public string NameInDatabase { get; set; }
11+
12+
public string DataType { get; set; }
13+
14+
public bool IsPrimaryKey { get; set; }
1115
}
1216
}

EntityFramework.Utilities/EntityFramework.Utilities/Configuration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ static Configuration(){
1212
Providers.Add(new SqlQueryProvider());
1313

1414
Log = m => { };
15+
16+
DisableDefaultFallback = true;
1517

1618
}
1719

EntityFramework.Utilities/EntityFramework.Utilities/EFBatchOperation.cs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ public interface IEFBatchOperationBase<TContext, T> where T : class
2323
/// <param name="batchSize">The size of each batch. Default depends on the provider. SqlProvider uses 15000 as default</param>
2424
void InsertAll<TEntity>(IEnumerable<TEntity> items, DbConnection connection = null, int? batchSize = null) where TEntity : class, T;
2525
IEFBatchOperationFiltered<TContext, T> Where(Expression<Func<T, bool>> predicate);
26+
27+
28+
/// <summary>
29+
/// Bulk update all items if the Provider supports it. Otherwise it will use the default update unless Configuration.DisableDefaultFallback is set to true in which case it would throw an exception.
30+
/// </summary>
31+
/// <typeparam name="TEntity"></typeparam>
32+
/// <param name="items">The items to update</param>
33+
/// <param name="updateSpecification">Define which columns to update</param>
34+
/// <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>
35+
/// <param name="batchSize">The size of each batch. Default depends on the provider. SqlProvider uses 15000 as default</param>
36+
void UpdateAll<TEntity>(IEnumerable<TEntity> items, Action<UpdateSpecification<TEntity>> updateSpecification, DbConnection connection = null, int? batchSize = null) where TEntity : class, T;
37+
}
38+
39+
public class UpdateSpecification<T>
40+
{
41+
/// <summary>
42+
/// Set each column you want to update, Columns that belong to the primary key cannot be updated.
43+
/// </summary>
44+
/// <param name="properties"></param>
45+
/// <returns></returns>
46+
public UpdateSpecification<T> ColumnsToUpdate(params Expression<Func<T, object>>[] properties)
47+
{
48+
Properties = properties;
49+
return this;
50+
}
51+
52+
public Expression<Func<T, object>>[] Properties { get; set; }
2653
}
2754

2855
public interface IEFBatchOperationFiltered<TContext, T>
@@ -71,7 +98,7 @@ public static IEFBatchOperationBase<TContext, T> For<TContext, T>(TContext conte
7198
public void InsertAll<TEntity>(IEnumerable<TEntity> items, DbConnection connection = null, int? batchSize = null) where TEntity : class, T
7299
{
73100
var con = context.Connection as EntityConnection;
74-
if (con == null)
101+
if (con == null && connection == null)
75102
{
76103
Configuration.Log("No provider could be found because the Connection didn't implement System.Data.EntityClient.EntityConnection");
77104
Fallbacks.DefaultInsertAll(context, items);
@@ -108,6 +135,55 @@ public void InsertAll<TEntity>(IEnumerable<TEntity> items, DbConnection connecti
108135
}
109136
}
110137

138+
139+
public void UpdateAll<TEntity>(IEnumerable<TEntity> items, Action<UpdateSpecification<TEntity>> updateSpecification, DbConnection connection = null, int? batchSize = null) where TEntity : class, T
140+
{
141+
var con = context.Connection as EntityConnection;
142+
if (con == null && connection == null)
143+
{
144+
Configuration.Log("No provider could be found because the Connection didn't implement System.Data.EntityClient.EntityConnection");
145+
Fallbacks.DefaultInsertAll(context, items);
146+
}
147+
148+
var connectionToUse = connection ?? con.StoreConnection;
149+
var currentType = typeof(TEntity);
150+
var provider = Configuration.Providers.FirstOrDefault(p => p.CanHandle(connectionToUse));
151+
if (provider != null && provider.CanBulkUpdate)
152+
{
153+
154+
var mapping = EntityFramework.Utilities.EfMappingFactory.GetMappingsForContext(this.dbContext);
155+
var typeMapping = mapping.TypeMappings[typeof(T)];
156+
var tableMapping = typeMapping.TableMappings.First();
157+
158+
var properties = tableMapping.PropertyMappings
159+
.Where(p => currentType.IsSubclassOf(p.ForEntityType) || p.ForEntityType == currentType)
160+
.Select(p => new ColumnMapping {
161+
NameInDatabase = p.ColumnName,
162+
NameOnObject = p.PropertyName,
163+
DataType = p.DataType,
164+
IsPrimaryKey = p.IsPrimaryKey
165+
}).ToList();
166+
167+
if (tableMapping.TPHConfiguration != null)
168+
{
169+
properties.Add(new ColumnMapping
170+
{
171+
NameInDatabase = tableMapping.TPHConfiguration.ColumnName,
172+
StaticValue = tableMapping.TPHConfiguration.Mappings[typeof(TEntity)]
173+
});
174+
}
175+
176+
var spec = new UpdateSpecification<TEntity>();
177+
updateSpecification(spec);
178+
provider.UpdateItems(items, tableMapping.Schema, tableMapping.TableName, properties, connectionToUse, batchSize, spec);
179+
}
180+
else
181+
{
182+
Configuration.Log("Found provider: " + (provider == null ? "[]" : provider.GetType().Name) + " for " + connectionToUse.GetType().Name);
183+
Fallbacks.DefaultInsertAll(context, items);
184+
}
185+
}
186+
111187
public IEFBatchOperationFiltered<TContext, T> Where(Expression<Func<T, bool>> predicate)
112188
{
113189
this.predicate = predicate;
@@ -178,5 +254,8 @@ public int Update<TP>(Expression<Func<T, TP>> prop, Expression<Func<T, TP>> modi
178254
return Fallbacks.DefaultUpdate(context, this.predicate, prop, modifier);
179255
}
180256
}
257+
258+
259+
181260
}
182261
}

EntityFramework.Utilities/EntityFramework.Utilities/ExpressionHelper.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ internal static Expression<Func<T, bool>> CombineExpressions<T, TP>(Expression<F
3030
return final;
3131
}
3232

33+
public static string GetPropertyName<TSource, TProperty>(this Expression<Func<TSource, TProperty>> propertyLambda)
34+
{
35+
Type type = typeof(TSource);
36+
37+
var temp = propertyLambda.Body;
38+
while (temp is UnaryExpression)
39+
{
40+
temp = (temp as UnaryExpression).Operand;
41+
}
42+
MemberExpression member = temp as MemberExpression;
43+
return member.Member.Name;
44+
}
45+
3346
//http://stackoverflow.com/a/2824409/507279
3447
internal static Action<T, TP> PropertyExpressionToSetter<T, TP>(Expression<Func<T, TP>> prop)
3548
{

EntityFramework.Utilities/EntityFramework.Utilities/IQueryProvider.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@ public interface IQueryProvider
1111
bool CanDelete { get; }
1212
bool CanUpdate { get; }
1313
bool CanInsert { get; }
14+
bool CanBulkUpdate { get; }
1415

1516
string GetDeleteQuery(QueryInformation queryInformation);
1617
string GetUpdateQuery(QueryInformation predicateQueryInfo, QueryInformation modificationQueryInfo);
1718
void InsertItems<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize);
19+
void UpdateItems<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize, UpdateSpecification<T> updateSpecification);
1820

1921
bool CanHandle(DbConnection storeConnection);
2022

2123

2224
QueryInformation GetQueryInformation<T>(System.Data.Entity.Core.Objects.ObjectQuery<T> query);
25+
26+
2327
}
2428
}

EntityFramework.Utilities/EntityFramework.Utilities/MappingHelper.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,6 @@ public class TableMapping
6060
/// Null if not TPH
6161
/// </summary>
6262
public TPHConfiguration TPHConfiguration { get; set; }
63-
64-
6563
}
6664

6765
public class TPHConfiguration
@@ -90,6 +88,10 @@ public class PropertyMapping
9088
/// Used when we have TPH to exclude entities
9189
/// </summary>
9290
public Type ForEntityType { get; set; }
91+
92+
public string DataType { get; set; }
93+
94+
public bool IsPrimaryKey { get; set; }
9395
}
9496

9597
/// <summary>
@@ -145,7 +147,7 @@ public EfMapping(DbContext db)
145147

146148
var tableMapping = new TableMapping
147149
{
148-
PropertyMappings = new List<PropertyMapping>()
150+
PropertyMappings = new List<PropertyMapping>(),
149151
};
150152
var mappingToLookAt = mapping.EntityTypeMappings.FirstOrDefault(m => m.IsHierarchyMapping) ?? mapping.EntityTypeMappings.First();
151153
tableMapping.Schema = mappingToLookAt.Fragments[0].StoreEntitySet.Schema;
@@ -169,6 +171,7 @@ public EfMapping(DbContext db)
169171
tableMapping.PropertyMappings.Add(new PropertyMapping
170172
{
171173
ColumnName = scalar.Column.Name,
174+
DataType = scalar.Column.TypeName,
172175
PropertyName = path + item.Property.Name,
173176
ForEntityType = t
174177
});
@@ -208,6 +211,13 @@ public EfMapping(DbContext db)
208211
tableMapping.PropertyMappings = tableMapping.PropertyMappings.GroupBy(p => p.PropertyName)
209212
.Select(g => g.OrderByDescending(outer => g.Count(inner => inner.ForEntityType.IsSubclassOf(outer.ForEntityType))).First())
210213
.ToList();
214+
foreach (var item in tableMapping.PropertyMappings)
215+
{
216+
if ((mappingToLookAt.EntityType ?? mappingToLookAt.IsOfEntityTypes[0]).KeyProperties.Any(p => p.Name == item.PropertyName))
217+
{
218+
item.IsPrimaryKey = true;
219+
}
220+
}
211221
}
212222
}
213223

EntityFramework.Utilities/EntityFramework.Utilities/SqlQueryProvider.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class SqlQueryProvider : IQueryProvider
1313
public bool CanDelete { get { return true; } }
1414
public bool CanUpdate { get { return true; } }
1515
public bool CanInsert { get { return true; } }
16+
public bool CanBulkUpdate { get { return true; } }
1617

1718
public string GetDeleteQuery(QueryInformation queryInfo)
1819
{
@@ -64,7 +65,7 @@ public void InsertItems<T>(IEnumerable<T> items, string schema, string tableName
6465
}
6566
else
6667
{
67-
copy.DestinationTableName = tableName;
68+
copy.DestinationTableName = "[" + tableName + "]";
6869
}
6970

7071
copy.NotifyAfter = 0;
@@ -80,6 +81,49 @@ public void InsertItems<T>(IEnumerable<T> items, string schema, string tableName
8081
}
8182

8283

84+
public void UpdateItems<T>(IEnumerable<T> items, string schema, string tableName, IList<ColumnMapping> properties, DbConnection storeConnection, int? batchSize, UpdateSpecification<T> updateSpecification)
85+
{
86+
var tempTableName = "temp_" + tableName + "_" + DateTime.Now.Ticks;
87+
var columnsToUpdate = updateSpecification.Properties.Select(p => p.GetPropertyName()).ToDictionary(x => x);
88+
var filtered = properties.Where(p => columnsToUpdate.ContainsKey(p.NameOnObject) || p.IsPrimaryKey).ToList();
89+
var columns = filtered.Select(c => "[" + c.NameInDatabase + "] " + c.DataType);
90+
var pkConstraint = string.Join(", ", properties.Where(p => p.IsPrimaryKey).Select(c => "[" + c.NameInDatabase + "]"));
91+
92+
var str = string.Format("CREATE TABLE {0}.[{1}]({2}, PRIMARY KEY ({3}))", schema, tempTableName, string.Join(", ", columns), pkConstraint);
93+
94+
var con = storeConnection as SqlConnection;
95+
if (con.State != System.Data.ConnectionState.Open)
96+
{
97+
con.Open();
98+
}
99+
100+
var setters = string.Join(",", filtered.Where(c => !c.IsPrimaryKey).Select(c => "[" + c.NameInDatabase + "] = TEMP.[" + c.NameInDatabase + "]"));
101+
var pks = properties.Where(p => p.IsPrimaryKey).Select(x => "ORIG.[" + x.NameInDatabase + "] = TEMP.[" + x.NameInDatabase + "]");
102+
var filter = string.Join(",", pks);
103+
var mergeCommand = string.Format(@"UPDATE [{0}]
104+
SET
105+
{3}
106+
FROM
107+
[{0}] ORIG
108+
INNER JOIN
109+
[{1}] TEMP
110+
ON
111+
{2}", tableName, tempTableName, filter, setters);
112+
113+
using (var createCommand = new SqlCommand(str, con))
114+
using (var mCommand = new SqlCommand(mergeCommand, con))
115+
using (var dCommand = new SqlCommand(string.Format("DROP table {0}.[{1}]", schema, tempTableName), con))
116+
{
117+
createCommand.ExecuteNonQuery();
118+
InsertItems(items, schema, tempTableName, filtered, storeConnection, batchSize);
119+
mCommand.ExecuteNonQuery();
120+
dCommand.ExecuteNonQuery();
121+
}
122+
123+
124+
}
125+
126+
83127
public bool CanHandle(System.Data.Common.DbConnection storeConnection)
84128
{
85129
return storeConnection is SqlConnection;
@@ -106,5 +150,6 @@ public QueryInformation GetQueryInformation<T>(System.Data.Entity.Core.Objects.O
106150
}
107151
return queryInfo;
108152
}
153+
109154
}
110155
}

EntityFramework.Utilities/PerformanceTests/Program.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ static void Main(string[] args)
2525
BatchIteration(50000);
2626
//NormalIteration(50000);
2727
BatchIteration(100000);
28-
//NormalIteration(100000);
28+
NormalIteration(100000);
2929
}
3030

3131

@@ -61,9 +61,27 @@ private static void NormalIteration(int count)
6161
item.Reads++;
6262
}
6363
db.SaveChanges();
64+
stop.Stop();
6465
Console.WriteLine("Update all entities with a: " + stop.ElapsedMilliseconds + "ms");
6566
}
6667

68+
using (var db = new Context())
69+
{
70+
db.Configuration.AutoDetectChangesEnabled = true;
71+
db.Configuration.ValidateOnSaveEnabled = false;
72+
var toUpdate = db.Comments.ToList();
73+
var rand = new Random();
74+
foreach (var item in toUpdate)
75+
{
76+
item.Reads = rand.Next(0, 9999999);
77+
}
78+
stop.Restart();
79+
db.SaveChanges();
80+
81+
stop.Stop();
82+
Console.WriteLine("Update all with a random read: " + stop.ElapsedMilliseconds + "ms");
83+
}
84+
6785
using (var db = new Context())
6886
{
6987
db.Configuration.AutoDetectChangesEnabled = false;
@@ -115,6 +133,17 @@ private static void BatchIteration(int count)
115133
stop.Stop();
116134
Console.WriteLine("Update all entities with a: " + stop.ElapsedMilliseconds + "ms");
117135

136+
var commentsFromDb = db.Comments.AsNoTracking().ToList();
137+
var rand = new Random();
138+
foreach (var item in commentsFromDb)
139+
{
140+
item.Reads = rand.Next(0, 9999999);
141+
}
142+
stop.Restart();
143+
EFBatchOperation.For(db, db.Comments).UpdateAll(commentsFromDb, x => x.ColumnsToUpdate(c => c.Reads));
144+
stop.Stop();
145+
Console.WriteLine("Bulk update all with a random read: " + stop.ElapsedMilliseconds + "ms");
146+
118147
stop.Restart();
119148
EFBatchOperation.For(db, db.Comments).Where(x => x.Text == "a").Delete();
120149
stop.Stop();

EntityFramework.Utilities/Tests/DeleteByQueryTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public void DeleteAll_DateIsInRangeAndTitleEquals_DeletesAllMatchesAndNothingEls
156156
public void DeleteAll_NoProvider_UsesDefaultDelete()
157157
{
158158
string fallbackText = null;
159-
159+
Configuration.DisableDefaultFallback = false;
160160
Configuration.Log = str => fallbackText = str;
161161

162162
using (var db = Context.SqlCe())

EntityFramework.Utilities/Tests/InsertTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public void InsertAll_WrongColumnOrderAndRenamedColumn_InsertsItems()
184184
public void InsertAll_NoProvider_UsesDefaultInsert()
185185
{
186186
string fallbackText = null;
187-
187+
Configuration.DisableDefaultFallback = false;
188188
Configuration.Log = str => fallbackText = str;
189189

190190
using (var db = Context.SqlCe())

0 commit comments

Comments
 (0)