LINQ to SQL,說好的更新呢?
4 |
自從學會LINQ to SQL一行資料庫更新法,它便成為我專案裡常用的技巧。對於彈性要求較高、嚴謹性要求較低的複雜資料,我還喜歡借重SQL 2005起新增的XML資料型別作為儲存欄位。透過LINQ to SQL對應,Xml欄位會變成System.Xml.Linq.XElement Class,XElement在建立與操作上文件又比.NET 2.0時代XmlDocument、XmlElement的做法便捷許多。
例如: 我手上有一個簡單的XmlStore資料表。
CREATE TABLE [dbo].[XmlStore]( [DocId] [varchar](16) NOT NULL, [XmlContent] [xml] NULL, [ModDateTime] [datetime] NOT NULL, CONSTRAINT [PK_XmlStore] PRIMARY KEY CLUSTERED ( [DocId] ASC )
用以下的程式片段三兩下就塞進一筆新資料。
PlaygroundDataClassesDataContext db =
new PlaygroundDataClassesDataContext();
db.Log = new DebuggerWriter();
XmlStore xs = new XmlStore()
{
DocId = "Dummy",
XmlContent = new XElement("Root",
new XElement("CodeName", "Darkthread")
),
ModDateTime = DateTime.Now
};
db.XmlStores.InsertOnSubmit(xs);
db.SubmitChanges();
用上回介紹過的DebuggerWriter,我們可以觀察到它轉化成的T-SQL
INSERT INTO [dbo].[XmlStore]([DocId], [XmlContent], [ModDateTime])
VALUES (@p0, @p1, @p2)
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- @p1: Input Xml (Size = 50; Prec = 0; Scale = 0) [<Root>
<CodeName>Darkthread</CodeName>
</Root>]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:11:03]
接著,用一行更新法, 試圖更新XmlContent的內容。
PlaygroundDataClassesDataContext db =
new PlaygroundDataClassesDataContext();
db.Log = new DebuggerWriter();
var xs = (from o in db.XmlStores
where o.DocId == "Dummy"
select o).Single();
XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.ModDateTime = DateTime.Now;
db.SubmitChanges();
噹! 踢到鐵板~~~
依據DebuggerWriter截錄的UPDATE T-SQL,它只更新了ModDateTime,並未更新XmlDocument。
SELECT [t0].[DocId], [t0].[XmlContent], [t0].[ModDateTime]
FROM [dbo].[XmlStore] AS [t0]
WHERE [t0].[DocId] = @p0
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1UPDATE [dbo].[XmlStore]
SET [ModDateTime] = @p2
WHERE ([DocId] = @p0) AND ([ModDateTime] = @p1)
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- @p1: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:11:03]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:12:52]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1
我知道LINQ to SQL會聰明地察覺哪些欄位有更動,只UPDATE必要的欄位,不會傻呼呼地所有欄位重新設定一次。我想,先前的寫法只改變XElement的內容,在LINQ to SQL的比對邏輯上並未被視為資料有異動。為了一探究竟,追進dbml對應的designer.cs,找到以下這段邏輯:
[Column(Storage="_XmlContent", DbType="Xml",
UpdateCheck=UpdateCheck.Never)]
public System.Xml.Linq.XElement XmlContent
{
get
{
return this._XmlContent;
}
set
{
if ((this._XmlContent != value))
{
this.OnXmlContentChanging(value);
this.SendPropertyChanging();
this._XmlContent = value;
this.SendPropertyChanged("XmlContent");
this.OnXmlContentChanged();
}
}
}
由這段程式來看,它用!=做新舊值比對,我取出XmlConent做修改,根本未執行到set段,自然不會觸發SendPropertyChanged等相關邏輯。
依此方向,我做了幾個實驗:
【測試1】
XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.XmlContent = xe;
無效!! 我想是因為對Reference Type來說,自始至終都只有一個Instance,xs.XmlConent與xe,this._XmlConent與value也就保持恆等。
【測試2】
XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.XmlContent = null;
xs.XmlContent = xe;
還是無效! 由Line-by-Line Debug,設定null再設回來的過程中,我偵測到this.SendPropertyChanged("XmlContent");被執行,但我猜想底一層的比對可能會回歸到如測試1 Reference Type自己等於自己的狀況,所以功敗垂成。
最後我測試成功的版本如下: (我找不到XElement有Clone() Method,所以用轉為XML字串再重新建成另一個XElement的做法。)
XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.XmlContent = XElement.Parse(xe.ToString());
令人感動的結果出現!
UPDATE [dbo].[XmlStore]
SET [XmlContent] = @p2, [ModDateTime] = @p3
WHERE ([DocId] = @p0) AND ([ModDateTime] = @p1)
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- @p1: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:22:39]
-- @p2: Input Xml (Size = 77; Prec = 0; Scale = 0) [<Root>
<CodeName>Darkthread</CodeName>
<Language>C#</Language>
</Root>]
-- @p3: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:24:51]
【結論】
在LINQ to SQL中如要更新XElement資料,可用xs.XmlContent = XElement.Parse(newXml);讓資料被判定為有變動,以觸發資料更新。
Comments
# by Will 保哥
這應該算 LINQ to SQL 的 Bug 吧?
# by Johnny
搞不好根本就是個 bug! ;-)
# by chicken
這種問題我也在 Entity Framework 碰過... XML 的例子跟你一樣我就不提了。我碰到的是 BLOB 的狀況... EDMX 一樣是透過 code gen 產生一組合用的 Get / Set,Set 的部份就用 value 是否跟原物件 reference equal 來決定要不要更新回資料庫的依據... BLOB 是被對應成 byte[], 如果你陣列沒換一個新的,只是改掉其中一個 byte, 那就被呼略掉了... 當然把它改成 value equal 才是正解,不過,byte[] 要做好比對內容很花時間吧 @@ 看來這是跟效能妥協的結果,犧牲掉一小部份這種語意正確,但效能很糟的情況... 換來整體的效能 @@ 害我想到之前我碰到 XmlWriter 的另一個 bug ... 感覺也是被犧牲掉的一員.. http://columns.chicken-house.net/post/e58e9fe4be86-XmlWellFormedWriter-e4b99fe69c89-Bug-.aspx 後續發展: http://columns.chicken-house.net/post/XmlWellFormedWriterWriteRaw(-)-e79a84-Bug-e5be8ce7ba8ce799bce5b195.aspx
# by Ark
恩 LINQ 是MS給白老鼠吃的一種新飼料 很多老鼠吃得很開心,但是也有吃到落賽的 下一版本叫 PLINQ , 預設目標是充分利用多核(餘)CPU作"平行"處理..... 我是這麼看 小朋友趴著爬著~一路跌跌撞撞,最後會走會跑還會跳 隨時光流逝,老的時候還可以拿柺杖和義肢和人對幹 如上~也是有小朋友就得拿拐杖的例子~但是社會還是期望他能有堅強開朗的笑容以便鼓舞經濟的不景氣.... 我的解釋是 實值型別的老牌咖都可以比對目前除了enum 這怪咖 參考型別 object 不能比 object(陣列算是) class 不能比 class (99.99%) System.String可以比 datetime會轉成 double來比 guid....如果這個不給比~應該不用玩了,只是不知是拿byte[32]還是轉型成string來比 能不能比~比的對不對~牽扯到一個語言boxing 轉型的特性 以上的比~equal 不equal 是指直接拿來比~間接的不算~那叫棉角 還有一個叫超級比一比的Regex ~每次看到這個我就懷念到綜藝節目比手畫腳的單元(我討厭啥歌唱PK大賽的~唱的又沒我好聽有啥好比的)~意指:一定得把身體扭曲到某種程度才有辦法被理解....也不知道是幕後策劃還是人腦真的可以理解那種扭曲....總而言之~算娛樂效果 Xml不在實值型別或可以直接比對的參考型別內 當成是為不能比對的class (新人總是矯情~做作~裝嫩~不給扭曲~還不懂八卦的好處與經濟效應) 所以就不比或是比不出來而沒能觸發 但是有new 的時候就可以連比都不必比就觸發(除了那些老牌咖 new完一定是不相等 XElement.Parse 相當於有new到~以前很笨不會理解的時候,我用背的:"有扭到一定不相等....有扭到一定不相等...." 可以跟著我念10次) 不知道是否正確 有疑惑可以用js 跑看看 var oo = [1.2], pp = [1, 2];(oo == pp) ? document.write("港款") : document.write("某港款"); 沒看到new ??? var oo = [1.2] 就是 var oo=new Array(1,2) 的意思 歲月不留人,更不會留程式碼,程式語言也是有生命週期的(高階的~低階的幾乎看不到啥變化,吃到退休還是那幾招),現在看到vbscript的code都有一種噁心的感覺