Batch Updating in Entity Framework in EF6

  多數的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()
{
     Model1 context = new Model1();
     EMPLOYEES c = new EMPLOYEES();
     c.ID = 1;
     context.EMPLOYEES.Attach(c);
     c.NAME = "code6421";
     context.SaveChanges();
}

注意,AttachTo的位置很重要,在這之前所設定的值,都不會被寫入,例如下列的Region便不會被寫入。

static void UpdateWithoutQuery2()
{
     Model1 context = new Model1();
     context.Database.Log = s => Console.WriteLine(s);
     EMPLOYEES c = new EMPLOYEES();
     c.ID = 1;
     c.AREA = "TOKYO";
     context.EMPLOYEES.Attach(c);
     c.NAME = "tommy15";
     context.SaveChanges();
}
Delete Row without Query

  同樣的手法,也可以用在刪除資料列上。

static void DeleteWithoutQuery()
{
     Model1 context = new Model1();
     EMPLOYEES c = new EMPLOYEES();
     c.ID = 3;
     context.EMPLOYEES.Attach(c);
     context.EMPLOYEES.Remove(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.Database.ExecuteSqlCommand("UPDATE Customers SET SomeFlag = 1 WHERE Region = 'TW'");

不過,這種方法會失去Entity Framework可切換資料庫的特色,所以得特別小心把這部份獨立出來,為日後切換資料庫時留條後路。

Batch Update

   那麼,有沒有一個方法,可以達到Batch Update,又不失去Entity Framework可切換資料庫的特色呢?答案是有,下列的類別可以辦到。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Data.Common;
using System.Data;
using System.Reflection;
using System.Collections;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Core.EntityClient;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Common;
using System.Data.Entity;
using System.Data.Entity.Core.Objects.DataClasses;
using System.Data.Entity.Core;
using System.Data.Entity.Infrastructure;

namespace EntityHelper
{
    public class EntityBatchUpdater<T> : IDisposable where T : DbContext
    {
        private static Assembly _systemDataEntity = null;
        private static Type _updateTranslatorType = null;
        private static Type _entityAdapterType = null;
        static EntityBatchUpdater()
        {
            _systemDataEntity = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "EntityFramework").FirstOrDefault();
            _entityAdapterType = _systemDataEntity.GetType("System.Data.Entity.Core.EntityClient.Internal.EntityAdapter");
            _updateTranslatorType = _systemDataEntity.GetType("System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator");
        }

        private T _context = null;

        public T ObjectContext
        {
            get
            {
                return _context;
            }
        }

        public EntityBatchUpdater()
        {
            _context = (T)typeof(T).GetConstructor(new Type[] { }).Invoke(new object[] { });
        }

        static object GetUpdateTranslator(ObjectContext context)
        {
            var field = typeof(ObjectContext).GetField("_adapter", BindingFlags.NonPublic | BindingFlags.Instance);
            var adapter = field.GetValue(context);
            var prop = _entityAdapterType.GetProperty("Connection", BindingFlags.Public | BindingFlags.Instance);
            prop.SetValue(adapter, context.Connection);
            ConstructorInfo ci = _updateTranslatorType.GetConstructor(new Type[] { _entityAdapterType });
            return ci.Invoke(new object[] { adapter });
        }

        public void UpdateBatch<TEntity>(IQueryable query)
        {          
            var dbQuery = query as DbQuery<TEntity>;           
            var objectContext = ((IObjectContextAdapter)_context).ObjectContext;
            object updateTranslator = GetUpdateTranslator(objectContext);
            IEnumerable o = (IEnumerable)updateTranslator.GetType().InvokeMember("ProduceCommands",
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, null, updateTranslator, null);
            Dictionary<int, object> identifierValues = new Dictionary<int, object>();
            objectContext.Connection.Open();
            try
            {
                foreach (var item in o)
                {
                    DbCommand cmd = (DbCommand)item.GetType().InvokeMember("CreateCommand", BindingFlags.NonPublic | BindingFlags.Instance |
                        BindingFlags.InvokeMethod, null, item,
                        new object[] { identifierValues });
                    cmd.Connection = ((EntityConnection)objectContext.Connection).StoreConnection;
                    string queryStatement = dbQuery.Sql;
                    if (queryStatement.ToLower().Contains("where"))
                        queryStatement = queryStatement.Substring(queryStatement.ToLower().IndexOf("where ") + 5);
                    cmd.CommandText = cmd.CommandText.Substring(0, cmd.CommandText.ToLower().IndexOf("where ") - 1) + " Where " +
                              queryStatement.Replace("[Extent1].", "").Replace("\"Extent1\".", "").Replace("Extent1.", "");
                    cmd.ExecuteReader(CommandBehavior.CloseConnection);
                }
            }
            finally
            {
                objectContext.Connection.Close();
            }
        }

        public void TrackEntity(object entityObject)
        {
            var objectContext = ((IObjectContextAdapter)_context).ObjectContext;
            _context.Set(entityObject.GetType()).Attach(entityObject);
            var t = _context.Entry(entityObject);
            t.State = EntityState.Modified;
        }

        public void Dispose()
        {
            _context.Dispose();
        }
    }
}

 這個類別的程式碼,說穿了就是透過Entity Framework原本提供,但不公開的函式及物件來達到目的,運用此類別,我們可以寫下以下這段程式碼,然後進行批次更新:

static void BatchUpdate()
{
     EMPLOYEES c = new EMPLOYEES();
     EntityBatchUpdater<Model1> batchContext =
                   new EntityBatchUpdater<Model1>();
     //設定c為要Tracking的對象物件
     batchContext.TrackEntity(c);

     //要更新的欄位
     c.NAME = "code6421";
     c.AREA = "TWN";
     //更新c物件,第二個參數為查詢條件.
       batchContext.UpdateBatch<EMPLOYEES>(batchContext.ObjectContext.EMPLOYEES.Where(a => a.AREA == "TWN"));
}


static void Main(string[] args)
{
      BatchUpdate();
      Console.ReadLine();
}

當對要更新的物件呼叫TrackEntity函式時,EntityBatchUpdater會自動開始追蹤該物件,此後更新的欄位都將被視為是要寫入資料庫的值,呼叫UpdateBatch則是將c的變動寫入資料庫中,注意,第二個參數是更新c時的查詢條件,此例會將所有Region = “ru”的資料列的NAME更新為code6421。

同樣的結果,也可以這樣寫:

batchContext.UpdateBatch(c, from s1 in batchContext.ObjectContext.Customers where s1.Region == "TWN" select s1);
Batch Delete

  EntityBatchUpdater也可以用在刪除,如下:

static void BatchDelete()
{
     EMPLOYEES c = new EMPLOYEES();
     EntityBatchUpdater<Model1> batchContext =
                    new EntityBatchUpdater<Model1>();
     //設定c為要Tracking的對象物件
     batchContext.TrackEntity(c);
     batchContext.ObjectContext.EMPLOYEES.Remove(c);
     batchContext.UpdateBatch<EMPLOYEES>(batchContext.ObjectContext.EMPLOYEES.Where(a => a.AREA == "TWN"));

}

此例會將所有Region = “TWN”的資料列刪除。

 

你該知道的事

  EntityBatchUpdater可以完成Batch Update及Batch Delete,現在問題出在跨資料庫上,EntityBatchUpdater所使用的手法可以適用於SQL Server及Oracle,而其它的資料庫就沒測試過了,如果你遭遇到問題,那麼可查看UpdateBatch最後的SQL字串組合部份,通常問題會出現在Alias。