程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 使用LINQ to SQL更新數據庫(上):問題重重

使用LINQ to SQL更新數據庫(上):問題重重

編輯:關於.NET

在學習LINQ時,我幾乎被一個困難所擊倒,這就是你從標題中看到的更新數據庫的操作。下面我就一 步步帶你走入這泥潭,請准備好磚頭和口水,Follow me。

從最簡單的情況入手

我們以Northwind數據庫為例,當需要修改一個產品的ProductName時,可以在客戶端直接寫下這樣的 代碼:

// List 0
NorthwindDataContext db = new NorthwindDataContext();
Product product = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai Changed";
db.SubmitChanges();

測試一下,更新成功。不過我相信,在各位的項目中不會出現這樣的代碼,因為它簡直沒法復用。好 吧,讓我們對其進行重構,提取至一個方法中。參數應該是什麼呢?是新的產品名稱,以及待更新的產品 ID。嗯,好像是這樣的。

public void UpdateProduct(int id, string productName)
{
   NorthwindDataContext db = new NorthwindDataContext();
   Product product = db.Products.Single(p => p.ProductID == id);
   product.ProductName = productName;
   db.SubmitChanges();
}

在實際的項目中,我們不可能僅僅只修改產品名稱。Product的其他字段同樣也是修改的對象。那麼 UpdateProduct方法的簽名將變成如下的形式:

public void UpdateProduct(int id,
   string productName,
   int suplierId,
   int categoryId,
   string quantityPerUnit,
   decimal unitPrice,
   short unitsInStock,
   short unitsOnOrder,
   short reorderLevel)

當然這只是簡單的數據庫,在實際項目中,二十、三十甚至上百個字段的情況也不少見。誰能忍受這 樣的方法呢?這樣寫,還要Product對象干什麼呢?

對啊,把Product作為方法的參數,把惱人的賦值操作拋給客戶代碼吧。同時,我們將獲取Product實 例的代碼提取出來,形成 GetProduct方法,並且將與數據庫操作相關的方法放到一個專門負責和數據庫 打交道的ProductRepository類中。哦耶,SRP!

// List 1
// ProductRepository 
public Product GetProduct(int id)
{
   NorthwindDataContext db = new NorthwindDataContext();
   return db.Products.SingleOrDefault(p => p.id == id);
}

public void UpdateProduct(Product product)
{
   NorthwindDataContext db = new NorthwindDataContext();
   db.Products.Attach(product);
   db.SubmitChanges();
}

// Client code
ProductRepository repository = new ProductRepository();
Product product = repository.GetProduct(1);
product.ProductName = "Chai Changed";
repository.UpdateProduct(product);

在這裡我使用了Attach方法,將Product的一個實例附加到其他的DataContext上。對於默認的 Northwind數據庫來說,這樣做的結果就是得到下面的異常:

// Exception 1
NotSupportException:
已嘗試Attach或Add實體,該實體不是新實體,可能是從其他DataContext中加載來的。不支持這種操作 。
An attempt has been made to Attach or Add an entity that is not new,
perhaps having been loaded from another DataContext. This is not  supported

查看MSDN我們知道,

在將實體序列化到客戶端時,這些實體會與其原始DataContext分離。DataContext不再跟蹤這些實體 的更改或它們與其他對象的關聯。

這時如果要更新或者刪除數據,則必須在調用SubmitChanges之前使用Attach方法將實體附加到新的 DataContext中,否則就會拋出上面的異常。

而在Northwind數據庫中,Product類包含三個與之相關的類(即外鍵關聯):Order_Detail、 Category和 Supllier。在上面的例子中,我們雖然把Product進行了Attach,但卻沒有Attach與其相關聯 的類,因此拋出 NotSupportException。

那麼如何關聯與Product相關的類呢?這看上去似乎十分復雜,即便簡單地如Northwind這樣的數據庫 亦是如此。我們似乎必須先獲取與原始 Product相關的Order_Detail、Category和Supllier的原始類,然 後再分別Attach到當前的DataContext 中,但實際上即使這樣做也同樣會拋出NotSupportException。

那麼究竟該如何實現更新操作呢?為了簡便起見,我們刪除Northwind.dbml中的其他實體類,只保留 Product。這樣就可以從最簡單的情況開始入手分析了。

問題重重

刪除其他類之後,我們再次執行List 1中的代碼,然而數據庫並沒有更改產品的名稱。通過查看 Attach方法的重載版本,我們很容易發現問題所在。

Attach(entity)方法默認調用Attach(entity, false)重載,它將以未修改的狀態附加相應實體。如果 Product對象沒有被修改,那麼我們應該調用該重載版本,將Product對象以未修改的狀態附加到 DataContext,以便後續操作。而此時的Product對象的狀態是“已修改”,我們只能調用Attach(entity, true)方法。

於是我們將List 1的相關代碼改為Attach(product, true),看看發生了什麼?

// Exception 2
InvalidOperationException:
如果實體聲明了版本成員或者沒有更新檢查策略,則只能將它附加為沒有原始狀態的已修改實體。
An entity can only be attached as modified without original state
if it declares a version member or does not have an update check  policy.

LINQ to SQL使用RowVersion列來實現默認的樂觀式並發檢查,否則在以修改狀態向DataContext附加 實體的時候,就會出現上面的錯誤。實現RowVersion列的方法有兩種,一種是為數據庫表定義一個 timestamp類型的列,另一種方法是在表主鍵所對應的實體屬性上,定義IsVersion=true特性。注意,不 能同時擁有TimeStamp列和IsVersion=true特性,否則將拋出InvalidOprationException:成員 “System.Data.Linq.Binary TimeStamp”和“Int32 ProductID”都標記為行版本。在本文中,我們使用 timestamp列來舉例。

為Products表建立名為TimeStamp、類型為timestamp的列之後,將其重新拖拽到設計器中,然後執行 List 1中的代碼。謝天謝地,終於成功了。

現在,我們再向設計器中拖入Categories表。這次學乖了,先在Categories表中添加timestamp列。測 試一下,居然又是 Exception 1中的錯誤!刪除Categories的timestamp列,問題依舊。天哪,可怕的 Attach方法裡究竟干了什麼?

哦,對了,Attach方法還有一個重載版本,我們來試一下吧。

public void UpdateProduct(Product product)
{
   NorthwindDataContext db = new NorthwindDataContext();
   Product oldProduct = db.Products.SingleOrDefault(p => p.ProductID ==  product.ProductID);
   db.Products.Attach(product, oldProduct);
   db.SubmitChanges();
}

還是Exception 1的錯誤!

我就倒!Attach啊Attach,你究竟怎麼了?

探索LINQ to SQL源代碼

我們使用Reflector的FileDisassembler插件,將System.Data.Linq.dll反編譯成cs代碼,並生成項目 文件,這有助於我們在Visual Studio中進行查找和定位。

什麼時候拋出Exception 1?

我們先從System.Data.Linq.resx中找到Exception 1所描述的信息,得到鍵 “CannotAttachAddNonNewEntities”,然後找到 System.Data.Linq.Error.CannotAttachAddNonNewEntities()方法,查找該方法的所有引用,發現在兩個 地方使用了該方法,分別為StandardChangeTracker.Track方法和InitializeDeferredLoader方法。

我們再打開Table.Attach(entity, bool)的代碼,不出所料地發現它調用了 StandardChangeTracker.Track方法(Attach(entity, entity)方法中也是如此):

trackedObject = this.context.Services.ChangeTracker.Track(entity,  true);

在Track方法中,拋出Exception 1的是下面的代碼:

if (trackedObject.HasDeferredLoaders)
{
   throw System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}

於是我們將注意力轉移到StandardTrackedObject.HasDeferredLoaders屬性上來:

internal override bool HasDeferredLoaders
{
   get
   {
     foreach (MetaAssociation association in this.Type.Associations)
     {
       if (this.HasDeferredLoader(association.ThisMember))
       {
         return true;
       }
     }
     foreach (MetaDataMember member in from p in  this.Type.PersistentDataMembers
       where p.IsDeferred && !p.IsAssociation 
       select p)
     {
       if (this.HasDeferredLoader(member))
       {
         return true;
       }
     }
     return false;
   }
}

從中我們大致可以推出,

只要實體中存在延遲加載的項時,執行Attach操作就會拋出Exception 1

。這正好符合我們發生Exception 1的場景——Product類含有延遲加載的項。

那麼避免該異常的方法也浮出水面了——移除Product中需要延遲加載的項。如何移除呢?可以使用 DataLoadOptions立即加載,也可以

將需要延遲加載的項設置為null

。但是第一種方法行不通,只好使用第二種方法了。

// List 2

class ProductRepository 
{
   public Product GetProduct(int id)
   {
     NorthwindDataContext db = new NorthwindDataContext();
     return db.Products.SingleOrDefault(p => p.ProductID == id);
   }

   public Product GetProductNoDeffered(int id)
   {
     NorthwindDataContext db = new NorthwindDataContext();
     //DataLoadOptions options = new DataLoadOptions();
     //options.LoadWith<Product>(p => p.Category);
     //db.LoadOptions = options;
     var product = db.Products.SingleOrDefault(p => p.ProductID ==  id);
     product.Category = null;
     return product;
   }

   public void UpdateProduct(Product product)
   {
     NorthwindDataContext db = new NorthwindDataContext();
     db.Products.Attach(product, true);
     db.SubmitChanges();
   }
}

// Client code
ProductRepository repository = new ProductRepository();
Product product = repository.GetProductNoDeffered(1);
product.ProductName = "Chai Changed";
repository.UpdateProduct(product);

什麼時候拋出Exception 2?

按照上一節的方法,我們很快找到了拋出Exception 2的代碼,幸運的是,整個項目中只有這一處:

if (asModified && ((inheritanceType.VersionMember == null) &&  inheritanceType.HasUpdateCheck))
{
   throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}

可以看到,當Attach的第二個參數asModified為true、不包含RowVersion列(VersionMember=null) 、且含有更新檢查的列(HasUpdateCheck)時,會拋出Exception 2。HasUpdateCheck的代碼如下:

public override bool HasUpdateCheck 
{
   get
   {
     foreach (MetaDataMember member in this.PersistentDataMembers)
     {
       if (member.UpdateCheck != UpdateCheck.Never)
       {
         return true;
       }
     }
     return false;
   }
}

這也符合我們的場景——Products表沒有RowVersion列,並且設計器自動生成的代碼中,所有字段的 UpdateCheck特性均為默認的Always,即HasUpdateCheck屬性為true。

避免Exception 2的方法就更簡單了,為所有表都添加TimeStamp列或對所有表的主鍵字段上設置 IsVersion=true字段。由於後一種方法要修改自動生成的類,並隨時都會被新的設計所覆蓋,因此我建議 使用前一種方法。

如何使用Attach方法?

經過上面的分析,我們可以找出與Attach方法相關的兩個條件:是否有RowVersion列以及是否存在外 鍵關聯(即需要延遲加載的項)。我將這兩個條件與Attach的幾個重載使用的情況總結出了一個表,在看 下面這個表時,你需要做好充分的心理准備。

序號 Attach方法 RowVersion列 是否有關聯 描述 1 Attach(entity) 否 否 沒有修改 2 Attach(entity) 否 是 NotSupportException: 已嘗試Attach或Add實體,該實體不是新實體,可能是從其他 DataContext中加載來的。不支持這種操作。 3 Attach(entity) 是 否 沒有修改 4 Attach(entity) 是 是 沒有修改。如果子集沒有RowVersion列則與

2

一樣。

5 Attach(entity, true) 否 否 InvalidOperationException:如果實體聲明了版本成員或者沒有更新檢查策略,則只能將它 附加為沒有原始狀態的已修改實體。 6 Attach(entity, true) 否 是 NotSupportException: 已嘗試Attach或Add實體,該實體不是新實體,可能是從其他 DataContext中加載來的。不支持這種操作。 7 Attach(entity, true) 是 否 正常修改(強制修改RowVersion列會報錯) 8 Attach(entity, true) 是 是 NotSupportException: 已嘗試Attach或Add實體,該實體不是新實體,可能是從其他 DataContext中加載來的。不支持這種操作。 9 Attach(entity, entity) 否 否 DuplicateKeyException:不能添加其鍵已在使用中的實體。 10 Attach(entity, entity) 否 是 NotSupportException: 已嘗試Attach或Add實體,該實體不是新實體,可能是從其他 DataContext中加載來的。不支持這種操作。 11 Attach(entity, entity) 是 否 DuplicateKeyException:不能添加其鍵已在使用中的實體。 12 Attach(entity, entity) 是 是 NotSupportException: 已嘗試Attach或Add實體,該實體不是新實體,可能是從其他 DataContext中加載來的。不支持這種操作。

Attach居然只能在第7種情況(包含RowVersion列並且無外鍵關聯)時才能正常更新!而這種情況對於 一個基於數據庫的系統來說,幾乎不可能出現!這是一個什麼樣的API啊?

總結

讓我們平靜一下心情,開始總結吧。

如果像List 0那樣,直接在UI裡寫LINQ to SQL代碼,則什麼不幸的事也不會發生。但是如果要抽象出 一個單獨的數據訪問層,災難就會降臨。這是否說明LINQ to SQL不適合多層架構的開發?很多人都說 LINQ to SQL適合小型系統的開發,但小型不意味著不分層啊。有沒有什麼辦法避免這麼多的異常發生呢 ?

本文其實已經給出了一些線索,在本系列的下一篇隨筆中,我將嘗試著提供幾種解決方案供大家選擇 。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved