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类。

相关文章
相关标签/搜索