在上一篇隨筆中,我們列舉了使用LINQ to SQL對數據庫進行更新的5中方案。本文將對這幾種方案進 行測試和對比,力求找出一個最佳實踐。
准備工作
我們的測試還是基於Products表。為了使測試更符合實際,我們將與之關聯的Categories、Suplliers 和Order_Details表都添加進來。首先創建一個IProductRepository接口,定義插入、查找、更新操作:
public interface IProductRepository
{
void InsertProduct(Product product);
Product GetProduct(int id);
void UpdateProduct(Product product);
}
然後創建一個AbstractProductRepository抽象類,實現IProductRepository接口。由於插入操作都是 一樣的,我們在抽象基類中提供了默認實現。同時還提供可選的查詢操作,因為除了“禁用查詢跟蹤”方 案外,其余的查詢操作也是一樣的。
public abstract class AbstractProductRepository : IProductRepository
{
public void InsertProduct(Product product)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.InsertOnSubmit(product);
db.SubmitChanges();
}
public virtual Product GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
public abstract void UpdateProduct(Product product);
}
接下來創建各個方案的實現類(具體的代碼請參考文章最後的下載鏈接):
方案一 重新賦值:CopyPropertiesProductRepository
方案二 禁用對象跟蹤:EnableObjectTrackingProductRepository
方案三 移除關聯:DetachAssociationProductRepository
方案四 使用委托:UsingDelegateProductRepository
開始測試
計時器采用CodeTimer,由於在下開發用的機器是XP(汗一個),所以使用的是修改版的CodeTimer。
我們對每個方案分別執行一次插入、查找和更新操作,代碼如下(方案四的代碼略有不同):
static void Execute(IProductRepository pr)
{
var p1 = new Product
{
CategoryID = 1,
ProductName = "Before changing",
SupplierID = 1,
UnitPrice = (decimal)2.0,
UnitsInStock = 100,
Discontinued = true,
ReorderLevel = 10
};
pr.InsertProduct(p1);
var p2 = pr.GetProduct(p1.ProductID);
p2.CategoryID = 2;
p2.ProductName = "Arfer changing";
p2.SupplierID = 2;
p2.UnitPrice = (decimal)3;
p2.UnitsInStock = 200;
p2.Discontinued = false;
p2.ReorderLevel = 20;
pr.UpdateProduct(p2);
}
然後分別計時:
static void Main(string[] args)
{
CodeTimer.Time("Copy Properties", 100, () => Execute(new CopyPropertiesProductRepository()));
CodeTimer.Time("Enable Object Tracking", 100, () => Execute(new EnableObjectTrackingProductRepository()));
CodeTimer.Time("Detach Associations", 100, () => Execute(new DetachAssociationProductRepository()));
CodeTimer.Time("Using Delegate", 100, () => ExecuteDelegate(new UsingDelegateProductRepository()));
Console.ReadLine();
}
執行100次的結果如下圖所示:
執行1000次的結果如下圖所示:
可見,使用反射復制屬性的方法時最不可取的。實際上,即使不使用反射而直接復制屬性,其性能都 是最差的。下圖是使用直接復制屬性時的數據:
直觀上來看,禁用對象跟蹤方法似乎性能最好,委托次之。但這種差距我認為是可以接受的(1000次 操作的執行時間之差在1秒之內,使用的對象數量也相差無幾)。那麼剩下的比較就在代碼的簡潔性和API 的易用性等方面了。
禁用對象跟蹤方法的代碼略多,因為為了能夠訪問與查詢對象關聯的其他對象,必須使用 DataLoadOptions類來進行加載。
public override Product GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
db.ObjectTrackingEnabled = false;
DataLoadOptions loads = new DataLoadOptions();
loads.LoadWith<Product>(p => p.Order_Details);
loads.LoadWith<Product>(p => p.Category);
loads.LoadWith<Product>(p => p.Supplier);
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
而方案四的API略顯復雜,畢竟不是所有業務層的程序員都能對表達式樹和委托運用自如。另一方面, 這種接口的約束也過於寬泛,不太好控制。因此可以將方法簽名改成如下的形式:
public void UpdateProduct(int id, Action<Product> action)
這樣一來性能超越了方案二,執行1000次的截圖如下:
方案四的優勢似乎已經很明顯了(當然單從執行時間上來說,方案二與其1秒以內的差距同樣是可以忽 略的),更少的代碼,更快的速度。
然而讓人遺憾的是,這仍然是一個避開Attach方法的方案。此外,由於必須將所有屬性的賦值放置在 一個委托中,也喪失了一定的靈活性。比如在實際的項目中,我們常常會希望獲取到Product的實體後, 針對每個屬性做一些操作,在方法的不同位置對不同屬性的值進行修改,然後再統一調用Update 方法進 行更新。這時方案四就顯得很別扭了。
因此我更傾向於提供多個UpdateProduct方法的重載版本,在不同的場景下使用不同的重載。您的意見 呢?