剖析 ADO.NET 批處理更新(深入研究數據訪問)
2024-07-10 13:03:18
供稿:網友
ado.net 應用程序和基礎數據源之間的交互基于一個具有雙向信道的雙體系結構。您可以使用各個特定于提供程序的命令或批處理更新過程來訪問數據源,以讀取和寫入行。在這兩種情況下,數據訪問都會產生完全雙向綁定,并涉及各種不同的對象和方法。您可以使用如 sqlcommand 和 oledbcommand 等命令類來執行單個命令。可使用數據適配器對象來下載斷開連接的數據,提交更新的行集。雖然 “數據集” 是數據適配器用于返回和提交記錄塊的容器對象,但各個命令通過數據讀取器對象返回數據。
更新是通過各個命令、存儲過程完成的,通常,托管提供程序理解的任何命令文本一般都被稱為更新。更新命令總是執行嵌入在語句正文中的新數據。更新命令總是需要一個打開的連接,可能還需要一個正在進行的事務處理或一個新的事務處理。批處理更新則是一個略有不同的方法分支。從最高的抽象級別來看,您并不發出命令,無論它可能有多么復雜。取而代之的是,您提交在客戶端修改的當前行的快照,并等待數據源批準。批處理更新背后的關鍵概念是數據斷開連接的概念。您下載行表,通常為數據集,根據需要在客戶端對它進行修改,然后將這些行的新映像提交到數據庫服務器。您所作的是提交更改,而不是執行一個對數據源創建更改的命令。這就是更新(我在 july column 一文中討論過這個問題)和批處理更新之間的本質區別。
下圖說明了 ado.net 的雙更新體系結構。
圖 1. ado.net 應用程序和數據源之間的兩個雙向交互
在進一步詳細討論 ado.net 批處理更新之前,我需要闡明常常會導致某種誤解的批處理更新模型的一個方面。雖然更新和批處理更新在 ado.net 內的實際實現方面有著本質的區別,但它們遵循的是同一個更新模型。更新和批處理更新都是通過直接的并且特定于提供程序的語句來完成的。當然,由于批處理更新通常涉及到更多的行,所以這些語句會被組合為一個批處理調用。批處理更新會對目標數據集的行進行從頭到尾的循環,只要發現更新的行,就會發出適當的更新命令(insert、delete 或 update)。對更新的行進行通信時,將運行一個預定義的直接 sql 命令。從本質上來說,這就是批處理更新。
這個過程是理所當然的。實際上,如果批處理更新使用完全不同的更新模型,就需要來自數據源的特殊支持。(這正是向 sql server 2000 提交 xml updategram 時發生的情況。)批處理更新只是一個用來簡化多個行更新提交的客戶端提供的軟件機制。在任何情況下,每個新行提交總是通過數據源直接命令的正常通道完成的。
到目前為止,本文只提及了 sql 命令,但這些提及的內容都明確表明了 ado 批處理更新實現和 ado.net 批處理更新實現之間的一個重要區別。在 ado 中,批處理更新只可能發生在基于 sql 的數據源上。而在 ado.net 中,批處理更新則可能發生在任何種類的托管提供程序上,其中包括那些不應該通過 sql 查詢語言公開其數據的托管提供程序。現在,我們可以開始討論 ado.net 批處理更新編程的關鍵內容了。
準備用于提交的數據集
ado.net 批處理更新通過數據適配器對象的 “更新” 方法進行。數據只能以每個表為基礎進行提交。如果您調用 “更新” 時沒有指定表名,則使用 table 這個默認的表名。如果不存在具有該名稱的表,則會產生異常。“更新” 首先檢查每個表行的 rowstate 屬性,然后為所指定表中的每個插入行、更新行或刪除行準備自定義的 insert、update 或 delete 語句。
“更新” 方法有幾個超載。它可以采用數據集和數據表提供的對、某個數據表、甚至是一個 datarow 對象數組。該方法會返回一個整數值,即成功更新的行數。
為了最大限度地減少網絡通信,通常會對正在操作的數據集的一個子集調用 “更新”。毫無疑問,這個子集只包含當時已修改的行。您可以通過調用數據集的 getchanges 方法來獲得這樣的子集。
if (ds.haschanges())
{
dataset dschanges = ds.getchanges();
adapter.update(dschanges, "mytable");
}
另外,您可以使用 haschanges 方法檢查數據集是否發生了更改。haschanges 返回一個布爾值。
getchanges 返回的數據集包含當時已插入、刪除或修改的行。但這里所說的當時是什么時間呢?這正是 ado.net 批處理更新比較復雜的一個方面,必須與表行的當前狀態一起處理。
返回頁首
行的狀態
“數據表” 中的每一行都是通過 datarow 對象呈現的。datarow 對象主要是作為父 “數據表” 對象的 rows 集合的一個元素而存在的。從概念上來看,數據庫行固有地鏈接到了某個給定表的結構。就是由于這個原因,ado.net 中的 datarow 類不提供公用構造函數。創建新 datarow 對象的唯一方式是借助于對 “數據表” 對象的某個實時實例調用名為 newrow 的方法。剛剛創建好的行還不屬于父表的 rows 集合,但該行與此集合的關系決定了該行的狀態。下表顯示了 rowstate 屬性的一些可取值。這些值組合在了 datarowstate 枚舉中。
added
該行已添加到表中。
deleted
該行已標記為從父表刪除。
detached
該行已創建但尚未添加到表中,或者該行已從表行的集合中刪除。
modified
該行中的某些列已更改。
added
該行已添加到表中。
unchanged
在創建后或上次調用 acceptchanges 方法后未對該行進行任何更改。
每一行的 rowstate 屬性都會影響 haschanges 方法的返回值以及 getchanges 返回的子數據集的內容。
從這些可取值的范圍可以看出,rowstate 的值主要取決于對行已經執行的操作。ado.net 表基于兩個方法 - acceptchanges 和 rejectchanges - 來實現類似事務處理的提交模型。從數據源下載表時或在內存中新建表時,所有行都是沒有更改的。您輸入的所有更改不會立即變為永久性更改,隨時都可以通過調用 rejectchanges 來回滾更改。您可以在三個級別調用 rejectchanges 方法:
• 在數據集級別上可拒絕所有更改(無論是什么更改)。
• 在數據表級別上可取消某個表中的所有更改。
• 在某個特定的行級別上可還原到該行以前的狀態。
方法 acceptchanges 能夠提交所有正在進行的更改。它使得數據集會將當前值接受為新的原始值。因此,所有掛起的更改都被清除。與 rejectchanges 一樣,也可以對整個數據集、某個表或某個行調用 acceptchanges。
當您開始一個批處理更新操作時,只會考慮提交那些標記為 added、deleted 和 modified 的行。如果您恰好在批處理更新之前調用了 acceptchanges,則對數據源不進行任何持久更改。
另一方面,一旦批處理更新操作成功完成,您必須調用 acceptchanges 來清除掛起的更改,并將當前數據集值標記為原始值。注意,如果省略了最后對 acceptchanges 的調用,數據集中則會保留掛起的更改,從而導致在下次進行批處理更新時重新發出這些更改。
// get changes in the dataset
dschanges = ds.getchanges();
// performs the batch update for the given table
da.update(dschanges, strtable);
// clears any pending change in memory
ds.acceptchanges();
上面的代碼說明了 ado.net 批處理更新背后的三個主要步驟。
如果從數據集表中刪除行,請注意您使用的方法是 “刪除” 還是 “移除”。“刪除” 方法會通過將行標記為 “刪除”,執行邏輯刪除。而 “移除” 方法則從 rows 集合中物理刪除該行。因此,通過 “移除” 刪除的行不會標記為刪除,因此在后面的批處理更新期間也不會被處理。如果您的最終刪除目標是從數據源刪除行,則應使用 “刪除”。
返回頁首
更新的深入內容
有三個操作可改變表的狀態:
• 插入一個新行
• 刪除一個現有的行
• 更新一個現有的行
對于其中的每一個關鍵操作,數據適配器都會定義一個作為屬性公開的自定義的命令對象。這樣的屬性包括 insertcommand、deletecommand 和 updatecommand。程序員負責為這些屬性分配有意義的命令對象,例如,sqlcommand 對象。
僅提供的 insertcommand、deletecommand 和 updatecommand 屬性就代表了從 ado 到 ado.net 的巨大突破。利用這種屬性,您可以對內存中的更新提交到數據庫服務器的方式進行前所未有的控制。如果您不滿意 ado.net 生成的更新代碼,現在則可以修改這些更新代碼,而不會否定批處理更新的整體特性。使用 ado 的時候,您對庫靜默生成的 sql 命令毫無控制權。而在 ado.net 中,利用公開顯示的命令對象,您可以使用更符合用戶期望的自定義存儲過程或 sql 語句來應用更新。特別是,您可以對交叉引用的表使用批處理更新系統,甚至可以諸如 active directory™ 或 indexing services 這樣的非 sql 數據提供程序為目標。
更新命令應該針對表中每個更改的行運行,并且必須非常通用,以適應不同的值。對于這種任務,非常適合使用命令參數,只要您可以將它們綁定到數據庫列的值。ado.net 參數對象公開兩個用于這種綁定的屬性,例如, sourcecolumn 和 sourceversion。尤其是 sourcecolumn,它表示一種指示參數值的間接方式。您可以使用列名設置 sourcecolumn 屬性,并且使批處理更新機制不時地提取有效值,而不是使用 value 屬性并用標量值設置它。
sourceversion 指示應該讀取列上的哪個值。默認情況下,ado.net 會返回行的當前值。另一種方法是,您可以選擇原始值和 datarowversion 枚舉中的所有值。
如果您希望對 northwind 的 employees 表中的幾個列進行批處理更新,可以使用以下自定義命令。insert 命令的定義如下:
stringbuilder sb = new stringbuilder("");
sb.append("insert employees (firstname, lastname) values(");
sb.append("@sfirstname, @slastname)");
da.insertcommand = new sqlcommand();
da.insertcommand.commandtext = sb.tostring();
da.insertcommand.connection = conn;
所有參數都將添加到數據適配器的 parameters 集合并綁定到一個數據表列。
sqlparameter p1 = new sqlparameter("@sfirstname", sqldbtype.nvarchar, 10);
p1.sourceversion = datarowversion.current;
p1.sourcecolumn = "firstname";
da.insertcommand.parameters.add(p1);
sqlparameter p2 = new sqlparameter("@slastname", sqldbtype.nvarchar, 30);
p2.sourceversion = datarowversion.current;
p2.sourcecolumn = "lastname";
da.insertcommand.parameters.add(p2);
注意,自動遞增的列不應該列在 insert 命令的語法中,因為它們的值是由數據源生成的。
update 命令需要確定一個特定的行來應用其更改。為此,您可以使用 where 子句,在該子句中對參數化的值與鍵字段進行比較。在這種情況下,where 子句中使用的參數必須綁定到行的原始值,而不是當前值。
stringbuilder sb = new stringbuilder("");
sb.append("update employees set ");
sb.append("[email protected], [email protected] ");
sb.append("where [email protected]");
da.updatecommand = new sqlcommand();
da.updatecommand.commandtext = sb.tostring();
da.updatecommand.connection = conn;
// p1 and p2 set as before
:
p3 = new sqlparameter("@nempid", sqldbtype.int);
p3.sourceversion = datarowversion.original;
p3.sourcecolumn = "employeeid";
da.updatecommand.parameters.add(p3);
最后,delete 命令需要用 where 子句來確定要刪除的行。在這種情況下,您需要使用行的原始版本來綁定參數值。
stringbuilder sb = new stringbuilder("");
sb.append("delete from employees ");
sb.append("where [email protected]");
da.deletecommand = new sqlcommand();
da.deletecommand.commandtext = sb.tostring();
da.deletecommand.connection = conn;
p1 = new sqlparameter("@nempid", sqldbtype.int);
p1.sourceversion = datarowversion.original;
p1.sourcecolumn = "employeeid";
da.deletecommand.parameters.add(p1);
sql 命令的實際結構取決于您。這些命令不一定是普通的 sql 語句,它們可以是更有效的存儲過程(如果您想采用這種方向)。如果存在某個很具體的風險 - 其他人可能更新您讀取和修改的行,那么您可能想采取一些更有效的防范措施。如果是這種情況,您可以在 delete 和 update 命令中使用一個限制性更強的 where 子句。where 子句可以明確地確定行,但同時還應確保所有列仍然保留原始值。
update employees
set [email protected]_field1, [email protected]_field2, ???…, [email protected]_fieldn
where [email protected]_field1 and
[email protected]_field2 and
:
[email protected]_fieldn
注意,您無需填充所有命令參數,只填充您計劃使用的那些即可。如果代碼要使用尚未指定的命令,則會引發異常。為批處理更新過程設置命令可能需要許多代碼,但您無需在每一次進行批處理更新時都編寫大量代碼。實際上,在相當多的情況下,ado.net 都能為您自動生成有效的更新命令。
返回頁首
命令生成器
要利用默認命令,必須滿足兩個要求。首先,必須為 selectcommand 屬性分配一個有效的命令對象。您無需填充其他命令對象,但 selectcommand 必須指向一個有效的查詢語句。用于批處理更新的有效查詢是返回主鍵列的查詢。另外,該查詢不得包括 inner join、計算的列,也不得引用多個表。
selectcommand 對象中列出的列和表實際上將用于準備更新和插入語句的正文。如果不設置 selectcommand,則無法實現 ado.net 命令自動生成。下面的代碼說明了如何為 selectcommand 屬性編寫代碼。
sqlcommand cmd = new sqlcommand();
cmd.commandtext = "select employeeid, firstname, lastname from employees";
cmd.connection = conn;
da.selectcommand = cmd;
不要擔心 selectcommand 可能對性能產生影響。相關的語句只在批處理更新過程之前執行一次,但它只檢索列元數據。無論您怎樣編寫 sql 語句,也永遠不會向調用程序返回任何行。發生這種情況的原因是,在執行時,selectcommand 追加到以下面的代碼開頭的 sql 批處理語句最后
set fmtonly off
set no_browsetable on
set fmtonly on
因此,查詢不返回行,而返回列元數據信息。
您的代碼必須滿足的第二個要求與命令生成器有關。命令生成器是一個特定于托管提供程序的類,它工作在數據適配器對象之上,并自動設置其 insertcommand、deletecommand 和 updatecommand 屬性。命令生成器首先運行 selectcommand,以收集有關所涉及表和列的足夠信息,然后會創建更新命令。實際的命令創建在命令生成器類構造函數中進行。
sqlcommandbuilder cb = new sqlcommandbuilder(da);
sqlcommandbuilder 類確保指定的數據適配器可成功地用于對特定的數據源進行批處理更新。sqlcommandbuilder 利用了 selectcommand 對象中定義的某些屬性。這些屬性是 connection、commandtimeout 和 transaction。只要更改其中的任何屬性,您就需要調用命令生成器的 refreshschema 方法來更改進一步批處理更新的生成命令的結構。
您可以混合使用命令生成器和自定義命令。如果 insertcommand 屬性在調用命令生成器之前指向一個有效的命令對象,生成器則只會為 deletecommand 和 updatecommand 生成代碼。而非空的 selectcommand 屬性才是命令生成器得以正常工作的關鍵。
通常,您之所以使用命令生成器,是因為您覺得自己編寫 sql 命令太復雜了。不過,如果您希望查看生成器生成的源代碼,則可以調用如 getinsertcommand、getupdatecommand 和 getdeletecommand 這樣的方法。
命令生成器是一個特定于提供程序的特性。因此,不可能期望所有類型的托管提供程序都支持它。sql server 7.0 和更高版本的提供程序以及 ole db 提供程序支持命令生成器。
命令生成器有一個很好的特性,它可以檢測自動遞增的字段,并相應地優化代碼。尤其是,只要它有辦法識別某些字段是自動遞增字段,就會將自動遞增字段從 insert 語句中提取出來。這個過程可以通過兩種方式來實現。例如,您可以手動設置相應的 datacolumn 對象的 autoincrement 屬性,或者,更好的方法是,使其基于列在數據源(如 sql server)中的屬性自動進行。要自動繼承這樣的屬性,請確保將數據適配器的 missingschemaaction 屬性從默認值 add 改為 addwithkey。
返回頁首
沖突檢測
批處理更新機制對并發有著很樂觀的看法。每個記錄在讀取后并不鎖定,仍然公開給其他用戶用于進行讀取和寫入。在這種情況下,可能會發生一些潛在的不一致的情形。例如,將某一行從 select 語句傳遞到您的應用程序之后,但在批處理更新過程真正將更改返回服務器之前,它可能進行了修改,甚至已被刪除。
如果您更新服務器上數據的同時,這些數據已經被另外的某個用戶修改,則可能會產生數據沖突。為了避免新的數據被覆蓋,ado.net 命令生成器會生成帶有 where 子句的語句,只有當數據源行的當前狀態與應用程序以前讀取時的狀態一致時,where 子句才生效。如果這樣的命令未能更新行,ado.net 運行時則會引發一個 dbconcurrencyexception 類型的異常。
下面的代碼片斷說明了如何以一種更準確的方法用 ado.net 執行批處理更新操作。
try
{
da.update(dschanges, "employees");
}
catch (dbconcurrencyexception dbdcex)
{
// resolve the conflict
}
您正在使用的數據適配器的 “更新” 方法對于第一個更新失敗的行會引發異常。此時,控制權又回到客戶端應用程序,批處理更新過程停止。不過,仍然會執行所有以前提交的更改。這個過程代表了從 ado 批處理更新模型到 ado.net 的另一個轉變。
通過 dbconcurrencyexception 類的 row 屬性可使用沖突更新中涉及的 datarow 對象。這個 datarow 對象包含行的提交值和原始值。它不包含某個給定列當前存儲在數據庫中的值。此值 - 即 ado 的 underlyingvalue 屬性 - 只能通過另一個查詢命令檢索。
解決沖突、并且有可能繼續進行批處理更新的方式是嚴格特定于應用程序的。如果存在您的應用程序需要繼續執行更新的情況,您則應該了解一個微妙的、然而卻很棘手的問題。想盡辦法解決了行上的沖突之后,還必須想出一種方法來接受批處理已成功完成的內存中行的更改。如果您忽略了這個技術細節,對于以前成功更新的第一個行將產生一個新的沖突!這種情況會反復不斷地發生,您的應用程序很快就會進入死鎖狀態。
返回頁首
小結
與 ado 相比,ado.net 中的批處理更新功能更強大,具有更高的可訪問性。在 ado 中,批處理更新機制是一種黑盒子,我們幾乎不可能深入其內部,也不可能略微改變一下您需要執行的任務。ado.net 中的批處理更新更偏向于一種低級的解決方案,它的實現為您進入其內部并控制事件提供了幾個切入點。ado.net 批處理更新最棘手的部分是沖突解決。作者真心建議您盡可能將更多的時間用于測試、再測試。這種投資可通過命令生成器節省的所有時間來得到回報。
返回頁首
對話欄:數據表中的 null 值
我從數據庫提取數據集,一切順利。然后我嘗試將此數據集保存到 xml 文件,仍然很順利。但將這個 xml 文件讀回數據集時,問題出現了。這是因為,所有具有 null 值的列不能持久地保存到 xml 中。是否可以利用某種方法,使 null 值作為空標記添加到所得到的 xml?
這種行為是設計使然,是隨著在 xml 序列化過程中保存幾個字節這種最佳的意圖引入的。如果這種行為發生在網絡上(比如,在 xml web 服務內),它所帶來的優勢會非常明顯。
也就是說,可以用一個很簡單的辦法來解決您的問題。這個竅門就是,通過 isnull t-sql 函數提取列。我們不使用以下代碼:
select mycolumn from mytable
而應該使用:
select isnull(mycolumn, '') from mytable
在這種情況下,列的任何 null 值將自動變成空字符串,并且不會在數據集轉換為 xml 的序列化過程中被忽略。非特定值不一定是空字符串。數值列可以使用 0 或任何其他您希望使用的邏輯空值。