196 Microsoft ADO.NET 4 Step by Step Instead of creating instances of SqlTransaction directly, you generate connection-specific transactions using the SqlConnection object’s BeginTransaction method. Transactions work only on open database connections, so you must call the connection object’s Open method first. C# using (SqlConnection linkToDB = new SqlConnection(connectionString)) { linkToDB.Open(); SqlTransaction envelope = linkToDB.BeginTransaction(); Visual Basic Using linkToDB As SqlConnection = New SqlConnection(connectionString) linkToDB.Open() Dim envelope As SqlTransaction = linkToDB.BeginTransaction() After obtaining a transaction object, add it to any SqlCommand objects that should be part of the transaction. C# // Include the transaction in the SqlCommand constructor. SqlCommand updateCommand = new SqlCommand(sqlText, linkToDB, envelope); // Or add it to an existing SqlCommand object. SqlCommand updateCommand = new SqlCommand(sqlText, linkToDB); updateCommand.Transaction = envelope; Visual Basic ' Include the transaction in the SqlCommand constructor. Dim updateCommand As New SqlCommand(sqlText, linkToDB, envelope) ' Or add it to an existing SqlCommand object. Dim updateCommand As New SqlCommand(sqlText, linkToDB) updateCommand.Transaction = envelope After you’ve issued all the transaction-specific commands, you can commit or roll back the entire transaction by calling the SqlTransaction object’s Commit or Rollback method. Chapter 12 Guaranteeing Data Integrity 197 C# // Commit the transaction. envelope.Commit(); // Rollback the transaction. envelope.Rollback(); Visual Basic ' Commit the transaction. envelope.Commit() ' Rollback the transaction. envelope.Rollback() You should always call Commit or Rollback explicitly. If you dispose of the object or allow it to go out of scope without calling one of these two methods, the transaction will be rolled back, but at a time determined by the .NET garbage collection system. Both Commit and Rollback—and the initial BeginTransaction call as well—generate excep- tions if there is a database or local failure in the transaction. Always surround these calls with exception handling statements. C# try { envelope.Commit(); } catch (Exception ex) { MessageBox.Show("Error saving data: " + ex.Message); try { envelope.Rollback(); } catch (Exception ex2) { // Although the rollback generated an error, the // transaction will still be rolled back by the // database because it did not get a commit order. MessageBox.Show("Error undoing the changes: " + ex2.Message); } } 198 Microsoft ADO.NET 4 Step by Step Visual Basic Try envelope.Commit() Catch ex As Exception MessageBox.Show("Error saving data: " & ex.Message) Try envelope.Rollback() Catch ex2 As Exception ' Although the rollback generated an error, the ' transaction will still be rolled back by the ' database because it did not get a commit order. MessageBox.Show("Error undoing the changes: " & ex2.Message) End Try End Try If you include SELECT statements in your transactions, especially on records that will not be modified as part of the transaction, there is a chance that these selected records might become locked during the transaction, preventing other users from making modifications to them, or even reading them. Depending on the configuration of your SQL Server instance, SELECT statements might apply “read locks” on the returned records by default. To avoid such locks, exclude SELECT statements from your transactions or use the WITH (NOLOCK) hint in your SQL Server SELECT statements. SELECT * FROM OrderEntry WITH (NOLOCK) WHERE OrderDate >= DATEADD(day, -3, GETDATE()) Processing with a Local Transaction: C# 1. Open the “Chapter 12 CSharp” project from the installed samples folder. The project includes a Windows.Forms class named AccountTransfer, which simulates the transfer of funds between two bank accounts. 2. Open the code for the AccountTransfer class. Locate the GetConnectionString function, which is a routine that uses a SqlConnectionStringBuilder to create a valid connection string to the sample database. It currently includes the following statements: builder.DataSource = @"(local)\SQLExpress"; builder.InitialCatalog = "StepSample"; builder.IntegratedSecurity = true; Adjust these statements as needed to provide access to your own test database. Chapter 12 Guaranteeing Data Integrity 199 3. Locate the TransferLocal routine. This code performs a transfer between two bank ac- count records using a local SqlTransa ction instance. A using block fills most of the procedure’s body. Just inside this usin g statement, immediately after the comment “The database must be opened to create the transaction,” add the following code: linkToDB.Open(); // Prepare a transaction to surround the transfer. envelope = linkToDB.BeginTransaction(); These statements open the database connection (a requirement for using transactions) and start the transfer’s transaction. 4. Just after the “Prepare and perform the withdrawal” comment, add the following statements: sqlText = @"UPDATE BankAccount SET Balance = Balance - @ToTransfer WHERE AccountNumber = @FromAccount"; withdrawal = new SqlCommand(sqlText, linkToDB, envelope); withdrawal.Parameters.AddWithValue("@ToTransfer", toTransfer); if (OptFromChecking.Checked) withdrawal.Parameters.AddWithValue("@FromAccount", CheckingAccountID); else withdrawal.Parameters.AddWithValue("@FromAccount", SavingsAccountID); These lines create a parameterized UPDATE query within the context of the envelop e transaction. The presence of envelope as the final argument to the SqlCommand con- structor provides this context. 5. Just after the “Prepare and perform the deposit” comment, add the following lines: sqlText = @"UPDATE BankAccount SET Balance = Balance + @ToTransfer WHERE AccountNumber = @ToAccount"; deposit = new SqlCommand(sqlText, linkToDB, envelope); deposit.Parameters.AddWithValue("@ToTransfer", toTransfer); if (OptFromChecking.Checked) deposit.Parameters.AddWithValue("@ToAccount", SavingsAccountID); else deposit.Parameters.AddWithValue("@ToAccount", CheckingAccountID); This block is the same as in the previous step, but it performs the second half of the two-statement transaction. 6. Just after the “Perform the transfer” comment within the t ry block, add these three statements: withdrawal.ExecuteNonQuery(); deposit.ExecuteNonQuery(); envelope.Commit(); This set of lines performs the actual transaction, issuing distinct UPDATE queries for the withdrawal and deposit halves of the atomic transaction. The third method call, Commit, makes the transaction permanent. Any failure on any of these three lines raises an exception in the subsequent catch block. 200 Microsoft ADO.NET 4 Step by Step 7. Just after the “Do a rollback instead” comment, within the inner try block, add the fol- lowing line: envelope.Rollback(); This line undoes the transaction in case of failure in the previous step. 8. Run the program. The form that appears lets you transfer funds between a checking and a savings account. If you try to transfer an amount greater than the amount in the source account, the transaction fails due to “check constraints” defined on the SQL Server table that prevent negative values. Select From Checking To Savings as the trans- fer type and enter 1000 in the Transfer Amount field (or any value that exceeds the bal- ance in the checking account). Click Transfer. The error that occurs triggers a rollback of the transaction. In contrast, operations that transfer funds within the limits of the source account’s balance result in a successful, committed transfer. Processing with a Local Transaction: Visual Basic 1. Open the “Chapter 12 VB” project from the installed samples folder. The project in- cludes a Windows.Forms class named AccountTransfer, which simulates the transfer of funds between two bank accounts. 2. Open the code for the AccountTransfer class. Locate the GetConnectionString function, which is a routine that uses a Sq lCon nectionStringBuilder to create a valid connection string to the sample database. It currently includes the following statements: builder.DataSource = "(local)\SQLExpress" builder.InitialCatalog = "StepSample" builder.IntegratedSecurity = True Adjust these statements as needed to provide access to your own test database. 3. Locate the TransferLocal routine. This code performs a transfer between two bank ac- count records using a local SqlTransaction instance. A Using block fills most of the procedure’s body. Just inside this Using statement, immediately after the comment “The database must be opened to create the transaction,” add the following code: Chapter 12 Guaranteeing Data Integrity 201 linkToDB.Open() ' Prepare a transaction to surround the transfer. envelope = linkToDB.BeginTransaction() These statements open the database connection (a requirement for using transactions) and start the transfer’s transaction. 4. Just after the “Prepare and perform the withdrawal” comment, add the following statements: sqlText = "UPDATE BankAccount SET Balance = Balance - @ToTransfer " & "WHERE AccountNumber = @FromAccount" withdrawal = New SqlCommand(sqlText, linkToDB, envelope) withdrawal.Parameters.AddWithValue("@ToTransfer", toTransfer) If (OptFromChecking.Checked = True) Then withdrawal.Parameters.AddWithValue("@FromAccount", CheckingAccountID) Else withdrawal.Parameters.AddWithValue("@FromAccount", SavingsAccountID) End If These lines create a parameterized UPDATE query within the context of the envelop e transaction. The presence of envelope as the final argument to the SqlCommand con- structor provides this context. 5. Just after the “Prepare and perform the deposit” comment, add the following lines: sqlText = "UPDATE BankAccount SET Balance = Balance + @ToTransfer " & "WHERE AccountNumber = @ToAccount" deposit = New SqlCommand(sqlText, linkToDB, envelope) deposit.Parameters.AddWithValue("@ToTransfer", toTransfer) If (OptFromChecking.Checked = True) Then deposit.Parameters.AddWithValue("@ToAccount", SavingsAccountID) Else deposit.Parameters.AddWithValue("@ToAccount", CheckingAccountID) End If This block is the same as in the previous step, but it performs the second half of the two-statement transaction. 6. Just after the “Perform the transfer” comment within the Try block, add these three statements: withdrawal.ExecuteNonQuery() deposit.ExecuteNonQuery() envelope.Commit() This set of lines performs the actual transaction, issuing distinct UPDATE queries for the withdrawal and deposit halves of the atomic transaction. The third method call, Commit, makes the transaction permanent. Any failure on any of these three lines raises an exception in the subsequent Catch block. 202 Microsoft ADO.NET 4 Step by Step 7. Just after the “Do a rollback instead” comment, within the inner Try block, add the fol- lowing line: envelope.Rollback() This line undoes the transaction in case of failure in the previous step. 8. Run the program. The form that appears lets you transfer funds between a checking and a savings account. If you try to transfer an amount greater than the amount in the source account, the transaction fails due to “check constraints” defined on the SQL Server table that prevent negative values. Select From Checking To Savings as the trans- fer type and enter 1000 in the Transfer Amount field (or any value that exceeds the bal- ance in the checking account). Click Transfer. The error that occurs triggers a rollback of the transaction. In contrast, operations that transfer funds within the limits of the source account’s balance result in a successful, committed transfer. Employing Savepoints Normally a transaction is an all-or-nothing operation. However, the SQL Server provider also includes support for savep oints, which are named partial transactions that can be indepen- dently rolled back. Note Savepoints are available only with the SQL Server provider. The OLE DB and ODBC providers do not support this feature. To add a savepoint to a transaction, call the SqlTransaction object’s Save method, passing it the name of the new savepoint. Chapter 12 Guaranteeing Data Integrity 203 C# // Run the pre-savepoint transaction statements. SqlCommand firstCommand = new SqlCommand(sqlText1, linkToDB, envelope); firstCommand.ExecuteNonQuery(); // Mark this place for possible partial rollback. envelope.Save("HalfwayPoint"); // Run the post-savepoint transaction statements. SqlCommand secondCommand = new SqlCommand(sqlText2, linkToDB, envelope); secondCommand.ExecuteNonQuery(); Visual Basic ' Run the pre-savepoint transaction statements. Dim firstCommand As New SqlCommand(sqlText1, linkToDB, envelope) firstCommand.ExecuteNonQuery() ' Mark this place for possible partial rollback. envelope.Save("HalfwayPoint") ' Run the pre-savepoint transaction statements. Dim secondCommand As New SqlCommand(sqlText2, linkToDB, envelope) secondCommand.ExecuteNonQuery() Calling the Rollback method will roll the transaction back to the very beginning, undoing all statements issued in the context of the transaction. Calling Rollback and passing it a save- point name argument also rolls back the transaction, but only to the state at which the indicated savepoint occurred. In the previous code block, the following statement will roll back the transaction to just after the processing of firstCommand, undoing the effects of secondCommand : C# envelope.Rollback("HalfwayPoint"); Visual Basic envelope.Rollback("HalfwayPoint") You can issue as many savepoints as you need within a transaction. You must still issue a final Commit or Rollb ack on the transaction to save or cancel the transaction’s overall changes. 204 Microsoft ADO.NET 4 Step by Step Using Distributed Transactions The .NET Framework includes support for distributed transactions through the Microsoft Distributed Transaction Coordinator (MSDTC). This system allows an ACID-enabled trans- action to span multiple databases on different servers. Platforms beyond standard relational databases can also participate in MSDTC-distributed transactions as long as they provide full commit/rollback support for included operations. Distributed transactions occur through the System.Transactions.TransactionScope class. This class is not part of ADO.NET, but ADO.NET does include automatic support for it when you use it in your application. To access this class, you must add a reference to the System. Transactions.dll library in your application through the Project | Add Reference menu com- mand in Visual Studio. To begin a distributed transaction, create an instance of TransactionScope. C# using System.Transactions; // Later using (TransactionScope envelope = new TransactionScope()) { // Include all relevant ADO.NET commands here. Visual Basic Imports System.Transactions ' Later Using envelope As TransactionScope = New TransactionScope() ' Include all relevant ADO.NET commands here. That’s it. As long as the TransactionScope object is valid (not disposed), all new ADO.NET database connections become part of the distributed transaction. You don’t need to use SqlTransaction objects or provide support for distributed transactions; it’s automatic. Chapter 12 Guaranteeing Data Integrity 205 The TransactionScope instance monitors all relevant activity until it is disposed (by calling Dispose or letting the object go out of scope), at which time the entire transaction is either committed or rolled back. By default, the transaction is rolled back. To ensure that the trans- action is committed, call the TransactionScope object’s Comp lete method. C# using (TransactionScope envelope = new TransactionScope()) { // Include all relevant ADO.NET commands here, then envelope.Complete(); } Visual Basic Using envelope As TransactionScope = New TransactionScope() ' Include all relevant ADO.NET commands here, then envelope.Complete() End Using For those times when you want to specifically exclude a connection (and its associated com- mands) from the active TransactionScope, add “Enlist=False” to the connection string. Data Source=MyServer;Integrated Security=True; Initial Catalog=MyDatabase;Enlist=False If you have a connection that is not enlisted in the overall transaction, you can move it into the transaction scope using the connection’s EnlistTransaction method. After a connection is part of a transaction scope, it cannot be delisted. Note When running an application that creates a distributed transaction, you might receive the following error: "MSDTC on server 'servername' is unavailable". This typically indicates that the Distributed Transaction Controller service is not running. Start the service through either the Services applet or the Component Services applet, both of which can be found in the Administrative Tools section of the Windows Control Panel. . ex2.Message); } } 198 Microsoft ADO. NET 4 Step by Step Visual Basic Try envelope.Commit() Catch ex As Exception MessageBox.Show("Error saving data: " & ex.Message) Try envelope.Rollback() . exception in the subsequent catch block. 200 Microsoft ADO. NET 4 Step by Step 7. Just after the “Do a rollback instead” comment, within the inner try block, add the fol- lowing line: envelope.Rollback(); This. 196 Microsoft ADO. NET 4 Step by Step Instead of creating instances of SqlTransaction directly, you generate connection-specific transactions using the SqlConnection