在前一篇文章中,我提出了在使用LINQ to SQL進行更新操作時可能會遇到的幾個問題。其實這不是我一個人遇到的問題,當我在網路上尋找答案時,我發現很多人都對這個主題發表過類似文章。但另我無法滿足的是,他們儘管提出了問題,卻沒有進行詳細的剖析,只給了解決方案(如添加RowVersion列、去除關聯等),但卻沒有說明為什麼必須這麼做。這也是我寫上篇的初衷,希望透過對LINQ to SQL原始碼的分析,來一步一步找出解決問題的方法。本文將對這些方法一一進行討論。
方案一:重新賦值在TerryLee、Anytao和Ding Xue等人的開源框架Ezsocio中,有些地方採取了重新賦值的方法。在Update方法內部,根據主鍵取得資料庫中的實體,然後與參數中的實體對其屬性一一賦值。
public void UpdateProfile(Profile p)
{
using (RepositoryContext db = new RepositoryContext())
{
var profile = db.GetTable<Profile>().First<Profile>(u => u.ID == p.ID);
profile.Birthday = p.Birthday;
profile.Gender = p.Gender;
profile.Hometown = p.Hometown;
profile.MSN = p.MSN;
profile.NickName = p.NickName;
profile.PhoneNumber = p.PhoneNumber;
profile.QQ = p.QQ;
profile.State = p.State;
profile.TrueName = p.TrueName;
profile.StateRefreshTime = p.StateRefreshTime;
profile.Avatar = p.Avatar;
profile.Website = p.Website;
db.SubmitChanges();
}
}
楊過兄也同樣給出了此方案的反射方法,實現屬性值的自動拷貝。
但我個人認為這是一種避實就虛的方案,沒有使用LINQ to SQL提供的用於更新操作的API,而採取了一種迂迴的策略。這其實是一種妥協,難道因為Attach方法“不好用”,我們就不用了嗎?呵呵。
方案二:停用物件追蹤對此,lea提出可以透過將DataContext的ObjectTrackingEnabled屬性設為false,來達到正確更新的目的。
public Product GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
db.ObjectTrackingEnabled = false;
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
其他的程式碼沒有任何變化。
為什麼禁用物件追蹤之後,就能正常更新了呢?我們還是從原始碼中來找答案吧。
public bool ObjectTrackingEnabled
{
get
{
this.CheckDispose();
return this.objectTrackingEnabled;
}
set
{
this.CheckDispose();
if (this.Services.HasCachedObjects)
{
throw System.Data.Linq.Error.OptionsCannotBeModifiedAfterQuery();
}
this.objectTrackingEnabled = value;
if (!this.objectTrackingEnabled)
{
this.deferredLoadingEnabled = false;
}
this.services.ResetServices();
}
}
原來設定ObjectTrackingEnabled為false時,會同時將DeferredLoadingEnabled設定為false。這樣,在執行查詢時,將不會為實體載入任何需延遲查詢的數據,因此Attach時也不會拋出異常(請參閱上篇的分析)。
在MSDN中我們還得到下面這條有用的資訊:將ObjectTrackingEnable屬性設定為false,可以提高檢索時的效能,因為這樣可以減少要追蹤的項目。這真是一個很誘人的特性。
但禁用物件追蹤時,要特別注意兩點:(1)必須在執行查詢前停用。 (2)禁用之後不能再呼叫Attach和SubmitChanges方法。否則都將引發異常。
方案三:移除關聯在前一篇文章中已經介紹一個蹩腳的方法,即在GetProduct方法中手動設定與Product關聯的Category為null。我們可以把這部分程式碼提取出來,放入一個Detach方法中。因為這個Detach是實體的方法,可以使用分部類別:
public partial class Product
{
public void Detach()
{
this._Category = default(EntityRef<Category>);
}
}
public partial class Category
{
public void Detach()
{
foreach (var product in this.Products)
{
product.Detach();
}
}
}但是這種對每個實體都定義Detach的方法過於繁瑣。隨著實體的增多,關係越來越複雜,容易出現漏掉的屬性。張逸提出了一個非常優雅的方法,利用反射對此邏輯進行抽象化:
private void Detach(TEntity entity)
{
foreach (FieldInfo fi in entity.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
if (fi.FieldType.ToString().Contains("EntityRef"))
{
var value = fi.GetValue(entity);
if (value != null)
{
fi.SetValue(entity, null);
}
}
if (fi.FieldType.ToString().Contains("EntitySet"))
{
var value = fi.GetValue(entity);
if (value != null)
{
MethodInfo mi = value.GetType().GetMethod("Clear");
if (mi != null)
{
mi.Invoke(value, null);
}
fi.SetValue(entity, value);
}
}
}
}
也有人認為在Detach時應該把PropertyChanging和PropertyChanged事件設定為null,但整體的想法是一樣的。
方案四:使用委託這是ZC29同學在我上一篇文章的評論裡給出的方法,我個人認為非常值得借鏡。
public void UpdateProductWithDelegate(Expression<Func<Product, bool>> predicate, Action<Product> action)
{
NorthwindDataContext db = new NorthwindDataContext();
var product = db.Products.SingleOrDefault(predicate);
action(product);
db.SubmitChanges();
}
// Client code
ProductRepository repository = new ProductRepository();
repository.UpdateProductWithDelegate(p => p.ProductID == 1, p =>
{
p.ProductName = "Changed";
});
使用Lambda表達式將GetProduct的邏輯植入UpdateProduct中,並且使用委託將更新邏輯也延緩執行,這樣巧妙地將查找和更新放進了一個DataContext裡,從而繞開了Attach。但是這種方法API有些過於複雜,對客戶端程式設計人員的水平要求過高。而且在Update裡還要執行一遍Get的邏輯,儘管性能上的損失微乎其微,但看上去總多多少少給人一種不夠DRY的感覺。
方案五:使用UPDATE語句在Ezsocio的原始碼中,我發現了RepositoryBase.UpdateEntity方法。在方法內部進行SQL語句的拼接,並且只會更新發生變更的欄位。由於此處已經不再使用ITable,並且需要完整的框架支持,因此不再進行過多的評論。詳情請參考Ezsocio的原始碼。
總結本文列舉了近幾天我在網路上找到的幾種解決方案,它們各有利弊,孰優孰劣,見仁見智。在下篇中,我將對這幾種方法進行效能上的比較,從而找出最優方案。