LINQ to SQL,說好的更新呢?
自從學會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.1
UPDATE [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);讓資料被判定為有變動,以觸發資料更新。