http://www.dotblogs.com.tw/code6421/archive/2010/09/02/17526.aspx
Batch Updating in Entity Framework
文
/
黃忠成
The Update Story of Entity Framework
多數的
O/R Mapping Framework
都有個共同的行為模式,在刪除資料或是修改資料前,必須隱式的下達一個
Query
,由資料庫取得即將要更新的資料列,
然後轉成物件後再更新。
這個行為模式,多半也會成為設計師考慮是否使用
O/R Mapping Framework
的考量之一,因為多一個
Query
,就代表著效能會因此降低,雖然對於
O/R Mapping Framework
而言,這是一個必要的行為模式,因為它們得考量到當物件有著關聯時的情況。但對於實際的專案來說,跳過這個
Query
來更新資料,
卻也是必然會出現的情況,既然是必然會出現的情況,多數的
O/R Mapping Framework
也只好為此做出讓步,提供可跳過
Query
來更新資料的機制,
Entity Framework
自然也擁有這個機制。
Update Row without Query
Entity Framework
支援跳過
Query
步驟來更新資料列,寫法如下
:
static
void UpdateWithoutQuery()
{
NorthwindEntities context = new NorthwindEntities();
Customers c = new Customers();
c.CustomerID = "VINET";
context.AttachTo("Customers", c);
c.CompanyName = "15556";
context.SaveChanges();
}
注意,
AttachTo
的位置很重要,在這之前所設定的值,都不會被寫入,例如下列的
Region
便不會被寫入。
static
void UpdateWithoutQuery()
{
NorthwindEntities context = new NorthwindEntities();
Customers c = new Customers();
c.CustomerID = "VINET";
c.Region = "TWN";
context.AttachTo("Customers", c);
c.CompanyName = "15556";
context.SaveChanges();
}
Delete Row without Query
同樣的手法,也可以用在刪除資料列上。
static
void DeleteWithoutQuery()
{
NorthwindEntities context = new NorthwindEntities();
Customers c = new Customers();
c.CustomerID = "CT002";
context.AttachTo("Customers", c);
context.DeleteObject(c);
context.SaveChanges();
}
缺點?
那麼這樣就夠了嗎?事實上,
O/R Mapping Framework
一直都缺少著一種機制,那就是
Batch Update
,在很多情況下,我們希望能
下達下列的指令來更新一筆以上的資料列。
UPDATE Customers SET SomeFlag = 1 WHERE Region = “TW”
在
O/R Mapping Framework
中,這得以迴圈方式,一一查詢出每一筆
Region=”TW”
的資料,然後更新
SomeFlag
,由於沒有指定主鍵,
所以也無法使用先前提及的方法來跳過
Query
動作,我們得遵守
O/R Mapping Framework
的規則,一筆筆
Query
後更新,這是很沒效率的動作。
當然,所有
O/R Mapping Framework
都支援讓設計師直接下達
SQL
的方法,以
Entity Framework
而言,可以這麼下
:
context.ExecuteStoreCommand(“
UPDATE Customers SET SomeFlag = 1 WHERE Region = ‘TW’);
不過,這種方法會失去
Entity Framework
可切換資料庫的特色,所以得特別小心把這部份獨立出來,為日後切換資料庫時留條後路。
Batch Update
那麼,有沒有一個方法,可以達到
Batch Update
,又不失去
Entity Framework
可切換資料庫的特色呢?答案是有,下列的類別可以辦到。
view source
print
?
001using System;
002using System.Collections.Generic;
003using System.Linq;
004using System.Text;
005using System.Data.Objects;
006using System.ComponentModel;
007using System.Data.Common;
008using System.Data;
009using System.Data.EntityClient;
010using System.Data.Objects.DataClasses;
011using System.Reflection;
012using System.Collections;
013
014
015namespace EntityHelper
016{
017 public class EntityBatchUpdater<T>:IDisposable where T :ObjectContext
018 {
019 private static Assembly _systemDataEntity = null;
020 private static Type _propagatorResultType = null;
021 private static Type _entityAdapterType = null;
022 private static Type _updateTranslatorType = null;
023 private static Type _entityStateType = null;
024
025 static EntityBatchUpdater()
026 {
027 _systemDataEntity = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "System.Data.Entity").FirstOrDefault();
028 Type t = _systemDataEntity.GetType("System.Data.Mapping.Update.Internal.PropagatorResult");
029 Type t1 = typeof(KeyValuePair<,>).MakeGenericType(t, typeof(object));
030 Type t2 = typeof(List<>).MakeGenericType(t1);
031 _entityAdapterType = _systemDataEntity.GetType("System.Data.IEntityAdapter");
032 _updateTranslatorType = _systemDataEntity.GetType("System.Data.Mapping.Update.Internal.UpdateTranslator");
033 _entityStateType = _systemDataEntity.GetType("System.Data.IEntityStateManager");
034 _propagatorResultType = t2;
035 }
036
037 private T _context = null;
038
039 public T ObjectContext
040 {
041 get
042 {
043 return _context;
044 }
045 }
046
047 public EntityBatchUpdater()
048 {
049 _context = (T)typeof(T).GetConstructor(new Type[]{}).Invoke(new object[]{});
050 }
051
052 static object CreatePropagatorResultDictionary()
053 {
054 return Activator.CreateInstance(_propagatorResultType);
055 }
056
057 static object GetEntityAdapter(ObjectContext context)
058 {
059 object providerFactory = typeof(EntityConnection).GetProperty("ProviderFactory",
060 BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.Connection, null);
061 object result = ((IServiceProvider)providerFactory).GetService(_entityAdapterType);
062 return result;
063 }
064
065 static object CreateUpdateTranslator(object entityStateManager, System.Data.Metadata.Edm.MetadataWorkspace workspace, EntityConnection connection, int? commandTimeout)
066 {
067 ConstructorInfo ci = _updateTranslatorType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
068 new Type[] { _entityStateType, typeof(System.Data.Metadata.Edm.MetadataWorkspace), typeof(EntityConnection), typeof(int?) }, null);
069 return ci.Invoke(new object[] { entityStateManager, workspace, connection, commandTimeout });
070 }
071
072 static string GetQueryStatement(ObjectQuery query)
073 {
074 object queryState = typeof(ObjectQuery).GetProperty("QueryState", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(query, null);
075 object queryPlan = queryState.GetType().BaseType.InvokeMember("GetExecutionPlan", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod,
076 null, queryState, new object[] { null });
077 DbCommandDefinition cmddef = (DbCommandDefinition)queryPlan.GetType().GetField("CommandDefinition", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(queryPlan);
078
079
080 IEnumerable<string> cmds = (IEnumerable<string>)cmddef.GetType().GetProperty("MappedCommands", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(cmddef, null);
081 return cmds.FirstOrDefault();
082 }
083
084 public static void Update(ObjectContext context)
085 {
086 object entityAdapter = GetEntityAdapter(context);
087 object updateTranslator = CreateUpdateTranslator(context.ObjectStateManager, ((EntityConnection)context.Connection).GetMetadataWorkspace(), (EntityConnection)context.Connection, context.CommandTimeout);
088 IEnumerable o = (IEnumerable)updateTranslator.GetType().InvokeMember("ProduceCommands",
089 BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, updateTranslator, null);
090 Dictionary<int, object> identifierValues = new Dictionary<int, object>();
091 object generateValues = CreatePropagatorResultDictionary();
092 context.Connection.Open();
093 try
094 {
095 foreach (var item in o)
096 {
097 item.GetType().InvokeMember("Execute", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, item,
098 new object[] { updateTranslator, (EntityConnection)context.Connection, identifierValues, generateValues });
099 }
100 }
101 finally
102 {
103 context.Connection.Close();
104 }
105 }
106
107 private static void MarkModifiedProperty(ObjectContext context, object entity, params string[] propertys)
108 {
109 context.ObjectStateManager.ChangeObjectState(entity, EntityState.Unchanged);
110 ObjectStateEntry objectStateEntry = context.ObjectStateManager.GetObjectStateEntry(entity);
111 PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entity.GetType());
112 foreach (FieldMetadata metadata in objectStateEntry.CurrentValues.DataRecordInfo.FieldMetadata)
113 {
114 string name = objectStateEntry.CurrentValues.GetName(metadata.Ordinal);
115 PropertyDescriptor descriptor = properties[name];
116 if (propertys.Contains(descriptor.Name))
117 objectStateEntry.SetModifiedProperty(descriptor.Name);
118 }
119 }
120
121 public static void UpdateDirect(ObjectContext context, string orKeyFields)
122 {
123 object entityAdapter = GetEntityAdapter(context);
124 object updateTranslator = CreateUpdateTranslator(context.ObjectStateManager, ((EntityConnection)context.Connection).GetMetadataWorkspace(),
125 (EntityConnection)context.Connection, context.CommandTimeout);
126 IEnumerable o = (IEnumerable)updateTranslator.GetType().InvokeMember("ProduceCommands",
127 BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, updateTranslator, null);
128 Dictionary<int, object> identifierValues = new Dictionary<int, object>();
129 object generateValues = CreatePropagatorResultDictionary();
130 context.Connection.Open();
131 try
132 {
133 foreach (var item in o)
134 {
135 DbCommand cmd = (DbCommand)item.GetType().InvokeMember("CreateCommand", BindingFlags.NonPublic | BindingFlags.Instance |
136 BindingFlags.InvokeMethod, null, item,
137 new object[] { updateTranslator, identifierValues });
138 cmd.Connection = ((EntityConnection)context.Connection).StoreConnection;
139 cmd.CommandText = cmd.CommandText + " OR " + orKeyFields;
140 cmd.ExecuteReader(CommandBehavior.CloseConnection);
141 }
142 }
143 finally
144 {
145 context.Connection.Close();
146 }
147 }
148
149 public void UpdateBatch(EntityObject entity, IQueryable query)
150 {
151 if (!(query is ObjectQuery))
152 throw new Exception("only support ObjectQuery.");
153 object entityAdapter = GetEntityAdapter(_context);
154 object updateTranslator = CreateUpdateTranslator(_context.ObjectStateManager, ((EntityConnection)_context.Connection).GetMetadataWorkspace(),
155 (EntityConnection)_context.Connection, _context.CommandTimeout);
156 IEnumerable o = (IEnumerable)updateTranslator.GetType().InvokeMember("ProduceCommands",
157 BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, updateTranslator, null);
158 Dictionary<int, object> identifierValues = new Dictionary<int, object>();
159 object generateValues = CreatePropagatorResultDictionary();
160 _context.Connection.Open();
161 try
162 {
163 foreach (var item in o)
164 {
165 DbCommand cmd = (DbCommand)item.GetType().InvokeMember("CreateCommand", BindingFlags.NonPublic | BindingFlags.Instance |
166 BindingFlags.InvokeMethod, null, item,
167 new object[] { updateTranslator, identifierValues });
168 cmd.Connection = ((EntityConnection)_context.Connection).StoreConnection;
169 string queryStatement = GetQueryStatement(query as ObjectQuery);
170 if (queryStatement.ToLower().Contains("where"))
171 queryStatement = queryStatement.Substring(queryStatement.ToLower().IndexOf("where ") + 5);
172 cmd.CommandText = cmd.CommandText.Substring(0, cmd.CommandText.ToLower().IndexOf("where ") - 1) + " Where " +
173 queryStatement.Replace("[Extent1].", "").Replace("/"Extent1/".", "").Replace("Extent1.", "");
174 RemovePrimaryKeyParameter(cmd, entity);
175 cmd.ExecuteReader(CommandBehavior.CloseConnection);
176 }
177 }
178 finally
179 {
180 _context.Connection.Close();
181 }
182 }
183
184 private static void RemovePrimaryKeyParameter(DbCommand cmd, EntityObject entity)
185 {
186 foreach (var prop in entity.GetType().GetProperties())
187 {
188 EdmScalarPropertyAttribute[] attrs = (EdmScalarPropertyAttribute[])prop.GetCustomAttributes(typeof(EdmScalarPropertyAttribute), true);
189 if (attrs != null && attrs.Length > 0)
190 {
191 if (attrs[0].EntityKeyProperty)
192 cmd.Parameters.RemoveAt(cmd.Parameters.Count - 1);
193 }
194 }
195 }
196
197 public void TrackEntity(EntityObject entity)
198 {
199 if (entity.EntityKey == null)
200 {
201 EntityKey keys = new EntityKey();
202 List<EntityKeyMember> members = new List<EntityKeyMember>();
203 foreach (var prop in entity.GetType().GetProperties())
204 {
205 EdmScalarPropertyAttribute[] attrs = (EdmScalarPropertyAttribute[])prop.GetCustomAttributes(typeof(EdmScalarPropertyAttribute), true);
206 if (attrs != null && attrs.Length > 0)
207 {
208 if (attrs[0].EntityKeyProperty)
209 {
210 object defaultValue = null;
211
212 if (prop.PropertyType == typeof(string))
213 defaultValue = "";
214 else if (prop.PropertyType == typeof(int) ||
215 prop.PropertyType == typeof(double) ||
216 prop.PropertyType == typeof(float) ||
217 prop.PropertyType == typeof(Int32) ||
218 prop.PropertyType == typeof(Int16) ||
219 prop.PropertyType == typeof(Int64) ||
220 prop.PropertyType == typeof(long) ||
221 prop.PropertyType == typeof(short))
222 defaultValue = -1;
223 else if (prop.PropertyType == typeof(DateTime))
224 defaultValue = DateTime.MinValue;
225 else if (prop.PropertyType == typeof(TimeSpan))
226 defaultValue = TimeSpan.MinValue;
227 else if (prop.PropertyType == typeof(Char))
228 defaultValue = 'C';
229 prop.SetValue(entity, defaultValue, null);
230 members.Add(new EntityKeyMember(prop.Name, defaultValue));
231 }
232 }
233 }
234 keys.EntityKeyValues = members.ToArray();
235 EdmEntityTypeAttribute[] attrs1 = (EdmEntityTypeAttribute[])entity.GetType().GetCustomAttributes(typeof(EdmEntityTypeAttribute), true);
236 if (attrs1 != null && attrs1.Length > 0)
237 {
238 keys.EntityContainerName = _context.DefaultContainerName;
239 keys.EntitySetName = attrs1[0].Name;
240 }
241 entity.EntityKey = keys;
242 }
243
244 _context.Attach(entity);
245
246 entity.PropertyChanged += (s, args) =>
247 {
248 MarkModifiedProperty(_context, entity, args.PropertyName);
249 };
250 }
251
252 public void Dispose()
253 {
254 _context.Dispose();
255 }
256 }
257}
這個類別的程式碼,說穿了就是透過
Entity Framework
原本提供,但不公開的函式及物件來達到目的,運用此類別,我們可以寫下以下這段程式碼,然後進行批次更新
:
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
System.Data.Objects;
using
System.ComponentModel;
using
System.Data.Common;
using
System.Data;
using
System.Data.EntityClient;
using
System.Reflection;
using
System.Collections;
using
EntityHelper;
namespace
ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Customers c = new Customers();
EntityBatchUpdater<NorthwindEntities> batchContext =
new EntityBatchUpdater<NorthwindEntities>();
//
設定
c
為要
Tracking
的對象物件
batchContext.TrackEntity(c);
//
要更新的欄位
c.CompanyName = "CR4";
//
更新
c
物件
,
第二個參數為查詢條件
.
batchContext.UpdateBatch(c,
batchContext.ObjectContext.Customers.Where(a => a.Region == "ru"));
}
}
}
當對要更新的物件呼叫
TrackEntity
函式時,
EntityBatchUpdater
會自動開始追蹤該物件,此後更新的欄位都將被視為是要寫入資料庫的值,呼叫
UpdateBatch
則是將
c
的變動
寫入資料庫中,注意,第二個參數是更新
c
時的查詢條件,此例會將所有
Region = “ru”
的資料列的
CompanyName
更新為
CR4
。
同樣的結果,也可以這樣寫
:
batchContext.UpdateBatch(c, from s1 in batchContext.ObjectContext.Customers where s1.Region == "ru" select s1);
Batch Delete
EntityBatchUpdater
也可以用在刪除,如下
:
static
void Main(string[] args)
{
Customers c = new Customers();
EntityBatchUpdater<NorthwindEntities> batchContext =
new EntityBatchUpdater<NorthwindEntities>();
batchContext.TrackEntity(c);
batchContext.ObjectContext.DeleteObject(c);
batchContext.UpdateBatch(c,
from
s1 in batchContext.ObjectContext.Customers where s1.Region == "ru" select s1);
}
此例會將所有
Region = “ru”
的資料列刪除。
你該知道的事
EntityBatchUpdater
可以幫我們完成
Batch Update
及
Batch Delete
,現在問題出在跨資料庫上,
EntityBatchUpdater
所使用的手法可以適用於
SQL Server
及
Oracle(Devart)
,
而其它的資料庫就沒測試過了,如果你遭遇到問題,那麼可查看
UpdateBatch
最後的
SQL
字串組合部份,通常問題會出現在
Alias
。
转载请注明原文地址: https://ibbs.8miu.com/read-2272293.html