剖析 ADO.NET 批處理更新(深入研究資料訪問)

來源:互聯網
上載者:User
ado|訪問|資料 ADO.NET 應用程式和基礎資料來源之間的互動基於一個具有雙向通道的雙體繫結構。您可以使用各個特定於提供者的命令或批處理更新過程來訪問資料來源,以讀取和寫入行。在這兩種情況下,資料訪問都會產生完全雙向繫結,並涉及各種不同的對象和方法。您可以使用如 SqlCommand 和 OleDbCommand 等命令類來執行單個命令。可使用資料配接器對象來下載中斷連線的資料,提交更新的行集。雖然 “資料集” 是資料配接器用於返回和提交記錄塊的容器物件,但各個命令通過資料讀取器對象返回資料。



更新是通過各個命令、預存程序完成的,通常,Managed 提供者理解的任何命令文本一般都被稱為更新。更新命令總是執行嵌入在語句本文中的新資料。更新命令總是需要一個開啟的串連,可能還需要一個進行中的交易處理或一個新的交易處理。批處理更新則是一個略有不同的方法分支。從最高的抽象層級來看,您並不發出命令,無論它可能有多麼複雜。取而代之的是,您提交在用戶端修改的當前行的快照,並等待資料來源批准。批處理更新背後的關鍵概念是資料中斷連線的概念。您下載行表,通常為資料集,根據需要在用戶端對它進行修改,然後將這些行的新映像提交到資料庫伺服器。您所作的是提交更改,而不是執行一個對資料來源建立更改的命令。這就是更新(我在 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 中,批處理更新則可能發生在任何種類的Managed 提供者上,其中包括那些不應該通過 SQL 查詢語言公開其資料的Managed 提供者。現在,我們可以開始討論 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("lastname=@sLastName, firstname=@sFirstName ");
sb.Append("WHERE employeeid=@nEmpID");
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 employeeid=@nEmpID");
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 field1=@new_field1, field2=@new_field2, ???…, fieldn=@new_fieldn
WHERE field1=@old_field1 AND
field2=@old_field2 AND
:
fieldn=@old_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

因此,查詢不返回行,而返回列中繼資料資訊。

您的代碼必須滿足的第二個要求與命令產生器有關。命令產生器是一個特定於Managed 提供者的類,它工作在資料配接器對象之上,並自動化佈建其 InsertCommand、DeleteCommand 和 UpdateCommand 屬性。命令產生器首先運行 SelectCommand,以收集有關所涉及表和列的足夠資訊,然後會建立更新命令。實際的命令建立在命令產生器類建構函式中進行。

SqlCommandBuilder cb = new SqlCommandBuilder(da);

SqlCommandBuilder 類確保指定的資料配接器可成功地用於對特定的資料來源進行批處理更新。SqlCommandBuilder 利用了 SelectCommand 對象中定義的某些屬性。這些屬性是 Connection、CommandTimeout 和 Transaction。只要更改其中的任何屬性,您就需要調用命令產生器的 RefreshSchema 方法來更改進一步批處理更新的產生命令的結構。

您可以混合使用命令產生器和自訂命令。如果 InsertCommand 屬性在調用命令產生器之前指向一個有效命令對象,產生器則只會為 DeleteCommand 和 UpdateCommand 產生代碼。而非空的 SelectCommand 屬性才是命令產生器得以正常工作的關鍵。

通常,您之所以使用命令產生器,是因為您覺得自己編寫 SQL 命令太複雜了。不過,如果您希望查看產生器產生的原始碼,則可以調用如 GetInsertCommand、GetUpdateCommand 和 GetDeleteCommand 這樣的方法。

命令產生器是一個特定於提供者的特性。因此,不可能期望所有類型的Managed 提供者都支援它。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 值將自動變成Null 字元串,並且不會在資料集轉換為 XML 的序列化過程中被忽略。非特定值不一定是Null 字元串。數值列可以使用 0 或任何其他您希望使用的邏輯空值。


相關文章

E-Commerce Solutions

Leverage the same tools powering the Alibaba Ecosystem

Learn more >

Apsara Conference 2019

The Rise of Data Intelligence, September 25th - 27th, Hangzhou, China

Learn more >

Alibaba Cloud Free Trial

Learn and experience the power of Alibaba Cloud with a free trial worth $300-1200 USD

Learn more >

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。