.NET 數據訪問架構指南(二)

使用自動化事務

自動化事務簡化了編程模型,因爲它們不需要明確地開始新事務處理過程,或明確執行或取消事務。然而,自動化事務的最大優點是它們能與DTC結合起來,這就使單個事務可以擴展到多個分佈式數據源中。在大型分佈式應用程序中,這個優點是很重要的。儘管通過手工對DTC直接編程來控制分佈式事務是可能的,但自動化事務處理極大的簡化了工作量,並且它是爲基於組件的系統而設計的。例如,可以方便地以說明方式配置多個組件以執行包含了單個事務處理的任務。

自動化事務依賴於COM+提供的分佈式事務處理支持特性。結果,只有服務組件(即從ServicedComponent類中派生的組件)能夠使用自動化事務。

要爲自動化事務處理配置類,操作如下:

  • 從位於EnterpriseServices名稱空間的ServicedComponent類中派生新類。
  • 通過Transaction屬性定義類的事務處理需求。來自TransactionOption的枚舉值決定了如何在COM+類中配置類。可與此屬性一同設置的其它屬性包括事務處理分離等級和超時上限。
  • 爲了避免必須明確選出事務處理結果,可以用AutoComplete屬性對方法進行註釋。如果這些方法釋放異常,事務將自動取消。注意,如果需要,仍可以直接挑選事務處理結果。更多詳情,見本文稍後確定事務處理結果的節。

更多信息

  • 關於COM+自動化事務的更多信息,可在平臺SDK文檔中搜索“通過COM+的自動化事務”獲取。
  • 關於.NE T事務處理類的示例,見附錄中的如何編碼.NET事務處理

配置事務處理分離級別

用於COM+1.0版--即運行在Windows 2000中的COM+--的事務處理分離級別被序列化了。這樣做提供了最高的分離等級,卻是以性能爲代價的。系統的整體吞吐量被降低了。因爲所涉及到的資源管理器(典型地是數據庫)在事務處理期間必須保持讀和寫鎖。在此期間,其它所有事務處理都被阻斷了,這種情況將對應用程序的擴展能力產生極大衝擊。

隨微軟Windows .NET發行的COM+ 1.5版允許有COM+目錄中按組件配置事務處理分離等級。與事務中根組件相關的設置決定了事務處理的分離等級。另外,同一事務流中的內部子組件擁有的事務處理等級必須不能高於要組件所定義的等級。如果不是這樣,當子組件實例化時,將導致錯誤。

對.NET管理類,Transaction屬性支持所有的公有Isolation屬性。你可以用此屬性陳述式地指定一特殊分離等級,如下面的代碼所示:

[Transaction(TransactionOption.Supported, Isolation=TransactionIsolationLevel.ReadCommitted)]
public class Account : ServicedComponent
{
  . . .
}

更多信息

關於配置事務處理分離等級及其它Windows .NET COM+增強特性的更多信息,見MSDN雜誌2001年8月期的“Windows XP:利用COM+ 1.5的增強特性使你的組件更強壯”一文。

確定事務處理結果

在單個事務流的所有事務處理組件上下文中,自動化事務處理結果由事務取消標誌和一致性標誌的狀態決定。當事務流中的根組件成爲非活動狀態(並且控制權返回調用者)時,確定事務處理結果。這種情況在圖5中得到了演示,此圖顯示的是一個典型的銀行基金傳送事務。

adonet5.gif
圖5 事務流上下文

當根對象(在本例中是對象)變爲非活動狀態,並且客戶的方法調用返回時,確定事務處理結果。在任何上下文中的任何一致性標誌被設爲假,或如果事務處理取消標誌設爲真,那麼底層的物理DTC事務將被取消。

可以以下面兩種方式之一從.NET對象中控制事務處理結果:

  • 可以用AutoComplete屬性對方法進行註釋,並讓.NET自動存放將決定事務處理結果投票。如果方法釋放異常,利用此屬性,一致性標誌自動地被設爲假(此值最終使事務取消)。如果方法返回而沒有釋放異常,那麼一致性標誌將設爲真,此值指出組件樂於執行事務。這並沒有得到保證,因爲它依賴於同一事務流中其它對象的投票。
  • 可以調用ContextUtil類的靜態方法SetComplete或 SetAbort,這些方法分別將一致性標誌設爲真或假。

嚴重性大於10的SQL Server錯誤將導致管理數據供應器釋放SqlException類型的異常。如果方法緩存並處理異常,就要確保或者通過手工取消了事務,或者方法被標記了[AutoComplete],以保證異常能傳遞迴調用者。

AutoComplete方法

對於標記了屬性的方法,執行下面操作:

  • 將SqlException傳遞加調用堆棧。
  • 將SqlException封裝在外部例外中,並傳遞迴調用者。也可以將異常封裝在對調用者更有意義的異常類型中。

異常如果不能傳遞,將導致對象不會提出取消事務,從而忽視數據庫錯誤。這意味着共享同一事務流的其它對象的成功操作將被提交。

下面的代碼緩存了SqlException,然後將它直接傳遞迴調用者。事務處理最終將被取消,因爲對象的一致性標誌在對象變爲非活動狀態時自動被設爲假。

[AutoComplete]
void SomeMethod()
{
  try
  {
    // Open the connection, and perform database operation
    . . .
  }
  catch (SqlException sqlex )
  {
    LogException( sqlex ); // Log the exception details
    throw;                 // Rethrow the exception, causing the consistent 
                           // flag to be set to false.
  }
  finally
  {
    // Close the database connection
    . . .
  }
}

Non-AutoComlete方法

對於沒有AutoComplete的屬性的方法,必須:

  • 在catch塊內調用ContextUtil.SetAbort以終止事務處理。這就將相容標誌設置爲假。
  • 如果沒有發生異常事件,調用ContextUtil.SetComplete,以提交事務,這就將相容標誌設置爲真(缺省狀態)。

代碼說明了這種方法。

void SomeOtherMethod()
{
  try
  {
    // Open the connection, and perform database operation
    . . .
    ContextUtil.SetComplete(); // Manually vote to commit the transaction
  }
  catch (SqlException sqlex)
  {
    LogException( sqlex );   // Log the exception details
    ContextUtil.SetAbort();  // Manually vote to abort the transaction
    // Exception is handled at this point and is not propagated to the caller
  }
  finally
  {
    // Close the database connection
    . . .
  }
}

注意 如果有多個catch塊,在方法開始的時候調用ContextVtil.SetAbort,以及在try塊的末尾調用ContextUtil.SetComplete都會變得容易。用這種方法,就不需要在每個catch塊中重複調用ContextUtil.SetAbort。通過這種方法確定的相容標誌的設置只在方法返回時有效。

對於異常事件(或循環異常),必須把它傳遞到調用堆棧中,因爲這使得調用代碼認爲事務處理失敗。它允許調用代碼做出優化選擇。比如,在銀行資金轉賬中,如果債務操作失敗,則轉帳分支可以決定不執行債務操作。

如果把相容標誌設置爲假並且在返回時沒有出現異常事件,則調用代碼就沒有辦法知道事務處理是否一定失敗。雖然可以返回Boolean值或設置Boolean輸出參數,但還是應該前後一致,通過顯示異常事件以表明有錯誤發生。這樣代碼就有一種標準的錯誤處理方法,因此更簡明、更具有相容性。

數據分頁

在分佈式應用程序中利用數據進行分頁是一項普遍的要求。比如,用戶可能得到書的列表而該列表又不能夠一次完全顯示,用戶就需要在數據上執行一些熟悉的操作,比如瀏覽下一頁或上一頁的數據,或者跳到列表的第一頁或最後一頁。

這部分內容將討論實現這種功能的選項,以及每種選項在性能和縮放性上的效果。

選項比較

數據分頁的選項有:

  • 利用SqlDataAdapter的Fill方法,將來自查詢處的結果填充到DataSet中。
  • 通過COM的可相互操作性使用ADO,並利用服務器光標。
  • 利用存儲的過程手工實現數據分頁。

對數據進行分頁的最優選項依賴於下列因素:

  • 擴展性要求
  • 性能要求
  • 網絡帶寬
  • 數據庫服務器的存儲器和功率
  • 中級服務器的存儲器和功率
  • 由分頁查詢所返回的行數
  • 數據總頁數的大小

性能測試表明利用存儲過程的手工方法在很大的應力水平範圍上都提供了最佳性能。然而,由於手工方法在服務器上執行工作,如果大部分站點功能都依賴數據分頁功能,那麼服務器性能就會成一個關鍵要素。爲確保這種方法能適合特殊環境,應該測試各種特殊要求的選項。

下面將討論各種不同的選項。

使用SqlDataAdapter

如前面所討論的,SqlDataAdapter是用來把來自數據庫的數據填充到DataSet中,過載的Fill方法中的任一個都需要兩個整數索引值(如下列代碼所示):

public int Fill(
   DataSet dataSet,
   int startRecord,
   int maxRecords,
   string srcTable
);

StartRecord值標示從零開始的記錄起始索引值。MaxRecord值表示從startRecord開始的記錄數,並將拷貝到新的DataSet中。

SqlDataAdapter在內部利用SqlDataReader執行查詢並返回結果。SqlDataAdapter讀取結果並創建基於來自SalDataReader的數據的Dataset。SqlDataAdapter通過startRecord和maxRecords把所有結果都拷貝到新生成的DataSet中,並丟棄不需要的數據。這意味着許多不必要的數據將潛在的通過網絡進入數據訪問客戶--這是這種方法的主要缺陷。

比如,如果有1000個記錄,而需要的是第900到950個記錄,那麼前面的899個記錄將仍然穿越網絡然後被丟棄。對於小數量的記錄,這種開銷可能是比較小的,但如果針對大量數據的分頁,則這種開銷就會非常巨大。

使用ADO

實現分頁的另一個選項是利用基於COM的ADO進行分頁。這種方法的目標是獲得訪問服務器光標。服務器光標通過ADO Recordset對象顯示。可以把Recordset光標的位置設置到adUseServer中。如果你的OLE DB供應器支持這種設置(如SQLOLEDB那樣),就可以使用服務器光標。這樣就可以利用光標直接導航到起始記錄,而不需要將所有數據傳過網絡進入訪問數據的用戶代碼中。

這種方法有下面兩個缺點:

  • 在大多數情況下,可能需要將返回到Recordset對象中的記錄翻譯成DataSet中的內容,以便在客戶管理的代碼中使用。雖然OleDbDataAdapter確實在獲取ADO Recordset對象並把它翻譯成Dataset時過載了Fill方法,但是並沒有利用特殊記錄進行開始與結束操作的功能。唯一現實的選項是把開始記錄移動到Recordset對象中,循環每個記錄,然後手工拷貝數據到手工生成的新Dataset中。這種操作,尤其是利用COM Interop調用,其優點可能不僅僅是不需要在網絡上傳輸多餘的數據,尤其對於小的DataSet更明顯。
  • 從服務器輸出所需數據時,將保持連接和服務器光標開放。在數據庫服務器上,光標的開放與維護需要昂貴的資源。雖然該選項提高了性能,但是由於爲延長的時間兩消耗服務器資源,從而也有可能降低可擴展性。

提供手工實現

在本部分中討論的數據分頁的最後一個選項是利用存儲過程手工實現應用程序的分頁功能。對於包含唯一關鍵字的表格,實現存儲過程相對容易一些。而對於沒有唯一關鍵字的表格(也不應該有許多關鍵字),該過程會相對複雜一些。

帶有唯一關鍵字的表格的分頁

如果表格包含一個唯一關鍵字,就可以利用WHERE條款中的關鍵字創建從某個特殊行起始的結果設置。這種方法,與用來限制結果設置大小的SET ROWCOUNT狀態是相匹配的,提供了一種有效的分頁原理。這一方法將在下面存儲的代碼中說明:

CREATE PROCEDURE GetProductsPaged
@lastProductID int,
@pageSize int
AS
SET ROWCOUNT @pageSize
SELECT *
FROM Products
WHERE [standard search criteria]
AND ProductID > @lastProductID
ORDER BY [Criteria that leaves ProductID monotonically increasing]
GO

這個存儲過程的調用程序僅僅維護LastProductID的值,並通過所選的連續調用之間的頁的大小增加或減小該值。

不帶有唯一關鍵字的表格的分頁

如果需要分頁的表格沒有唯一關鍵字,可以考慮添加一個--比如利用標識欄。這樣就可以實現上面討論的分頁方案了。

只要能夠通過結合結果記錄中的兩個或更多區域來產生唯一性,就仍然有可能實現無唯一關鍵字表格的有效分頁方案。

比如,考察下列表格:

Col1 Col2 Col3 Other columns…
A 1 W
A 1 X   .
A 1 Y   .
A 1 Z   .
A 2 W   .
A 2 X   .
B 1 W
B 1 X   .

對於該表,結合Col 、Col2 和Col3就可能產生一種唯一性。這樣,就可以利用下面存儲過程中的方法實現分佈原理:

CREATE PROCEDURE RetrieveDataPaged
@lastKey char(40),
@pageSize int
AS
SET ROWCOUNT @pageSize
SELECT
Col1, Col2, Col3, Col4, Col1+Col2+Col3 As KeyField
FROM SampleTable
WHERE [Standard search criteria]
AND Col1+Col2+Col3 > @lastKey
ORDER BY Col1 ASC, Col2 ASC, Col3 ASC
GO

客戶保持存儲過程返回的keyField欄的最後值,然後又插入回到存儲過程中以控制表的分頁。

雖然手工實現增加了數據庫服務器上的應變,但它避免了在網絡上傳輸不必要的數據。性能測試表明在整個應變水平中這種方法都工作良好。然而,根據站點工作所涉及的數據分頁功能的多少,在服務器上進行手工分頁可能影響應用程序的可擴展性。應該在所在環境中運行性能測試,爲應用程序找到最合適的方法。

附錄

如何爲一個.NET類啓用對象結構

要利用Enterprise (COM+)Services爲對象結構啓用.NET管理的類,需要執行下列步驟:

  • 從位於System. Enterprise Services名字空間中的Serviced Component中導出所需類。
    using System.EnterpriseServices;
    public class DataAccessComponent : ServicedComponent
  • 爲該類添加Construction Enabled屬性,併合理地指定缺省結構字符串,該缺省值保存在COM+目錄中,管理員可以利用組件服務微軟管理控制檯(MNC)的snap-in來維護該缺省值。
    [ConstructionEnabled(Default="default DSN")]
    public class DataAccessComponent : ServicedComponent
  • 提供虛擬Construct方法的替換實現方案。該方法在對象語言構造程序之後調用。在COM目錄中保存的結構字符串是該方法的唯一字符串。
    public override void Construct( string constructString )
    {
      // Construct method is called next after constructor.
      // The configured DSN is supplied as the single argument
    }
  • 通過Assembly key文件或Assembly key Name屬性爲該彙編提供一個強名字。任何用COM+服務註冊的彙編必須有一個強名字。關於帶有強名字彙編的更多信息,參考:http://msdn.microsoft.com/library/en-us/cpguide/html/cpconworkingwithstrongly- namedassemblies.Asp。
    [assembly: AssemblyKeyFile("DataServices.snk")]
  • 爲支持動態註冊,可以利用匯編層上的屬性ApplicationName和Application Action分別指定用於保持彙編元素和應用程序動作類型的COM+應用程序的名字。關於彙編註冊的更多信息,參考: http://msdn.microsoft.com/library/en-us/cpguide/html/cpconregisteringserviced components.asp
// the ApplicationName attribute specifies the name of the
// COM+ Application which will hold assembly components
[assembly : ApplicationName("DataServices")]
   
// the ApplicationActivation.ActivationOption attribute specifies 
// where assembly components are loaded on activation
// Library : components run in the creator's process
// Server : components run in a system process, dllhost.exe
[assembly: ApplicationActivation(ActivationOption.Library)]

下列代碼段是一個叫做DataAccessComponent的服務組件,它利用COM+結構字符串來獲得數據庫連接字符串。

using System;
using System.EnterpriseServices;

// the ApplicationName attribute specifies the name of the
// COM+ Application which will hold assembly components
[assembly : ApplicationName("DataServices")]

// the ApplicationActivation.ActivationOption attribute specifies 
// where assembly components are loaded on activation
// Library : components run in the creator's process
// Server : components run in a system process, dllhost.exe
[assembly: ApplicationActivation(ActivationOption.Library)]

// Sign the assembly. The snk key file is created using the 
// sn.exe utility
[assembly: AssemblyKeyFile("DataServices.snk")]

[ConstructionEnabled(Default="Default DSN")]
public class DataAccessComponent : ServicedComponent
{
    private string connectionString;
    public DataAccessComponent()
    {
      // constructor is called on instance creation
    }
    public override void Construct( string constructString )
    {
      // Construct method is called next after constructor.
      // The configured DSN is supplied as the single argument
      this.connectionString = constructString;
    }
}

如何利用SqlDataAdapter來檢索多個行

下面的代碼說明如何利用SqlDataAdapter對象發出一個生成Data Set或Datatable的命令。它從SQL Server Northwind數據庫中檢索一系列產品目錄。

using System.Data;
using System.Data.SqlClient;

public DataTable RetrieveRowsWithDataTable()
{
  using ( SqlConnection conn = new SqlConnection(connectionString) )
  {
    SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlDataAdapter da = new SqlDataAdapter( cmd );
    DataTable dt = new DataTable("Products");
    da.Fill(dt);
    return dt;
  }
}

按下列步驟利用SqlAdapter生成DataSet或DataTable:

  • 創建SqlCommand對象啓用存儲過程,並把它與SqlConnection對象(顯示的)或連接字符串(未顯示)相聯繫。
  • 創建一個新的SqlDataAdapter對象,並把它SqlCommand對象相聯繫。
  • 創建DataTable(或者DataSet)對象。利用構造程序自變量命名DataTable.
  • 調用SqlData Adapter對象的Fill方法,把檢索的行轉移到DataSet或Datatable中。

如何利用SqlDataReader檢索多個行

下列代碼說明了如何利用SqlDataReader方法檢索多行:

using System.IO;
using System.Data;
using System.Data.SqlClient;

public SqlDataReader RetrieveRowsWithDataReader()
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  try
  {
    conn.Open();
    // Generate the reader. CommandBehavior.CloseConnection causes the
    // the connection to be closed when the reader object is closed
    return( cmd.ExecuteReader( CommandBehavior.CloseConnection ) );
  }
  catch
  {
    conn.Close();
    throw;
  }
}

// Display the product list using the console
private void DisplayProducts()
{
  SqlDataReader reader = RetrieveRowsWithDataReader();
  while (reader.Read())
  {
    Console.WriteLine("{0} {1} {2}", 
                      reader.GetInt32(0).ToString(), 
                      reader.GetString(1) );
  }
  reader.Close(); // Also closes the connection due to the
                  // CommandBehavior enum used when generating the reader
}

按下列步驟利用SqlDataReader檢索多行:

  • 創建用於執行存儲的過程的SqlCommand對象,並把它與SqlConnection對象相聯繫。
  • 打開鏈接。
  • 通過調用SqlCommand對象的Excute Reader方法生成SqlDataReader對象。
  • 從流中讀取數據,調用SqlDataReader對象的Read方法來檢索行,並利用分類的存取程序方法(如GetIut 32和Get String方法)檢索列的值。
  • 完成讀取後,調用Close方法。

如何利用XmlReader檢索多個行

可以利用SqlCommand對象生成XmlReader對象,它提供對XML數據的基於流的前向訪問。該命令(通常是一個存儲的過程)必鬚生成一個基於XML的結果設置,它對於SQL Server2000通常是由帶有有效條款FOR XML的SELECT狀態組成。下列代碼段說明了這種方法:

public void RetrieveAndDisplayRowsWithXmlReader()
{
  SqlConnection conn = new SqlConnection(connectionString);
  SqlCommand cmd = new SqlCommand("DATRetrieveProductsXML", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  try
  {
    conn.Open();
    XmlTextReader xreader = (XmlTextReader)cmd.ExecuteXmlReader();
    while ( xreader.Read() )
    {
      if ( xreader.Name == "PRODUCTS" ) 
      {
        string strOutput = xreader.GetAttribute("ProductID");
        strOutput += " ";
        strOutput += xreader.GetAttribute("ProductName");
        Console.WriteLine( strOutput );
      }
    }
    xreader.Close();  
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

上述代碼使用了下列存儲過程:

CREATE PROCEDURE DATRetrieveProductsXML
AS
SELECT * FROM PRODUCTS 
FOR XML AUTO
GO

按下列步驟檢索XML數據:

  • 創建SqlCommand對象啓用生成XML結果設置的過程。(比如,利用SELECT狀態中的FOR XML條款)。把SqlCommand對象與一個鏈接相聯繫。
  • 調用SqlCommand對象的ExecuteXmlReader方法,並把結果分配給前向對象XmlTextReader。當不需要任何返回數據的基於XML的驗證時,這是應該使用的最快類型的XmlReader對象。
  • 利用XmlTextReader對象的Read方法讀取數據。

如何利用存儲過程輸出參數檢索單個行

可以調用一個存儲過程,它通過一種稱做輸出參數的方式可以在單個行中返回檢索數據項。下列代碼段利用存儲的過程檢索產品的名稱和單價,該產品包含在Northwind數據庫中。

void GetProductDetails( int ProductID, 
                        out string ProductName, out decimal UnitPrice )
{
  SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=Northwind");

  // Set up the command object used to execute the stored proc
  SqlCommand cmd = new SqlCommand( "DATGetProductDetailsSPOutput", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  // Establish stored proc parameters.
  //  @ProductID int INPUT
  //  @ProductName nvarchar(40) OUTPUT
  //  @UnitPrice money OUTPUT

  // Must explicitly set the direction of output parameters
  SqlParameter paramProdID = 
         cmd.Parameters.Add( "@ProductID", ProductID );
  paramProdID.Direction = ParameterDirection.Input;
  SqlParameter paramProdName = 
         cmd.Parameters.Add( "@ProductName", SqlDbType.VarChar, 40 );
  paramProdName.Direction = ParameterDirection.Output;
  SqlParameter paramUnitPrice = 
         cmd.Parameters.Add( "@UnitPrice", SqlDbType.Money );
  paramUnitPrice.Direction = ParameterDirection.Output;
  try
  {
    conn.Open();
    // Use ExecuteNonQuery to run the command. 
    // Although no rows are returned any mapped output parameters 
    // (and potentially return values) are populated
    cmd.ExecuteNonQuery( );
    // Return output parameters from stored proc
    ProductName = paramProdName.Value.ToString();
    UnitPrice = (decimal)paramUnitPrice.Value;
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

按下列步驟利用存儲的過程輸出參數檢索單個行:

  • 創建一個SqlCommand對象,並把它與SqlConnection對象相聯繫。
  • 通過調用SqlCommand’s Parameters集合的Add方法設置存儲過程參數。缺省情況下,參數假定爲輸出參數,所以必須明確設置任何輸出參數的方向。

注意 明確設置所有參數的方向是一次很好的練習,包括輸入參數。

  • 打開連接。
  • 調用Sqlcommand對象的ExecuteNonQuery方法。它在輸出參數(並潛在地帶有一個返回值)中。
  • 利用Value屬性從合適的SqlParameter對象中檢索輸出參數。
  • 關閉連接。

上述代碼段啓用了下列存儲過程。

CREATE PROCEDURE DATGetProductDetailsSPOutput
@ProductID int,
@ProductName nvarchar(40) OUTPUT,
@UnitPrice money OUTPUT
AS
SELECT @ProductName = ProductName, 
       @UnitPrice = UnitPrice 
FROM Products 
WHERE ProductID = @ProductID
GO

如何利用SqlDataReader檢索單個行

可以利用SqlDataReader對象檢索單個行,以及來自返回數據流的所需欄的值。這由下列代碼說明:

void GetProductDetailsUsingReader( int ProductID, 
                        out string ProductName, out decimal UnitPrice )
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=Northwind");

  // Set up the command object used to execute the stored proc
  SqlCommand cmd = new SqlCommand( "DATGetProductDetailsReader", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  // Establish stored proc parameters.
  //  @ProductID int INPUT

  SqlParameter paramProdID = cmd.Parameters.Add( "@ProductID", ProductID );
  paramProdID.Direction = ParameterDirection.Input;
  try
  {
    conn.Open();
    SqlDataReader reader = cmd.ExecuteReader();
    reader.Read(); // Advance to the one and only row

    // Return output parameters from returned data stream
    ProductName = reader.GetString(0);
    UnitPrice = reader.GetDecimal(1);
    reader.Close();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

按下列步驟返回帶有SqlDataReader對象:

  • 建立SqlCommand對象。
  • 打開連接。
  • 調用SqlDReader對象的ExecuteReader對象。
  • 利用SqlDataReader對象的分類的存取程序方法檢索輸出參數--在這裏是GetString和GetDecimal.

上述代碼段啓用了下列存儲過程:

CREATE PROCEDURE DATGetProductDetailsReader
@ProductID int
AS
SELECT ProductName, UnitPrice FROM Products
WHERE ProductID = @ProductID
GO

如何利用ExecuteScalar單個項

ExecuteScalar方法是設計成用於返回單個值的訪問。在返回多列或多行的訪問事件中,ExecuteScalar只返回第一行的第一例。

下列代碼說明如何查詢某個產品ID的產品名稱:

void GetProductNameExecuteScalar( int ProductID, out string ProductName )
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  cmd.Parameters.Add("@ProductID", ProductID );
  try
  {
    conn.Open();
    ProductName = (string)cmd.ExecuteScalar();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

按下列步驟利用Execute Scalar檢索單個項:

  • 建立調用存儲過程的SqlCommand對象。
  • 打開鏈接。
  • 調用ExecuteScalar方法,注意該方法返回對象類型。它包含檢索的第一列的值,並且必須設計成合適的類型。
  • 關閉鏈接。

上述代碼啓用了下列存儲過程:

CREATE PROCEDURE LookupProductNameScalar
@ProductID int
AS
SELECT TOP 1 ProductName
FROM Products
WHERE ProductID = @ProductID
GO

如何利用存儲過程輸出或返回的參數檢索單個項

利用存儲過程輸出或返回的參數可以查詢單個值,下列代碼說明了輸出參數的使用:

void GetProductNameUsingSPOutput( int ProductID, out string ProductName )
{
  SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("LookupProductNameSPOutput", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  SqlParameter paramProdID = cmd.Parameters.Add("@ProductID", ProductID );
  ParamProdID.Direction = ParameterDirection.Input;
  SqlParameter paramPN = 
         cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );
  paramPN.Direction = ParameterDirection.Output;
  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
    ProductName = paramPN.Value.ToString();  
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

按下列步驟利用存儲過程的輸出參數檢索單個值:

  • 創建調用存儲過程的SqlCommand對象。
  • 通過把SqlParmeters添加到SqlCommand’s Parameters集合中設置任何輸入參數和單個輸出參數。
  • 打開鏈接。
  • 調用SqlCommand對象的Execute NonQuery方法。
  • 關閉鏈接。
  • 利用輸出SqlParameter的Value屬性檢索輸出值。

上述代碼使用了下列存儲過程:

CREATE PROCEDURE LookupProductNameSPOutput 
@ProductID int,
@ProductName nvarchar(40) OUTPUT
AS
SELECT @ProductName = ProductName
FROM Products
WHERE ProductID = @ProductID
GO

下列代碼說明如何利用返回值確定是否存在特殊行。從編碼的角度看,這與使用存儲過程輸出參數相類似,除了需要明確設置到ParameterDirection.ReturnValue的SqlParameter方向。

bool CheckProduct( int ProductID )
{
  SqlConnection conn = new SqlConnection(
       "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("CheckProductSP", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  cmd.Parameters.Add("@ProductID", ProductID );
  SqlParameter paramRet = 
         cmd.Parameters.Add("@ProductExists", SqlDbType.Int );
  paramRet.Direction = ParameterDirection.ReturnValue;
  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
  return (int)paramRet.Value == 1;
}

按下列步驟,可以利用存儲過程返回值檢查是否存在特殊行:

  • 建立調用存儲過程的SqlCommand對象。
  • 設置包含需要訪問的行的主要關鍵字的輸入參數。
  • 設置單個返回值參數。把SqlParameter對象添加到SqlCommand’s Parameter集合中,並設置它到ParameterDireetion.ReturnValue的方面。
  • 打開鏈接。
  • 調用SqlCommand對象的ExecuteNonQuery的方法.
  • 關閉鏈接。
  • 利用返回值SqlParameter的Value屬性檢索返回值。

上述代碼使用了下列存儲過程:

CREATE PROCEDURE CheckProductSP 
@ProductID int
AS
IF EXISTS( SELECT ProductID
           FROM Products
           WHERE ProductID = @ProductID )
  return 1
ELSE
  return 0
GO

如何利用SqlDataReader檢索單個項。

通過調用命令對象的ExecuteReader方法,可以利用SqlDataReader對象獲得單個輸出值。這需要稍微多一些的代碼,因爲SqlDataReader Read方法必須調用,然後所需值通過讀者存取程序方法得到檢索。SqlDataReader對象的使用在下列代碼中說明:

bool CheckProductWithReader( int ProductID )
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("CheckProductExistsWithCount", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  cmd.Parameters.Add("@ProductID", ProductID );
  cmd.Parameters["@ProductID"].Direction = ParameterDirection.Input;
  try
  {
    conn.Open();
    SqlDataReader reader = cmd.ExecuteReader(
                                CommandBehavior.SingleResult );
    reader.Read();

    bool bRecordExists = reader.GetInt32(0) > 0;
    reader.Close();
    return bRecordExists;
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close(); 
  }

}

上述代碼使用了下列存儲過程:

CREATE PROCEDURE CheckProductExistsWithCount 
@ProductID int
AS
SELECT COUNT(*) FROM Products
WHERE ProductID = @ProductID
GO

如何編碼ADO.NET手工事務

下列代碼說明如何利用SQL Server. NET數據供應器提供的事務支持來保護事務的支金轉帳操作。該操作在位於同一數據庫中的兩個帳戶之間轉移支金。

public void TransferMoney( string toAccount, string fromAccount, decimal amount )
{
  using ( SqlConnection conn = new SqlConnection(
            "server=(local);Integrated Security=SSPI;database=SimpleBank" ) )
  {
    SqlCommand cmdCredit = new SqlCommand("Credit", conn );
    cmdCredit.CommandType = CommandType.StoredProcedure;
    cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) );
    cmdCredit.Parameters.Add( new SqlParameter("@Amount", amount ));

    SqlCommand cmdDebit = new SqlCommand("Debit", conn );
    cmdDebit.CommandType = CommandType.StoredProcedure;
    cmdDebit.Parameters.Add( new SqlParameter("@AccountNo", fromAccount) );
    cmdDebit.Parameters.Add( new SqlParameter("@Amount", amount ));

    conn.Open();
    // Start a new transaction
    using ( SqlTransaction trans = conn.BeginTransaction() )
    {
      // Associate the two command objects with the same transaction
      cmdCredit.Transaction = trans;
      cmdDebit.Transaction = trans;
      try
      {
        cmdCredit.ExecuteNonQuery();
        cmdDebit.ExecuteNonQuery();
        // Both commands (credit and debit) were successful
        trans.Commit();
      }
      catch( Exception ex )
      {
        // transaction failed
        trans.Rollback();
        // log exception details . . .
        throw ex;
      }
    }
  }
}

如何利用Transact-SQL執行事務

下列存儲過程說明了如何在Transact-SQL過程內執行事務的支金轉移操作。

CREATE PROCEDURE MoneyTransfer
@FromAccount char(20),
@ToAccount char(20),
@Amount money
AS
BEGIN TRANSACTION
-- PERFORM DEBIT OPERATION
UPDATE Accounts
SET Balance = Balance - @Amount
WHERE AccountNumber = @FromAccount
IF @@RowCount = 0
BEGIN
  RAISERROR('Invalid From Account Number', 11, 1)
  GOTO ABORT
END
DECLARE @Balance money
SELECT @Balance = Balance FROM ACCOUNTS
WHERE AccountNumber = @FromAccount
IF @BALANCE < 0
BEGIN
  RAISERROR('Insufficient funds', 11, 1)
  GOTO ABORT
END
-- PERFORM CREDIT OPERATION
UPDATE Accounts 
SET Balance = Balance + @Amount 
WHERE AccountNumber = @ToAccount
IF @@RowCount = 0
BEGIN
  RAISERROR('Invalid To Account Number', 11, 1)
  GOTO ABORT
END
COMMIT TRANSACTION
RETURN 0
ABORT:
  ROLLBACK TRANSACTION
GO

該存儲過程使用BEGIN TRANSACTION, COMMIT TRANSACTION,和ROLLBACK TRANSACTION狀態手工控制事務。

如何編碼事務性的.NET類

下述例子是三種服務性的NET類,它們配置或用於自動事務。每個類都帶有Transaction屬性,它的值將決定是否啓動新事務流或者對象是否共享即時調用程序的數據流。這些元素一起工作來執行銀行支金轉移。Transfer類配置有RequiresNew事務屬性,而Debit和Credit類配置有Required屬性。這樣,在運行的時候三個對象共享同一個事務。

using System;
using System.EnterpriseServices;

[Transaction(TransactionOption.RequiresNew)]
public class Transfer : ServicedComponent
{
  [AutoComplete]
  public void Transfer( string toAccount, 
                        string fromAccount, decimal amount )
  {
    try
    {
      // Perform the debit operation
      Debit debit = new Debit();
      debit.DebitAccount( fromAccount, amount );
      // Perform the credit operation
      Credit credit = new Credit();
      credit.CreditAccount( toAccount, amount );
    }
    catch( SqlException sqlex )
    {
      // Handle and log exception details
      // Wrap and propagate the exception
      throw new TransferException( "Transfer Failure", sqlex );    
    }
  }
}
[Transaction(TransactionOption.Required)]
public class Credit : ServicedComponent
{
  [AutoComplete]
  public void CreditAccount( string account, decimal amount )
  {
    SqlConnection conn = new SqlConnection(
            "Server=(local); Integrated Security=SSPI"; database="SimpleBank");
    SqlCommand cmd = new SqlCommand("Credit", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );
    cmd.Parameters.Add( new SqlParameter("@Amount", amount ));
    try
    {
      conn.Open();
      cmd.ExecuteNonQuery();
    }
    catch (SqlException sqlex)
    {
      // Log exception details here
      throw; // Propagate exception
    }
  }
}
[Transaction(TransactionOption.Required)]
public class Debit : ServicedComponent
{
  public void DebitAccount( string account, decimal amount )
  {
    SqlConnection conn = new SqlConnection(
            "Server=(local); Integrated Security=SSPI"; database="SimpleBank");
    SqlCommand cmd = new SqlCommand("Debit", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );
    cmd.Parameters.Add( new SqlParameter("@Amount", amount ));
    try
    {
      conn.Open();
      cmd.ExecuteNonQuery();
    }
    catch (SqlException sqlex)
    {
      // Log exception details here
      throw; // Propagate exception back to caller
    }
  }
}

合作者

非常感謝下列撰稿者和審校者:

Bill Vaughn, Mike Pizzo, Doug Rothaus, Kevin White, Blaine Dokter, David Schleifer, Graeme Malcolm(內容專家), Bernard Chen(西班牙人), Matt Drucke(協調)和Steve kirk.

讀者有什麼樣的問題、評論和建議?關於本文的反饋信息,請發E-mail至devfdbck®microsoft.com。

你希望學習並利用.NET的強大功能嗎?與微軟技術中心的技術專家一起工作,學習開發最佳方案。詳細信息請訪問: http://www.micrsoft.com/business/services/mtc.asp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章