Batch Updating in Entity Framework

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 可切換資料庫的特色呢?答案是有,下列的類別可以辦到。
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
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值