Professional C# 6 and .NET Core 1.0 - Chapter 37 ADO.NET

本文内容为转载,供学习研究。如有侵权,请联系作者删除。

转载请注明本文出处:Professional C# 6 and .NET Core 1.0 - 37 ADO.NET

译文:C# 6 与 .NET Core 1.0 高级编程 - 37 章 ADO.NET

 

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

What’s In This Chapter?

  • Connecting to the database
  • Executing commands
  • Calling stored procedures
  • The ADO.NET object model

Wrox.com Code Downloads for This Chapter

The wrox.com code downloads for this chapter are found at

www.wrox.com/go/professionalcsharp6 on theDownload Code tab. The code for

this chapter is divided into the following major examples:

  • ConnectionSamples
  • CommandSamples
  • AsyncSamples
  • TransactionSamples 

ADO.NET Overview 

This chapter discusses how to access a relational database like SQL Server from your C# programs using ADO.NET. It shows you how to connect to and disconnect from a database, how to use queries, and how to add and update records. You learn the various command object options and see how commands can be used for each of the options presented by the SQL Server provider classes; how to call stored procedures with command objects; and how to use transactions.

ADO.NET previously shipped different database providers: a provider for SQL Server and one for Oracle, using OLEDB and ODBC. The OLEDB technology is discontinued, so this provider shouldn’t be used with new applications. Accessing the Oracle database, Microsoft’s provider is discontinued as well because a provider from Oracle (http://www.oracle.com/technetwork/topics/dotnet/)

better fits the needs. For other data sources (also for Oracle), many third-party providers are available. Before using the ODBC provider, you should use a provider specific for the data source you access. The code samples in this chapter are based on SQL Server, but you can easily change it to use different connection and command objects, such as OracleConnection and OracleCommand when accessing the Oracle database instead of SqlConnection and SqlCommand. 

NOTE This chapter does not cover the DataSet to have tables in memory. Datasets enable you to retrieve records from a database and store the content within in-memory data tables with relations. Instead you should use Entity Framework, which is covered in Chapter 38, “Entity Framework Core.” Entity Framework enables you to have object relations instead of table-based relations. 

Sample Database 

The examples in this chapter use the AdventureWorks2014 database. You can download this database from https://msftdbprodsamples.codeplex.com/. With this link you can download a backup of the AdventureWorks2014 database in a zip file. Select the recommended download—Adventure Works 2014 Full Database Backup.zip. After unzipping the file, you can restore the database backup using SQL Server Management Studio as shown in Figure 37.1. In case you don’t have SQL Server Management Studio on your system, you can download a free version from http://www.microsoft.com/downloads.

 

Figure 37.1  

The SQL server used with this chapter is SQL Server LocalDb. This is a database server that is installed as part of Visual Studio. You can use any other SQL Server edition as well; you just need to change the connection string accordingly. 

NuGet Packages and Namespaces 

The sample code for all the ADO.NET samples makes use of the following

dependencies and namespaces: 

Dependencies

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

 

Namespaces

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

 

Using Database Connections 

To access the database, you need to provide connection parameters, such as the machine on which the database is running and possibly your login credentials. You make a connection to SQL Server using the SqlConnection class.

The following code snippet illustrates how to create, open, and close a connection to the AdventureWorks2014 database (code file 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();
}

 NOTE The SqlConnection class implements the IDisposable interface with the Dispose method in addition to the Close method. Both do the same, to release the connection. With this, you can use the using statement to close the connection. 

In the example connection string, the parameters used are as follows (the parameters are delimited by a semicolon in the connection string):

  • server=(localdb)\MSSQLLocalDB—This denotes the database server to connect to. SQL Server permits a number of separate database server instances to be running on the same machine. Here, you are connecting to the localdb Server and the MSSQLLocalDB SQL Server instance that is created with the installation of SQL Server. If you are using the local installation of SQL Server, change this part to server=(local). Instead of using the keyword server, you can use Data Source instead. Connecting to SQL Azure, you can set Data Source=servername.database.windows.net.
  • database=AdventureWorks2014—This describes the database instance to connect to; each SQL Server process can expose several database instances. Instead of the keyword database, you can instead use Initial Catalog.
  • integrated security=SSPI—This uses Windows Authentication to connect to the database. In case you are using SQL Azure, you need to set User Id and Password instead.

 NOTE You can find great information about connection strings with many different databases at http://www.connectionstrings.com.

 The ConnectionSamples example opens a database connection using the defined connection string and then closes that connection. After you have opened the connection, you can issue commands against the data source; when you are finished, you can close the connection.

 Managing Connection Strings

Instead of hard-coding the connection string with the C# code, it is better to read it from a configuration file. With .NET 4.6 and .NET Core 1.0, configuration files can be JSON or XML formats, or read from environmental variables. With the following sample, the connection string is read from a JSON configuration file (code file ConnectionSamples/config.json):

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

          Trusted_Connection=True;"
    }
  }
} 

The JSON file can be read using the Configuration API defined in the NuGet package Microsoft.Framework.Configuration. To use JSON configuration files, the NuGet package Microsoft.Framework.Configuration.Json is added as well. For reading a configuration file, the ConfigurationBuilder is created. The AddJsonFile

extension method adds the JSON file config.json to read configuration

information from this file—if it is in the same path as the program. To configure a different path, you can invoke the method SetBasePath. Invoking the Build method of the ConfigurationBuilder builds up the configuration from all the added configuration files and returns an object implementing the Iconfiguration interface. With this, the configuration values can be retrieved, such as the configuration value for Data:DefaultConnection:ConnectionString (code file 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);
}

 

Connection Pools

When two-tier applications were done several years ago it was a good idea to open the connection on application start and close it only when the application was closed. Nowadays, this is not a good idea. The reason for this program architecture was that it takes some time to open a connection. Now, closing a connection doesn’t close the connection with the server. Instead, the connection is added to a

connection pool. When you open the connection again, it can be taken from the pool, thus it is very fast to open a connection; it only takes time to open the first connection.

Pooling can be configured with several options in the connection string. Setting the option Pooling to false disables the connection pool; by default it’s enabled —Pooling = true. Min Pool Size and Max Pool Size enable you to configure the number of connections in the pool. By default, Min Pool Size has a value of 0 and Max Pool Size has a value of 100. Connection Lifetime defines how long a connection should stay inactive in the pool before it is really released.

 Connection Information

After creating a connection, you can register event handlers to get some information about the connection. The SqlConnection class defines the InfoMessage and StateChange events. The InfoMessage event is fired every time an information or warning message is returned from SQL Server. The StateChange event is fired when the state of the connection changes—for example, the connection is opened or closed (code file 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
  }
}

 When you run the application, you can see the StateChange event fired and the Open and Closed state:

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

 

Commands

The “Using Database Connections” section briefly touched on the idea of issuing commands against a database. A command is, in its simplest form, a string of text containing SQL statements to be issued to the database. A command could also be a stored procedure, shown later in this section.

A command can be constructed by passing the SQL statement as a parameter to the constructor of the Command class, as shown in this example (code file 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.
  }
}

  A command can also be created by invoking the CreateCommand method of the SqlConnection and assigning the SQL statement to the CommandText property:

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

  Commands often need parameters. For example, the following SQL statement requires an EmailPromotion parameter. Don’t be incited to use string concatenation to build up parameters. Instead, always use the parameter features of ADO.NET: 

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

  When you add the parameter to the SqlCommand object, there’s a simple way to use the Parameters property that returns a SqlParameterCollection and the AddWithValue method:

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

 A more efficient method that’s more programming work is to use overloads of the Add method by passing the name and the SQL data type:

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

 It’s also possible to create a SqlParameter object and add this to the SqlParameterCollection. 

NOTE Don’t be inclined to use string concatenation with SQL parameters. This is often misused for SQL injection attacks. Using SqlParameter objects inhibits such attacks.

 After you have defined the command, you need to execute it. There are several ways to issue the statement, depending on what, if anything, you expect to be returned from that command. The SqlCommand class provides the following ExecuteXX methods:

  • ExecuteNonQuery—Executes the command but does not return any output
  • ExecuteReader—Executes the command and returns a typed IDataReader
  • ExecuteScalar—Executes the command and returns the value from the first column of the first row of any result set

 ExecuteNonQuery 

The ExecuteNonQuery method is commonly used for UPDATE, INSERT, or DELETE statements, for which the only returned value is the number of records affected. This method can, however, return results if you call a stored procedure that has output parameters. The sample code creates a new record within the Sales.SalesTerritory table. This table has a TerritoryID as primary key that is an identity column and thus does not need to be supplied creating the record. All the columns of this table don’t allow null (see Figure 37.2), but several of them have default values—such as a few sales and cost columns, the rowguid, and the ModifiedDate. The rowguid column is created from the function newid, and the ModifiedDate column is created from getdate. When creating a new row, just the Name, the CountryRegionCode, and Group columns need to be supplied. The method ExecuteNonQuery defines the SQL INSERT statement, adds values for the parameters, and invokes the ExecuteNonQuery method of the SqlCommand class (code file 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); } }

  

Figure 37.2 

ExecuteNonQuery returns the number of rows affected by the command as an int. When you run the method the first time, one record is inserted. When you run the same method a second time, you get an exception because of a unique index conflict. The Name has a unique index defined and thus is allowed only once. To run the method a second time, you need to delete the created record first. 

ExecuteScalar

 On many occasions it is necessary to return a single result from a SQL statement, such as the count of records in a given table or the current date/time on the server. You can use the ExecuteScalar method in such situations: 

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”);
  }
} 

The method returns an object, which you can cast to the appropriate type if required. If the SQL you are calling returns only one column, it is preferable to use ExecuteScalar over any other method of retrieving that column. That also applies to stored procedures that return a single value.

 ExecuteReader

 The ExecuteReader method executes the command and returns a data reader object. The object returned can be used to iterate through the record(s) returned. The ExecuteReader sample makes use of an SQL INNER JOIN clause that is shown in the following code snippet. This SQL INNER JOIN clause is used to get a price history of a single product. The price history is stored in the table Production.ProductCostHistory, the name of the product in the table Production.Product. With the SQL statement a single parameter is needed for the product identifier (code file 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";

 

When you invoke the method ExecuteReader of the SqlCommand object, a SqlDataReader is returned. Note that the SqlDataReader needs to be disposed after it has been used. Also note that this time the SqlConnection object is not explicitly disposed at the end of the method. Passing the parameter CommandBehavior.CloseConnection to the ExecuteReader method automatically closes the connection on closing of the reader. If you don’t supply this setting, you still need to close the connection.

For reading the records from the data reader, the Read method is invoked within a while loop. The first call to the Read method moves the cursor to the first record returned. When Read is invoked again, the cursor is positioned to the next record— as long as there’s a record available. The Read method returns false if no record is available at the next position. When accessing the values of the columns, different GetXXX methods are invoked, such as GetInt32, GetString, and GetDateTime. These methods are strongly typed as they return the specific type needed, such as int, string, and DateTime. The index passed to these methods corresponds to the columns retrieved with the SQL SELECT statement, so the index stays the same even if the database structure changes. With the strongly typed GetXXX methods you need to pay attention to values where null is returned from the database; here the GetXXX method throws an exception. With the data retrieved, only the CostHistory.EndDate can be null; all other columns can’t be null as defined by the database schema. To avoid an exception in this case, the C# conditional statement ?: is used to check whether the value is null with the SqlDataReader.IsDbNull method. In that case, null is assigned to a nullable DateTime. Only if the value is not null, the DateTime is accessed with the GetDateTime method (code file 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}"); } } }

  When you run the application and pass the product ID 717 to the ExecuteReader method, you see this output:

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 

 

 For the possible values for the product ID, check the content of the database. With the SqlDataReader, instead of using the typed methods GetXXX, you can use the untyped indexer that returns an object. With this, you need to cast to the corresponding type: 

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

 

The indexer of the SqlDataReader also allows a string to be used instead of the int, passing the column name. This is the slowest method of these different options, but it might fulfill your needs. Compared to the time it costs to make a service call, the additional time needed to access the indexer can be ignored: 

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

 Calling Stored Procedures 

Calling a stored procedure with a command object is just a matter of defining the name of the stored procedure, adding a definition for each parameter of the procedure, and then executing the command with one of the methods presented in the previous section.

The following sample calls the stored procedure uspGetEmployeeManagers to get all the managers of an employee. This stored procedure receives one parameter to return records of all managers using recursive queries:

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

 To see the implementation of the stored procedure, check the

AdventureWorks2014 database.

To invoke the stored procedure, the CommandText of the SqlCommand object is set to the name of the stored procedure, and the CommandType is set to CommandType.StoredProcedure. Other than that, the command is invoked similarly to the way you’ve seen before. The parameter is created using the CreateParameter method of the SqlCommand object, but you can use other methods to create the parameter used earlier as well. With the parameter, the SqlDbType, ParameterName, and Value properties are filled. Because the stored procedure returns records, it is invoked by calling the method ExecuteReader (code file 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}");
      }
    }
  }
}

  When you run the application and pass the entity ID 251, you get the managers of this employee as shown:

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

 

Depending on the return of the stored procedure, you need to invoke the stored procedure with ExecuteReader, ExecuteScalar, or ExecuteNonQuery.

With a stored procedure that contains Output parameters, you need to specify the Direction property of the SqlParameter. By default, the direction is ParameterDirection.Input:

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

 Asynchronous Data Access 

Accessing the database can take some time. Here you shouldn’t block the user interface. The ADO.NET classes offer task-based asynchronous programming by offering asynchronous methods in addition to the synchronous ones. The following code snippet is similar to the previous one using the SqlDataReader, but it makes use of Async method calls. The connection is opened with SqlConnection.OpenAsync, the reader is returned from the method

SqlCommand.ExecuteReaderAsync, and the records are retrieved using

SqlDataReader.ReadAsync. With all these methods, the calling thread is not blocked but can do other work before getting the result (code file 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}"); } } }

  Using the asynchronous method calls is not only advantageous with Windows applications but also useful on the server side for making multiple calls simultaneous. The asynchronous methods of the ADO.NET API have overloads to support the CancellationToken for an earlier stop of a long-running method.

 NOTE For more information about asynchronous method calls and the CancellationToken, read Chapter 15, “Asynchronous Programming.”

 

 Transactions 

By default, a single command is running within a transaction. If you need to issue multiple commands, and either all of these or none happen, you can start and commit transactions explicitly.

Transactions are described by the term ACID. ACID is a four-letter acronym for atomicity, consistency, isolation, and durability:

  • Atomicity—Represents one unit of work. With a transaction, either the complete unit of work succeeds or nothing is changed.
  • Consistency—The state before the transaction was started and after the transaction is completed must be valid. During the transaction, the state may have interim values.
  • Isolation—Transactions that happen concurrently are isolated from the state, which is changed during a transaction. Transaction A cannot see the interim state of transaction B until the transaction is completed.
  • Durability—After a transaction is completed, it must be stored in a durable way. This means that if the power goes down or the server crashes, the state must be recovered at reboot. 

NOTE Transactions and valid state can easily be described as a wedding ceremony. A bridal couple is standing before a transaction coordinator. The transaction coordinator asks the first of the couple: “Do you want to marry this person on your side?” If the first one agrees, the second is asked: “Do you want to marry this person?” If the second one declines, the first receives a rollback. A valid state with this transaction is only that both are married, or none are. If both agree, the transaction is committed and both are in the married state. If one denies, the transaction is aborted and both stay in the unmarried state. An invalid state is that one is married, and the other is not. The transaction guarantees that the result is never in an invalid state. 

With ADO.NET, transactions can be started by invoking the BeginTransaction method of the SqlConnection. A transaction is always associated with one connection; you can’t create transactions over multiple connections. The method BeginTransaction returns an SqlTransaction that in turn needs to be used with the commands running under the same transaction (code file TransactionSamples/Program.cs):

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

 NOTEIndeed, you can create transactions spanning multiple connections. With this, using the Windows operating system, the Distributed Transaction Coordinator is used. You can create distributed transactions using the TransactionScope class. However, this class is only part of the full .NET Framework and has not been brought forward to .NET Core; thus it is not part of this book. In case you need to know more about TransactionScope, consult a previous edition of the book, such asProfessional C# 5 and .NET 4.5.1.

The code sample creates a record in the Sales.CreditCard table. Using the SQL clause INSERT INTO, a record is added. The CreditCard table defines an auto-increment identifier that is returned with the second SQL statement SELECT SCOPE_IDENTITY() that returns the created identifier. After the SqlCommand object is instantiated, the connection is assigned by setting the Connection property, and the transaction is assigned by setting the Transaction property. With ADO.NET transactions, you cannot assign the transaction to a command that uses a different connection. However, you can also create commands with the same connection that are not related to a transaction:

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.
} 

 After defining the parameters and filling the values, the command is executed by invoking the method ExecuteScalarAsync. This time the ExecuteScalarAsync method is used with the INSERT INTO clause because the complete SQL statement ends by returning a single result: The created identifier is returned from SELECT SCOPE_IDENTITY(). In case you set a breakpoint after the WriteLine method and check the result in the database, you will not see the new record in the database although the created identifier is already returned. The reason is that the transaction is not yet committed:

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. }

  Now another record can be created within the same transaction. With the sample code, the same command is used that has the connection and transaction still associated, just the values are changed before invoking ExecuteScalarAsync again. You could also create a new SqlCommand object that accesses a different table in the

same database. The transaction is committed invoking the Commit method of the SqlTransaction object. After the commit, you can see the new records in the database: 

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. }

In case an error occurs, the Rollback method makes an undo of all the SQL commands in the same transaction. The state is reset as it was before the transaction was started. You can easily simulate a rollback by uncommenting the exception before the commit:

public static void TransactionSample()
{
    // etc. 

    catch (Exception ex)
    {
      WriteLine($"error {ex.Message}, rolling back");
      tx.Rollback();
    }
  }
}

In case you run the program in debugging mode and have a breakpoint active for too long, the transaction will be aborted because the transaction timeout is reached. Transactions are not meant to have user input while the transaction is active. It’s also not useful to increase the transaction timeout for user input, because having a transaction active causes locks within the database. Depending on the records you read and write, either row locks, page locks, or table locks can happen. You can influence the locks and thus performance of the database by setting an isolation level for creating the transaction. However, this also influences the ACID properties of the transaction—for example, not everything is isolated.

The default isolation level that is applied to the transaction is ReadCommitted. The following table shows the different options you can set.

Isolation Level

Description

ReadUncommitted

Transactions are not isolated from each other. With this level, there is no wait for locked records from other transactions. This way, uncommitted data can be read from other transactions—dirty reads. This level is usually only used for reading records for which it does not matter if you read interim changes, such as reports.

ReadCommitted

Waits for records with a write-lock from other transactions. This way, a dirty read cannot happen. This level sets a read-lock for the current record read and a write-lock for the records being written until the transaction is completed. During the reading of a sequence of records, with every new record that is read, the prior record is unlocked. That’s why nonrepeatable reads can happen.

RepeatableRead

Holds the lock for the records read until the transaction is completed. This way, the problem of nonrepeatable reads is avoided. Phantom reads can still occur.

Serializable

Holds a range lock. While the transaction is running, it is not possible to add a new record that belongs to the same range from which the data is being read.

Snapshot

With this level a snapshot is done from the actual data. This level reduces the locks as modified rows are copied. That way, other transactions can still read the old data without needing to wait for releasing of the lock.

Unspecified

Indicates that the provider is using an isolation level that is different from the values defined by the IsolationLevel enumeration.

Chaos

This level is similar to ReadUncommitted, but in addition to performing the actions of the ReadUncommitted value, Chaos does not lock updated records.

  The following table summarizes the problems that can occur as a result of setting the most commonly used transaction isolation levels.

Isolation Level

Dirty Reads

Nonrepeatable Reads

Phantom Reads

ReadUncommitted

Y

Y

Y

ReadCommitted

N

Y

Y

RepeatableRead

N

N

Y

Serializable

Y

Y

Y

Summary 

In this chapter, you’ve seen the core foundation of ADO.NET. You first looked at the SqlConnection object to open a connection to SQL Server. You’ve seen how to retrieve the connection string from a configuration file.

This chapter explained how to use connections properly so that they can be closed as early as possible, which preserves valuable resources. All the connection classes implement the IDisposable interface, called when the object is placed within a using statement. If there is one thing you should take away from this chapter, it is the importance of closing database connections as early as possible.

With commands you’ve seen passing parameters, getting a single return value, and retrieving records using the SqlDataReader. You’ve also seen how stored procedures can be invoked using the SqlCommand object.

Similar to other parts of the framework where processing can take some time, ADO.NET implements the task-based async pattern that was shown as well. You’ve also seen how to create and use transactions with ADO.NET.

The next chapter is about the ADO.NET Entity Framework that offers an abstraction to data access by offering a mapping between relations in the database and object hierarchies, and uses ADO.NET classes behind the scenes when you’re accessing a relational database.

posted @ 2017-02-06 14:09  沐汐Vicky  阅读(1208)  评论(2编辑  收藏  举报