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

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

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

-------------------------------數據庫

本章內容
編程

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

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

wrox.com中本章源代碼下載位於「Download Code」選項卡www.wrox.com/go/professionalcsharp6。本章分爲如下幾個主要例子:windows

  • 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類。

相關文章
相關標籤/搜索