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
可切換資料庫的特色呢?答案是有,下列的類別可以辦到。
002 | using System.Collections.Generic; |
005 | using System.Data.Objects; |
006 | using System.ComponentModel; |
007 | using System.Data.Common; |
009 | using System.Data.EntityClient; |
010 | using System.Data.Objects.DataClasses; |
011 | using System.Reflection; |
012 | using System.Collections; |
015 | namespace EntityHelper |
017 | public class EntityBatchUpdater<T>:IDisposable where T :ObjectContext |
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; |
025 | static EntityBatchUpdater() |
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; |
037 | private T _context = null; |
039 | public T ObjectContext |
047 | public EntityBatchUpdater() |
049 | _context = (T)typeof(T).GetConstructor(new Type[]{}).Invoke(new object[]{}); |
052 | static object CreatePropagatorResultDictionary() |
054 | return Activator.CreateInstance(_propagatorResultType); |
057 | static object GetEntityAdapter(ObjectContext context) |
059 | object providerFactory = typeof(EntityConnection).GetProperty("ProviderFactory", |
060 | BindingFlags.NonPublic | BindingFlags.Instance).GetValue(context.Connection, null); |
061 | object result = ((IServiceProvider)providerFactory).GetService(_entityAdapterType); |
065 | static object CreateUpdateTranslator(object entityStateManager, System.Data.Metadata.Edm.MetadataWorkspace workspace, EntityConnection connection, int? commandTimeout) |
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 }); |
072 | static string GetQueryStatement(ObjectQuery query) |
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); |
080 | IEnumerable<string> cmds = (IEnumerable<string>)cmddef.GetType().GetProperty("MappedCommands", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(cmddef, null); |
081 | return cmds.FirstOrDefault(); |
084 | public static void Update(ObjectContext context) |
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(); |
095 | foreach (var item in o) |
097 | item.GetType().InvokeMember("Execute", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, item, |
098 | new object[] { updateTranslator, (EntityConnection)context.Connection, identifierValues, generateValues }); |
103 | context.Connection.Close(); |
107 | private static void MarkModifiedProperty(ObjectContext context, object entity, params string[] propertys) |
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) |
114 | string name = objectStateEntry.CurrentValues.GetName(metadata.Ordinal); |
115 | PropertyDescriptor descriptor = properties[name]; |
116 | if (propertys.Contains(descriptor.Name)) |
117 | objectStateEntry.SetModifiedProperty(descriptor.Name); |
121 | public static void UpdateDirect(ObjectContext context, string orKeyFields) |
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(); |
133 | foreach (var item in o) |
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); |
145 | context.Connection.Close(); |
149 | public void UpdateBatch(EntityObject entity, IQueryable query) |
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(); |
163 | foreach (var item in o) |
165 | DbCommand cmd = (DbCommand)item.GetType().InvokeMember("CreateCommand", BindingFlags.NonPublic | BindingFlags.Instance | |
166 | BindingFlags.InvokeMethod, null, item, |
167 | new object[] { updateTranslator, identifierValues }); |