C# 6 與 .NET Core 1.0 高級編程 - 37 章 ADO.NET


譯文,個人原創,轉載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 37 章 ADO.NET),不對的地方歡迎指出與交流。 

英文原文:Professional C# 6 and .NET Core 1.0 - 37 ADO.NET

-------------------------------

本章內容

  • 連接數據庫
  • 執行命令
  • 調用存儲過程
  • ADO.NET對象模型

Wrox.com 網站關於本章的源代碼下載

wrox.com中本章源代碼下載位於“Download Code”選項卡www.wrox.com/go/professionalcsharp6。本章分為以下幾個主要例子:

  • ConnectionSamples
  • CommandSamples
  • AsyncSamples
  • TransactionSamples 

ADO.NET概述 

本章討論如何使用ADO.NET從C#程序訪問關系數據庫,如SQL Server。它顯示與數據庫的連接與關閉,以及如何使用查詢,添加和更新記錄。您將學習各種命令對象選項,並了解如何使用SQL Server程序類提供的命令中的選項如何使用;如何用命令對象調用存儲過程;以及如何使用事務。
早期版本的ADO.NET提供了不同的數據庫提供程序:SQL Server的提供程序和一個用於Oracle的提供程序,OLEDB和ODBC。 OLEDB技術已停產,因此新的應用程序不提倡使用該提供程序。訪問Oracle數據庫,Microsoft的提供程序也停止了,因為來自Oracle的提供程序(http://www.oracle.com/technetwork/topics/dotnet/)
更適合需求。對於其他數據源(也適用於Oracle),許多第三方提供程序都可用。在使用ODBC提供程序之前,應該使用特定的訪問數據源的提供程序。本章中的代碼示例基於SQL Server,但是您可以輕松地將其更改為使用不同的連接和命令對象,例如在訪問Oracle數據庫使用OracleConnection和OracleCommand,而不是SqlConnection和SqlCommand。

注意 本章不討論DataSet在內存中包含表。數據集雖然允許從數據庫檢索記錄,並將內容存儲在具有關系的內存數據表中。但我們應該使用Entity Framework,它在第38章“Entity Framework Core”中討論。Entity Framework能夠擁有對象關系而不是基於表的關系。

示例數據庫 

本章中的示例使用AdventureWorks2014數據庫,可以從https://msftdbprodsamples.codeplex.com/下載此數據庫。鏈接可以下載一個zip文件中的AdventureWorks2014數據庫的備份。選擇推薦的下載 - Adventure Works 2014 Full Database Backup.zip。解壓縮文件后,可以使用SQL Server Management Studio恢復數據庫備份,如圖37.1所示。如果您的系統上沒有SQL Server Management Studio,可以從http://www.microsoft.com/downloads下載免費版本。

 

圖 37.1  

本章使用的SQL服務器是SQL Server LocalDb。這是作為Visual Studio的一部分安裝的數據庫服務器。您也可以使用其他任意的SQL Server版本;只需要相應地改變連接字符串。

NuGet包和命名空間

所有ADO.NET示例的代碼使用以下依賴項和命名空間:

依賴項

NETStandard.Library
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
System.Data.SqlClient 

 命名空間

Microsoft.Extensions.Configuration
System
System.Data
System.Data.SqlClient
System.Threading.Tasks
static System.Console 

 使用數據庫連接 

訪問數據庫需要提供連接參數,例如運行數據庫的計算機以及可能的登錄憑據。可以使用SqlConnection類創建SQL Server的連接。
以下代碼段說明如何創建、打開和關閉AdventureWorks2014數據庫的連接(代碼文件ConnectionSamples / Program.cs):

public static void OpenConnection()
{
  string connectionString = @"server=(localdb)\MSSQLLocalDB;" +
                  "integrated security=SSPI;" +
                  "database=AdventureWorks2014";
  var connection = new SqlConnection(connectionString);
  connection.Open(); 
  // Do something useful
  WriteLine("Connection opened"); 
  connection.Close();
}

注意 除了Close方法外,SqlConnection類還使用Dispose方法實現IDisposable接口。兩者同樣可以釋放連接。有了這個,你可以使用using語句關閉連接。 

在示例連接字符串中,使用的參數如下(參數由連接字符串中的分號分隔):

  • server =(localdb)\ MSSQLLocalDB - 這表示要連接到的數據庫服務器。 SQL Server允許許多單獨的數據庫服務器實例在同一台機器上運行。表示您正在連接到localdb的服務器,MSSQLLocalDB是通過安裝SQL Server創建的SQL Server實例。如果使用SQL Server的本地安裝,請將此部件更改為server =(local)。連接到SQL Azure,使用關鍵字Data Source而不是 server,可以設置Data Source = servername.database.windows.net。
  • database = AdventureWorks2014 - 描述要連接的數據庫實例。每個SQL Server進程可以公開幾個數據庫實例,使用關鍵字 Initial Catalog,不是database。
  • 集成安全性= SSPI - 這里使用Windows身份驗證連接到數據庫。另外如果要用SQL Azure,需要設置 User Id 和Password。

注意 有關許多不同數據庫的連接字符串的詳細信息,請訪問http://www.connectionstrings.com

ConnectionSamples示例使用定義的連接字符串打開數據庫連接,然后關閉該連接。打開連接后,可以對數據源發出命令;完成后,可以關閉連接。

管理連接字符串

最好從配置文件中讀取連接字符串,不要用C#代碼硬編碼。對於.NET 4.6和.NET Core 1.0,配置文件可以是JSON或XML格式,也可以從環境變量讀取。以下示例從JSON配置文件中讀取連接字符串(代碼文件ConnectionSamples / config.json):

{
  "Data": {
    "DefaultConnection": {
      "ConnectionString":
        "Server=(localdb)\\MSSQLLocalDB;Database=AdventureWorks2014;Trusted_Connection=True;"
    }
  }
} 

可以使用NuGet包的Microsoft.Framework.Configuration中定義的配置API讀取JSON文件。要使用JSON配置文件,需要添加 NuGet包Microsoft.Framework.Configuration.Json。創建ConfigurationBuilder讀取配置文件。 AddJsonFile 擴展方法添加JSON文件config.json以讀取來自此文件的配置信息(如果它與程序在同一路徑中)。要配置不同的路徑,可以調用方法SetBasePath。調用ConfigurationBuilder的Build方法從所有添加的配置文件構建配置並返回實現Iconfiguration接口的對象。這樣,可以檢索配置值,例如Data的配置值:DefaultConnection:ConnectionString(代碼文件ConnectionSamples / Program.cs):

public static void ConnectionUsingConfig()
{
  var configurationBuilder = new ConfigurationBuilder().AddJsonFile("config.json");
  IConfiguration config = configurationBuilder.Build();
  string connectionString = config["Data:DefaultConnection:ConnectionString"];
  WriteLine(connectionString);
}

連接池

幾年前完成的兩層應用程序中,最好是在應用程序啟動時打開連接,並在應用程序關閉時關閉它。但現在這不是一個好主意。這個程序架構的原因是它需要一些時間來打開一個連接。現在,關閉連接不會關閉與服務器的連接。相反,連接將會被添加到一個連接池。當再次打開連接時,可以從池中取出,因此打開連接非常快;它僅在打開第一個連接時需要時間。
連接池可以使用連接字符串中的多個選項來配置。將選項Pooling設置為false將禁用連接池;默認情況下是啟用的-Pooling = true。屬性 Min Pool Size和Max Pool Size 能夠配置池中的連接數。默認情況下,“Min Pool Size”的值為0,“Max Pool Size”的值為100。Connection Lifetime 定義連接在真正釋放之前應在池中保持不活動狀態的時間。

連接信息

創建連接后,可以注冊事件處理程序以獲取有關連接的一些信息。 SqlConnection類定義了InfoMessage和StateChange事件。每次從SQL Server返回信息或警告消息時,將觸發InfoMessage事件。當連接的狀態更改(例如連接已打開或關閉)時會觸發StateChange事件(代碼文件ConnectionSamples / Program.cs): 

public static void ConnectionInformation()
{
  using (var connection = new SqlConnection(GetConnectionString()))
  {
    connection.InfoMessage += (sender, e) =>
    {
      WriteLine($"warning or info {e.Message}");
    };
    connection.StateChange += (sender, e) =>
    {
      WriteLine($"current state: {e.CurrentState}, before: {e.OriginalState}");
    };
    connection.Open();
    WriteLine("connection opened");
    // Do something useful
  }
}

 運行應用程序,可以看到StateChange事件觸發,已打開狀態和已關閉狀態:

current state: Open, before: Closed
connection opened
current state: Closed, before: Open 

 命令

“使用數據庫連接”一節簡要介紹了針對數據庫發出命令的思路。命令是最簡單的形式,是一個包含要發出到數據庫的SQL語句的文本字符串。命令也可以是存儲過程,稍后在本節中顯示。
可以通過將SQL語句作為參數傳遞到Command類的構造函數來構造命令,如本示例所示(代碼文件CommandSamples / Program.cs):

public static void CreateCommand()
{
  using (var connection = new SqlConnection(GetConnectionString()))
  {
    string sql ="SELECT BusinessEntityID, FirstName, MiddleName, LastName" +      "FROM Person.Person";
var command = new SqlCommand(sql, connection);
connection.Open();
    // etc.
  }
}

 還可以通過調用SqlConnection的CreateCommand方法將SQL語句分配給CommandText屬性來創建命令:

SqlCommand command = connection.CreateCommand();
command.CommandText = sql;

命令經常需要參數。例如,以下SQL語句需要EmailPromotion參數。不要使用字符串連接來建立參數。相反,請使用ADO.NET的參數功能:

string sql ="SELECT BusinessEntityID, FirstName, MiddleName, LastName" +     "FROM Person.Person WHERE EmailPromotion = @EmailPromotion";
var command = new SqlCommand(sql, connection);

有一個簡單的方法將參數添加到SqlCommand對象,那就是使用Parameters屬性返回SqlParameterCollection和使用AddWithValue方法:

 command.Parameters.AddWithValue("EmailPromotion", 1);

更有效的方法是通過傳遞名稱和SQL數據類型來使用Add方法的重載:

command.Parameters.Add("EmailPromotion", SqlDbType.Int);
command.Parameters["EmailPromotion"].Value = 1;

當然也可以創建一個SqlParameter對象,並將其添加到SqlParameterCollection。

注意 不要傾向於使用SQL參數的字符串連接,因為它通常被濫用於SQL注入攻擊。 使用SqlParameter對象可以禁止這種攻擊。

定義命令后,需要執行該命令。有幾種方法來發布語句,這取決於什么,如果有什么,你期望從該命令返回。 SqlCommand類提供了以下ExecuteXX方法:

  • ExecuteNonQuery—執行命令,但不返回任何輸出
  • ExecuteReader—執行命令並返回一個類型化的IDataReader
  • ExecuteScalar—執行命令,並從任何結果集的第一行的第一列返回值

 ExecuteNonQuery 

ExecuteNonQuery方法通常用於UPDATE,INSERT或DELETE語句,其中唯一的返回值是受影響的記錄數。但是,如果調用具有輸出參數的存儲過程,則此方法可能返回結果。示例代碼在Sales.SalesTerritory表中創建一條新記錄。此表的主鍵TerritoryID是標識列,因此不需要提供此屬性來創建記錄。此表的所有列都不允許為空(請參見圖37.2),但其中某些字段有默認值,例如 sales 和 cost 、rowguid和ModifiedDate 字段。 rowguid列是從函數newid創建的,而ModifiedDate列是由getdate創建的。創建一個新行時,只需要提供Name,CountryRegionCode和Group列。方法ExecuteNonQuery定義SQL INSERT語句,添加參數值,並調用SqlCommand類的ExecuteNonQuery方法(代碼文件CommandSamples / Program.cs):

public static void ExecuteNonQuery
{
  try
  {
    using (var connection = new SqlConnection(GetConnectionString()))
    {
      string sql ="INSERT INTO [Sales].[SalesTerritory]"  + "([Name], [CountryRegionCode], [Group])" + "VALUES (@Name, @CountryRegionCode, @Group)";
      var command = new SqlCommand(sql, connection);
      command.Parameters.AddWithValue("Name","Austria");
      command.Parameters.AddWithValue("CountryRegionCode","AT");
      command.Parameters.AddWithValue("Group","Europe");

connection.Open();
int records = command.ExecuteNonQuery(); WriteLine($"{records} inserted"); } } catch (SqlException ex) { WriteLine(ex.Message); } }

  

圖 37.2 

ExecuteNonQuery將命令影響的行數作為int返回。當第一次運行該方法時,將插入一個記錄。第二次運行相同的方法時,由於唯一的索引沖突會產生異常。Name 定義為唯一索引,因此一個Name值不會在表中出現多次。要再次運行該方法,需要首先刪除創建的記錄。

ExecuteScalar

在許多情況下需要從SQL語句返回一個結果值,例如指定表中的記錄計數或服務器上的當前日期/時間。這種情況下可以使用ExecuteScalar方法:

public static void ExecuteScalar()
{
  using (var connection = new SqlConnection(GetConnectionString()))
  {
    string sql ="SELECT COUNT(*) FROM Production.Product";
    SqlCommand command = connection.CreateCommand();
    command.CommandText = sql;
    connection.Open();
    object count = command.ExecuteScalar();
    WriteLine($”counted {count} product records”);
  }
} 

該方法返回一個對象,必要情況下可以將其轉換為適當的類型。如果要調用的SQL僅返回一列,使用ExecuteScalar比其他提取該列的方法更優。這也適用於返回單個值的存儲過程。

 ExecuteReader

ExecuteReader方法執行命令並返回數據讀取器對象,返回的對象可以用於遍歷返回的記錄。 下面代碼片段中的ExecuteReader示例顯示SQL INNER JOIN子句使用。此SQL INNER JOIN子句用於獲取單個產品的價格歷史記錄。價格歷史存儲在表Production.ProductCostHistory中,產品名稱在數據表Production.Product。SQL語句中產品標識符需要一個參數(代碼文件CommandSamples / Program.cs):

private static string GetProductInformationSQL() =>
  "SELECT Prod.ProductID, Prod.Name, Prod.StandardCost, Prod.ListPrice," +     "CostHistory.StartDate, CostHistory.EndDate, CostHistory.StandardCost" +  "FROM Production.ProductCostHistory AS CostHistory  " +   "INNER JOIN Production.Product AS Prod ON" + "CostHistory.ProductId = Prod.ProductId" +   "WHERE Prod.ProductId = @ProductId";

調用SqlCommand對象的方法ExecuteReader時,將返回SqlDataReader。請注意SqlDataReader需要在使用后進行釋放處理。另外注意這次SqlConnection對象在方法的結尾沒有顯式地釋放。將參數CommandBehavior.CloseConnection傳遞給ExecuteReader方法會在關閉閱讀器時自動關閉連接。如果不提供此設置,需要手動關閉連接。

為了從數據讀取器讀取記錄,在while循環中調用Read方法。第一次調用Read方法將光標移動到返回的第一條記錄。當再次調用讀取時,光標位於下一個記錄 - 只要有可用的記錄。如果在下一個位置沒有記錄可用,則Read方法返回false。訪問列的值時,調用不同的GetXXX方法,例如GetInt32,GetString和GetDateTime。這些方法是強類型的,因為它們返回所需的特定類型,如int,string和DateTime。傳遞到這些方法的索引對應於使用SQL SELECT語句檢索的列,即使數據庫結構更改索引也保持不變。需要注意從數據庫返回null的值,使用強類型的GetXXX方法時,GetXXX方法會拋出異常。在檢索到數據時,只有CostHistory.EndDate可以為null;所有其他列不能為數據庫模式定義的空值。為了避免這種異常情況,C#條件語句 ? : 用於檢查SqlDataReader.IsDbNull方法的值是否為空。在這種情況下null被分配給可空的DateTime。僅當值不為null時,DateTime才能被GetDateTime方法訪問(代碼文件CommandSamples / Program.cs):

public static void ExecuteReader(int productId)
{
  var connection = new SqlConnection(GetConnectionString());
string sql = GetProductInformationSQL(); var command = new SqlCommand(sql, connection); var productIdParameter = new SqlParameter("ProductId", SqlDbType.Int); productIdParameter.Value = productId; command.Parameters.Add(productIdParameter); connection.Open(); using (SqlDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection)) { while (reader.Read()) { int id = reader.GetInt32(0); string name = reader.GetString(1); DateTime from = reader.GetDateTime(4); DateTime? to = reader.IsDBNull(5) ? (DateTime?)null: reader.GetDateTime(5); decimal standardPrice = reader.GetDecimal(6); WriteLine($"{id} {name} from: {from:d} to: {to:d};" + $"price: {standardPrice}"); } } }

 當運行應用程序並將產品ID 717傳遞給ExecuteReader方法時,可以看到以下輸出:

717 HL Road Frame—Red, 62 from: 5/31/2011 to: 5/29/2012; price: 747.9682
717 HL Road Frame—Red, 62 from: 5/30/2012 to: 5/29/2013; price: 722.2568
717 HL Road Frame—Red, 62 from: 5/30/2013 to:; price: 868.6342 

 有關產品ID的可能值,請檢查數據庫的內容。使用SqlDataReader,可以使用返回對象的非類型化索引器而不必使用類型化的方法GetXXX,但需要轉換為相應的類型:

int id = (int)reader[0];
string name = (string)reader[1];
DateTime from = (DateTime)reader[2];
DateTime? to = (DateTime?)reader[3];

 SqlDataReader的索引器還允許使用字符串傳遞列名而不僅是int。這是這些不同選項中最慢的方法,但它可能滿足您的需求。與進行服務調用所花費的時間相比,訪問索引器所需的額外時間可以忽略:

int id = (int)reader["ProductID"];
string name = (string)reader["Name"];
DateTime from = (DateTime)reader["StartDate"];
DateTime? to = (DateTime?)reader["EndDate"];

調用存儲過程 

使用命令對象調用存儲過程只關系到存儲過程的名稱,為該存儲過程的每個參數添加定義,然后使用上一節中介紹的方法之一執行命令。
以下示例調用存儲過程uspGetEmployeeManagers以獲取員工的所有經理。此存儲過程接收一個參數,使用遞歸查詢返回所有管理器的記錄:

CREATE PROCEDURE [dbo].[uspGetEmployeeManagers]
    @BusinessEntityID [int]
AS
—...

要查看存儲過程的實現,請檢查AdventureWorks2014數據庫。
為了調用存儲過程,需將SqlCommand對象的CommandText設置為存儲過程的名稱,並將CommandType設置為CommandType.StoredProcedure。除此之外,該命令的調用方式與之前看到的方式類似。該參數是使用SqlCommand對象的CreateParameter方法創建的,但也可以使用早期的其他方法創建參數。使用參數需填充SqlDbType,ParameterName和Value屬性。由於存儲過程返回記錄,所以通過調用方法ExecuteReader來調用它(代碼文件CommandSamples / Program.cs):

 private static void StoredProcedure(int entityId)
{
  using (var connection = new SqlConnection(GetConnectionString()))
  {
    SqlCommand command = connection.CreateCommand();
    command.CommandText ="[dbo].[uspGetEmployeeManagers]";
    command.CommandType = CommandType.StoredProcedure;
    SqlParameter p1 = command.CreateParameter();
    p1.SqlDbType = SqlDbType.Int;
    p1.ParameterName ="@BusinessEntityID";
    p1.Value = entityId;
    command.Parameters.Add(p1);
    connection.Open();
    using (SqlDataReader reader = command.ExecuteReader())
    {
      while (reader.Read())
      {
        int recursionLevel = (int)reader["RecursionLevel"];
        int businessEntityId = (int)reader["BusinessEntityID"];
        string firstName = (string)reader["FirstName"];
        string lastName = (string)reader["LastName"];
        WriteLine($"{recursionLevel} {businessEntityId}" +
          $"{firstName} {lastName}");
      }
    }
  }
}

運行應用程序並傳遞實體ID 251時,可以獲得此員工的經理,如下所示:

0 251 Mikael Sandberg
1 250 Sheela Word
2 249 Wendy Kahn 

根據存儲過程的返回類型,需要使用ExecuteReader,ExecuteScalar或ExecuteNonQuery調用存儲過程。
使用包含輸出參數的存儲過程,需要指定SqlParameter的Direction屬性。通常情況下方向為ParameterDirection.Input:

var pOut = new SqlParameter();
pOut.Direction = ParameterDirection.Output;

異步數據訪問 

訪問數據庫可能需要一些時間,這里不應該限制用戶交互。 ADO.NET類通過提供異步方法以及同步方法來提供基於任務的異步編程。以下代碼片段與使用SqlDataReader的上一個代碼片段類似,但它使用Async方法調用。連接用SqlConnection.OpenAsync打開,讀取器從方法SqlCommand.ExecuteReaderAsync返回,同時檢索記錄使用SqlDataReader.ReadAsync。通過所有這些方法,調用線程不會被阻塞,這樣可以在獲取結果之前進行其他工作(代碼文件AsyncSamples / Program.cs):

public static void Main()
{
  ReadAsync(714).Wait();
}
public static async Task ReadAsync(int productId) { var connection = new SqlConnection(GetConnectionString()); string sql =
"SELECT Prod.ProductID, Prod.Name, Prod.StandardCost, Prod.ListPrice," + "CostHistory.StartDate, CostHistory.EndDate, CostHistory.StandardCost" + "FROM Production.ProductCostHistory AS CostHistory " + "INNER JOIN Production.Product AS Prod ON" + "CostHistory.ProductId = Prod.ProductId" + "WHERE Prod.ProductId = @ProductId"; var command = new SqlCommand(sql, connection); var productIdParameter = new SqlParameter("ProductId", SqlDbType.Int); productIdParameter.Value = productId; command.Parameters.Add(productIdParameter); await connection.OpenAsync(); using (SqlDataReader reader = await command.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { int id = reader.GetInt32(0); string name = reader.GetString(1); DateTime from = reader.GetDateTime(4); DateTime? to = reader.IsDBNull(5) ? (DateTime?)null: reader.GetDateTime(5); decimal standardPrice = reader.GetDecimal(6); WriteLine($"{id} {name} from: {from:d} to: {to:d};" +$"price: {standardPrice}"); } } }

使用異步方法調用不僅有利於Windows應用程序,在服務器端同時進行多個調用也很有用。 ADO.NET API的異步方法有重載以支持CancellationToken早期停止長時間運行的方法。
注意 有關異步方法調用和CancellationToken的更多信息,請參閱第15章“異步編程”。

事務

默認情況下單個命令在事務內運行。如果需要發出多個命令,並且所有這些命令都發生或者都沒有發生,那么可以顯式地啟動和提交事務。
事務由術語ACID描述。 ACID是原子性,一致性,隔離性和持久性四個詞的首字母縮寫:

  • 原子性 - 表示一個工作單元。使用事務,完整的工作單元成功或沒有任何更改。
  • 一致性 - 事務開始之前和事務完成后的狀態必須有效。在事務期間狀態可以具有臨時值。
  • 隔離性 - 並發的事務同時發生,但事務期間更改的狀態會被隔離。事務A在事務完成之前無法看到事務B的臨時狀態。
  • 持久性 - 事務完成后,必須以持久方式存儲。這意味着如果電源關閉或服務器崩潰,則必須在重新引導時恢復狀態。

注意 事務和有效的狀態可以簡單地形容為婚禮。一對新婚夫婦站在事務協調員面前,事務協調員問這對夫婦中的第一個:“你願意和你身邊的這個人結婚嗎?”如果第一個同意,第二個會被問:“你願意和這個人結婚嗎 ”如果第二個拒絕,第一個接收回滾。此事務的有效狀態只是兩者都已婚,或都沒有結婚。如果兩者都同意,則交易被提交並且兩者都處於已婚狀態。只要有一個拒絕,交易被中止,並且都保持在未婚狀態。無效的狀態是一個已婚,另一個未婚。事務能保證結果永遠不會處於無效狀態。

ADO.NET可以通過調用SqlConnection的BeginTransaction方法來啟動事務。事務總是與一個連接相關聯,不能通過多個連接創建事務。方法BeginTransaction會返回一個SqlTransaction,后者又需要與在同一事務下運行的命令一起使用(代碼文件TransactionSamples / Program.cs):

public static void TransactionSample()
{
  using (var connection = new SqlConnection(GetConnectionString()))
  {
    await connection.OpenAsync();
    SqlTransaction tx = connection.BeginTransaction();
    // etc.
  }
}

注意,實際上可以創建跨多個連接的事務。這樣,Windows操作系統將使用分布式事務處理協調器。可以使用TransactionScope類創建分布式事務。然而,這個類是完整的.NET框架中的一個功能,並沒有整合.NET Core中,因此它不是這本書的內容。如果您需要了解有關TransactionScope的更多信息,請參閱本書的前一版本,例如《Professional 5 and.NET 4.5.1》。

示例代碼在Sales.CreditCard表中創建一條記錄。使用SQL子句INSERT INTO添加一條記錄。 CreditCard表定義了自增標識符,第二個SQL語句SELECT SCOPE_IDENTITY()返回已創建的標識符。實例化SqlCommand對象后,通過設置Connection屬性來分配連接,並設置Transaction屬性來分配事務。使用ADO.NET事務,不能將事務分配給使用不同連接的命令。但是可以使用同一個與事務無關的連接創建命令:

public static void TransactionSample()
{
  // etc.
    try
    {
      string sql ="INSERT INTO Sales.CreditCard" + "(CardType, CardNumber, ExpMonth, ExpYear)" + "VALUES (@CardType, @CardNumber, @ExpMonth, @ExpYear);" + "SELECT SCOPE_IDENTITY()";
var command = new SqlCommand(); command.CommandText = sql; command.Connection = connection; command.Transaction = tx; // etc. }

在定義參數並填充值之后,調用ExecuteScalarAsync方法來執行命令。本次ExecuteScalarAsync方法與INSERT INTO子句一起使用,因為完整的SQL語句返回單個結果后結束:創建的標識符從SELECT SCOPE_IDENTITY()返回。如果在WriteLine方法之后設置斷點並檢查數據庫中的結果,則不會在數據庫中看到新記錄,盡管已返回創建的標識符,其中的原因是事務尚未提交:

public static void TransactionSample()
{
  // etc.
var p1 = new SqlParameter("CardType", SqlDbType.NVarChar, 50); var p2 = new SqlParameter("CardNumber", SqlDbType.NVarChar, 25); var p3 = new SqlParameter("ExpMonth", SqlDbType.TinyInt); var p4 = new SqlParameter("ExpYear", SqlDbType.SmallInt); command.Parameters.AddRange(new SqlParameter[] { p1, p2, p3, p4 }); command.Parameters["CardType"].Value ="MegaWoosh"; command.Parameters["CardNumber"].Value ="08154711123"; command.Parameters["ExpMonth"].Value = 4; command.Parameters["ExpYear"].Value = 2019; object id = await command.ExecuteScalarAsync(); WriteLine($"record added with id: {id}"); // etc. }

現在可以在同一事務中創建另一個記錄。示例代碼使用同一個連接和事務仍然關聯的命令,只是在再次調用ExecuteScalarAsync之前,命令參數值已被更改。還可以創建一個新的SqlCommand對象訪問同一個數據庫中不同的表。調用SqlTransaction對象的Commit方法提交該事務。提交后,可以在數據庫中看到新的記錄:

public static void TransactionSample()
{
      // etc.
      command.Parameters["CardType"].Value ="NeverLimits";
      command.Parameters["CardNumber"].Value ="987654321011";
      command.Parameters["ExpMonth"].Value = 12;
      command.Parameters["ExpYear"].Value = 2025;

id
= await command.ExecuteScalarAsync(); WriteLine($"record added with id: {id}");
// throw new Exception("abort the transaction");

tx.Commit(); } // etc. }

如果發生錯誤,Rollback方法會撤消同一事務中的所有SQL命令,並且狀態被重置為在事務開始之前。可以通過在提交之前取消注釋異常簡單地模擬回滾:

public static void TransactionSample()
{
    // etc. 
    catch (Exception ex)
    {
      WriteLine($"error {ex.Message}, rolling back");
      tx.Rollback();
    }
  }
}

如果在調試模式下運行程序時斷點活動時間過長,事務將被中止,原因是事務超時。事務並不意味着事務處於活動狀態時允許用戶輸入。增加用戶輸入的事務超時時長也沒有用,因為事務活動會導致數據庫內加鎖。根據讀取和寫入的記錄,可能發生行鎖,頁鎖或表鎖。創建事務時可以設置用隔離級別來決定鎖定,從而影響數據庫的性能。然而,這也影響事務的ACID屬性 - 例如,不是一切都是孤立的。
事務的隔離級別默認為ReadCommitted。可以設置的不同選項如下表所示。

隔離級別

說明

ReadUncommitted

事務不彼此隔離。使用此級別時不會等待其他事務鎖定的記錄。未提交的數據可以從其他事務讀取 - 臟讀。此級別通常只用於讀取記錄,如果讀取臨時更改(例如報告)也無關緊要。

ReadCommitted

等待其他事務寫鎖定的記錄。這種情況下臟讀不會發生。此級別會為讀取的當前記錄設置讀鎖,並為正在寫入的記錄設置寫鎖,直到事務完成。在讀取一系列的記錄期間,讀取新記錄之前會解鎖之前加了讀鎖的記錄。這就是不可重復讀取可能發生的原因。

RepeatableRead

保留讀取的記錄的鎖定,直到事務完成。這樣避免了不可重復讀取的問題。但幻讀(Phantom Reads)仍然可以發生。

Serializable

保持范圍鎖定。事務正在運行時,不能添加屬於該事務讀取范圍數據的新記錄。

Snapshot

使用此級別從實際數據完成快照。此級別減少了復制修改行時的鎖定。這樣其他事務仍然可以讀取舊數據而無需等待釋放鎖。

Unspecified

意味着提供程序使用的隔離級別無法識別IsolationLevel枚舉定義。

Chaos

此級別與ReadUncommitted類似,但除了執行ReadUncommitted值的操作之外,Chaos不會鎖定被更新的記錄。

下表總結了由於設置最常用的事務隔離級別而可能發生的問題。

隔離級別

臟讀

不可重復讀

幻讀

ReadUncommitted

Y

Y

Y

ReadCommitted

N

Y

Y

RepeatableRead

N

N

Y

Serializable

Y

Y

Y

總結 

本章中可以了解ADO.NET的核心基礎。首先接觸了SqlConnection對象打開SQL Server的連接。了解了如何從配置文件檢索連接字符串。
本章解釋了如何正確使用連接,盡早關閉它們從而節省寶貴的資源。所有連接類實現了IDisposable接口,對象可以在using語句中時調用,那么有一件事情可以從本章中刪除,就是盡早關閉數據庫連接的重要性(譯者:using 結束后會自動釋放資源)。
使用命令傳遞參數,獲取單個返回值,並使用SqlDataReader檢索記錄。還了解了如何使用SqlCommand對象調用存儲過程。
類似於框架的其他部分,其中的處理可能需要一些時間,ADO.NET實現了基於任務的異步模式。還了解了如何使用ADO.NET創建和使用事務。
下一章是關於ADO.NET實體框架,它通過數據庫和對象層次關系之間的映射來提供數據訪問的對象模型,並在訪問關系數據庫時在后台使用ADO.NET類。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM