|
在學(xué)習(xí)LINQ時(shí),我?guī)缀醣灰粋€(gè)困難所擊倒,這就是你從標(biāo)題中看到的更新數(shù)據(jù)庫的操作。下面我就一步步帶你走入這泥潭,請(qǐng)準(zhǔn)備好磚頭和口水,F(xiàn)ollow me。
從最簡單的情況入手
我們以Northwind數(shù)據(jù)庫為例,當(dāng)需要修改一個(gè)產(chǎn)品的ProductName時(shí),可以在客戶端直接寫下這樣的代碼:
// List 0
NorthwindDataContext db = new NorthwindDataContext();Product product = db.Products.Single(p => p.ProductID == 1);product.ProductName = "Chai Changed";db.SubmitChanges();
測試一下,更新成功。不過我相信,在各位的項(xiàng)目中不會(huì)出現(xiàn)這樣的代碼,因?yàn)樗喼睕]法復(fù)用。好吧,讓我們對(duì)其進(jìn)行重構(gòu),提取至一個(gè)方法中。參數(shù)應(yīng)該是什么呢?是新的產(chǎn)品名稱,以及待更新的產(chǎn)品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();}
在實(shí)際的項(xiàng)目中,我們不可能僅僅只修改產(chǎn)品名稱。Product的其他字段同樣也是修改的對(duì)象。那么UpdateProduct方法的簽名將變成如下的形式:
public void UpdateProduct(int id, string productName, int suplierId, int categoryId, string quantityPerUnit, decimal unitPrice, short unitsInStock, short unitsOnOrder, short reorderLevel)
當(dāng)然這只是簡單的數(shù)據(jù)庫,在實(shí)際項(xiàng)目中,二十、三十甚至上百個(gè)字段的情況也不少見。誰能忍受這樣的方法呢?這樣寫,還要Product對(duì)象干什么呢?
對(duì)啊,把Product作為方法的參數(shù),把惱人的賦值操作拋給客戶代碼吧。同時(shí),我們將獲取Product實(shí)例的代碼提取出來,形成GetProduct方法,并且將與數(shù)據(jù)庫操作相關(guān)的方法放到一個(gè)專門負(fù)責(zé)和數(shù)據(jù)庫打交道的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 codeProductRepository repository = new ProductRepository();Product product = repository.GetProduct(1);product.ProductName = "Chai Changed";repository.UpdateProduct(product);
在這里我使用了Attach方法,將Product的一個(gè)實(shí)例附加到其他的DataContext上。對(duì)于默認(rèn)的Northwind數(shù)據(jù)庫來說,這樣做的結(jié)果就是得到下面的異常:
// Exception 1
NotSupportException:已嘗試Attach或Add實(shí)體,該實(shí)體不是新實(shí)體,可能是從其他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我們知道,在將實(shí)體序列化到客戶端時(shí),這些實(shí)體會(huì)與其原始DataContext分離。DataContext不再跟蹤這些實(shí)體的更改或它們與其他對(duì)象的關(guān)聯(lián)。這時(shí)如果要更新或者刪除數(shù)據(jù),則必須在調(diào)用SubmitChanges之前使用Attach方法將實(shí)體附加到新的DataContext中,否則就會(huì)拋出上面的異常。
而在Northwind數(shù)據(jù)庫中,Product類包含三個(gè)與之相關(guān)的類(即外鍵關(guān)聯(lián)):Order_Detail、Category和Supllier。在上面的例子中,我們雖然把Product進(jìn)行了Attach,但卻沒有Attach與其相關(guān)聯(lián)的類,因此拋出NotSupportException。
那么如何關(guān)聯(lián)與Product相關(guān)的類呢?這看上去似乎十分復(fù)雜,即便簡單地如Northwind這樣的數(shù)據(jù)庫亦是如此。我們似乎必須先獲取與原始Product相關(guān)的Order_Detail、Category和Supllier的原始類,然后再分別Attach到當(dāng)前的DataContext中,但實(shí)際上即使這樣做也同樣會(huì)拋出NotSupportException。
那么究竟該如何實(shí)現(xiàn)更新操作呢?為了簡便起見,我們刪除Northwind.dbml中的其他實(shí)體類,只保留Product。這樣就可以從最簡單的情況開始入手分析了。
問題重重
刪除其他類之后,我們?cè)俅螆?zhí)行List 1中的代碼,然而數(shù)據(jù)庫并沒有更改產(chǎn)品的名稱。通過查看Attach方法的重載版本,我們很容易發(fā)現(xiàn)問題所在。
Attach(entity)方法默認(rèn)調(diào)用Attach(entity, false)重載,它將以未修改的狀態(tài)附加相應(yīng)實(shí)體。如果Product對(duì)象沒有被修改,那么我們應(yīng)該調(diào)用該重載版本,將Product對(duì)象以未修改的狀態(tài)附加到DataContext,以便后續(xù)操作。而此時(shí)的Product對(duì)象的狀態(tài)是“已修改”,我們只能調(diào)用Attach(entity, true)方法。
于是我們將List 1的相關(guān)代碼改為Attach(product, true),看看發(fā)生了什么?
// Exception 2
InvalidOperationException:如果實(shí)體聲明了版本成員或者沒有更新檢查策略,則只能將它附加為沒有原始狀態(tài)的已修改實(shí)體。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列來實(shí)現(xiàn)默認(rèn)的樂觀式并發(fā)檢查,否則在以修改狀態(tài)向DataContext附加實(shí)體的時(shí)候,就會(huì)出現(xiàn)上面的錯(cuò)誤。實(shí)現(xiàn)RowVersion列的方法有兩種,一種是為數(shù)據(jù)庫表定義一個(gè)timestamp類型的列,另一種方法是在表主鍵所對(duì)應(yīng)的實(shí)體屬性上,定義IsVersion=true特性。注意,不能同時(shí)擁有TimeStamp列和IsVersion=true特性,否則將拋出InvalidOprationException:成員“System.Data.Linq.Binary TimeStamp”和“Int32 ProductID”都標(biāo)記為行版本。在本文中,我們使用timestamp列來舉例。
為Products表建立名為TimeStamp、類型為timestamp的列之后,將其重新拖拽到設(shè)計(jì)器中,然后執(zhí)行List 1中的代碼。謝天謝地,終于成功了。
現(xiàn)在,我們?cè)傧蛟O(shè)計(jì)器中拖入Categories表。這次學(xué)乖了,先在Categories表中添加timestamp列。測試一下,居然又是Exception 1中的錯(cuò)誤!刪除Categories的timestamp列,問題依舊。天哪,可怕的Attach方法里究竟干了什么?
哦,對(duì)了,Attach方法還有一個(gè)重載版本,我們來試一下吧。
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();}
我就倒!Attach啊Attach,你究竟怎么了?
探索LINQ to SQL源代碼
我們使用Reflector的FileDisassembler插件,將System.Data.Linq.dll反編譯成cs代碼,并生成項(xiàng)目文件,這有助于我們?cè)赩isual Studio中進(jìn)行查找和定位。
什么時(shí)候拋出Exception 1?
我們先從System.Data.Linq.resx中找到Exception 1所描述的信息,得到鍵“CannotAttachAddNonNewEntities”,然后找到System.Data.Linq.Error.CannotAttachAddNonNewEntities()方法,查找該方法的所有引用,發(fā)現(xiàn)在兩個(gè)地方使用了該方法,分別為StandardChangeTracker.Track方法和InitializeDeferredLoader方法。
我們?cè)俅蜷_Table.Attach(entity, bool)的代碼,不出所料地發(fā)現(xiàn)它調(diào)用了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();}
于是我們將注意力轉(zhuǎn)移到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; }}
從中我們大致可以推出,只要實(shí)體中存在延遲加載的項(xiàng)時(shí),執(zhí)行Attach操作就會(huì)拋出Exception 1。這正好符合我們發(fā)生Exception 1的場景——Product類含有延遲加載的項(xiàng)。
那么避免該異常的方法也浮出水面了——移除Product中需要延遲加載的項(xiàng)。如何移除呢?可以使用DataLoadOptions立即加載,也可以將需要延遲加載的項(xiàng)設(shè)置為null。但是第一種方法行不通,只好使用第二種方法了。
// List 2class 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(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 codeProductRepository repository = new ProductRepository();Product product = repository.GetProductNoDeffered(1);product.ProductName = "Chai Changed";repository.UpdateProduct(product);
什么時(shí)候拋出Exception 2?
按照上一節(jié)的方法,我們很快找到了拋出Exception 2的代碼,幸運(yùn)的是,整個(gè)項(xiàng)目中只有這一處:
if (asModified && ((inheritanceType.VersionMember == null) && inheritanceType.HasUpdateCheck)){ throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();}
可以看到,當(dāng)Attach的第二個(gè)參數(shù)asModified為true、不包含RowVersion列(VersionMember=null)、且含有更新檢查的列(HasUpdateCheck)時(shí),會(huì)拋出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列,并且設(shè)計(jì)器自動(dòng)生成的代碼中,所有字段的UpdateCheck特性均為默認(rèn)的Always,即HasUpdateCheck屬性為true。
避免Exception 2的方法就更簡單了,為所有表都添加TimeStamp列或?qū)λ斜淼闹麈I字段上設(shè)置IsVersion=true字段。由于后一種方法要修改自動(dòng)生成的類,并隨時(shí)都會(huì)被新的設(shè)計(jì)所覆蓋,因此我建議使用前一種方法。
如何使用Attach方法?
經(jīng)過上面的分析,我們可以找出與Attach方法相關(guān)的兩個(gè)條件:是否有RowVersion列以及是否存在外鍵關(guān)聯(lián)(即需要延遲加載的項(xiàng))。我將這兩個(gè)條件與Attach的幾個(gè)重載使用的情況總結(jié)出了一個(gè)表,在看下面這個(gè)表時(shí),你需要做好充分的心理準(zhǔn)備。
序號(hào) | Attach方法 | RowVersion列 | 是否有關(guān)聯(lián) | 描述 |
1 | Attach(entity) | 否 | 否 | 沒有修改 |
2 | Attach(entity) | 否 | 是 | NotSupportException: 已嘗試Attach或Add實(shí)體,該實(shí)體不是新實(shí)體,可能是從其他DataContext中加載來的。不支持這種操作。 |
3 | Attach(entity) | 是 | 否 | 沒有修改 |
4 | Attach(entity) | 是 | 是 | 沒有修改。如果子集沒有RowVersion列則與2一樣。 |
5 | Attach(entity, true) | 否 | 否 | InvalidOperationException:如果實(shí)體聲明了版本成員或者沒有更新檢查策略,則只能將它附加為沒有原始狀態(tài)的已修改實(shí)體。 |
6 | Attach(entity, true) | 否 | 是 | NotSupportException: 已嘗試Attach或Add實(shí)體,該實(shí)體不是新實(shí)體,可能是從其他DataContext中加載來的。不支持這種操作。 |
7 | Attach(entity, true) | 是 | 否 | 正常修改(強(qiáng)制修改RowVersion列會(huì)報(bào)錯(cuò)) |
8 | Attach(entity, true) | 是 | 是 | NotSupportException: 已嘗試Attach或Add實(shí)體,該實(shí)體不是新實(shí)體,可能是從其他DataContext中加載來的。不支持這種操作。 |
9 | Attach(entity, entity) | 否 | 否 | DuplicateKeyException:不能添加其鍵已在使用中的實(shí)體。 |
10 | Attach(entity, entity) | 否 | 是 | NotSupportException: 已嘗試Attach或Add實(shí)體,該實(shí)體不是新實(shí)體,可能是從其他DataContext中加載來的。不支持這種操作。 |
11 | Attach(entity, entity) | 是 | 否 | DuplicateKeyException:不能添加其鍵已在使用中的實(shí)體。 |
12 | Attach(entity, entity) | 是 | 是 | NotSupportException: 已嘗試Attach或Add實(shí)體,該實(shí)體不是新實(shí)體,可能是從其他DataContext中加載來的。不支持這種操作。 |
Attach居然只能在第7種情況(包含RowVersion列并且無外鍵關(guān)聯(lián))時(shí)才能正常更新!而這種情況對(duì)于一個(gè)基于數(shù)據(jù)庫的系統(tǒng)來說,幾乎不可能出現(xiàn)!這是一個(gè)什么樣的API???
總結(jié)
讓我們平靜一下心情,開始總結(jié)吧。
如果像List 0那樣,直接在UI里寫LINQ to SQL代碼,則什么不幸的事也不會(huì)發(fā)生。但是如果要抽象出一個(gè)單獨(dú)的數(shù)據(jù)訪問層,災(zāi)難就會(huì)降臨。這是否說明LINQ to SQL不適合多層架構(gòu)的開發(fā)?很多人都說LINQ to SQL適合小型系統(tǒng)的開發(fā),但小型不意味著不分層啊。有沒有什么辦法避免這么多的異常發(fā)生呢?
本文其實(shí)已經(jīng)給出了一些線索,在本系列的下一篇隨筆中,我將嘗試著提供幾種解決方案供大家選擇。
相關(guān)文章
NET技術(shù):使用LINQ to SQL更新數(shù)據(jù)庫(上):問題重重,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。