Playing with transactions for the first time I thought I'd get the following code to work:
namespace database
{
class Program
{
static string connString = "Server=ServerName;Database=Demo;Trusted_Connection=True;";
SqlConnection connection = new SqlConnection(connString);
static Random r = new Random();
static void Add()
{
try
{
Thread.Sleep(r.Next(0, 10));
using (var trans = new TransactionScope())
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
Thread.Sleep(r.Next(0, 10));
SqlCommand cmd = new SqlCommand("update bank set balance = " + ++count + "where owner like '%Jan%'", conn);
cmd.ExecuteNonQuery();
}
trans.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
static void Remove()
{
try
{
Thread.Sleep(r.Next(0, 10));
using (var trans = new TransactionScope())
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
Thread.Sleep(r.Next(0, 10));
SqlCommand cmd = new SqlCommand("update bank set balance = " + --count + "where owner like '%Jan%'", conn);
cmd.ExecuteNonQuery();
}
trans.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(new ThreadStart(Add));
t.Start();
}
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(new ThreadStart(Remove));
t.Start();
}
Console.ReadLine();
}
}
}
I assumed that at the end after 100 adds and 100 subtractions my balane would be the same as my starting point - 100, however it keeps changing up and down every time I run the script. Even with isolationlevel serializable. Could anyone tell me why? O_o
EDIT: Moved connection opening and closing to inside the transaction scope.
The problem now is that I get "Transaction (Process ID XX) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction"
Like Marc Gravell Said:
Putting the connection inside the transaction scope and adding UPDLOCK to the select query combined with changing isolationlevel to repeatableRead did the trick :)
static void Add()
{
try
{
Thread.Sleep(r.Next(0, 10));
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.RepeatableRead }))
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
Thread.Sleep(r.Next(0, 10));
SqlCommand cmd = new SqlCommand("update bank set balance = " + ++count + "where owner like '%Jan%'", conn);
cmd.ExecuteNonQuery();
}
trans.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
1: currently the TransactionScope might be redundant and unused; try changing the transaction to wrap the connection, not the other way around (oh, and use using):
using (var trans = new TransactionScope(TransactionScopeOption.Required,
new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
using (var conn = new SqlConnection(connString))
{
conn.Open();
//...
trans.Complete();
}
this way, the connection should enlist correctly inside the transaction (and be cleaned up properly if something bad happens)
I think the above is the main problem; i.e. not enlisting in the transaction. That means that there can be lost changes, since the read/write operation is not actually being raised to a higher isolation level.
2: however, if you do that by itself, I expect you'll see deadlocks. To avoid deadlocks, if you know you're going to update, you might want to use (UPDLOCK) on that select - this will take a write lock at the start, so that if there is a competing thread you get a block rather than a deadlock.
To be clear, this deadlock scenario is caused by:
thread A reads the row, getting a read lock
thread B reads the row, getting a read lock
thread A tries to update the row, and is blocked by B
thread B tries to update the row, and is blocked by A
Adding the UPDLOCK, this becomes:
thread A reads the row, getting a write lock
thread B tries to read the row, and is blocked by A
thread A updates the row
thread A completes the transaction
thread B is able to continue, reads the row, getting a write lock
thread B updates the row
thread B completes the transaction
3: but querying to do a trivial update is silly; better just to issue an in-place update without selecting, i.e. update bank set balance = balance + 1 where ...
You have to open the connection inside the TransactionScope block.
Instead of
var conn = new SqlConnection(connString);
conn.Open();
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
{
// do stuff
}
use it like this
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
using (var conn = new SqlConnection(connString))
{
conn.Open();
// do stuff
}
This way opening the connection automatically enlists it in the TransactionScope as a lightweight transaction.
You can always look at the examples.
Related
I'm trying to figure out how to setup my database access correctly while using the SqlClient hitting a Microsoft SQL Server. For the most part it is working, but there's a particular scenario that is giving me trouble. Namely: attempting to simultaneously use two connections in the same thread; one with an open data reader and the other performing a delete operation.
The following code demonstrates my conundrum:
public class Database {
...
internal SqlConnection CreateConnection() => new SqlConnection(connectionString);
...
}
public IEnumerable<Model> GetModel() {
var cmd = new SqlCommand() { ... };
using(var conn = db.CreateConnection()) {
conn.Open();
cmd.Connection = conn;
using(var reader = cmd.ExecuteReader()) {
while(reader.Read()) {
var m = new Model();
// deserialization logic
yield return m;
}
}
}
}
public void Delete(int id) {
var cmd = new SqlCommand() { ... }
using(var conn = db.CreateConnection()) {
conn.Open(); // throwing the error here
cmd.Connection = conn;
cmd.ExecuteNonQuery();
}
}
Application Code:
using(var scope = new TransactionScope()) {
var models = GetModels();
foreach(var m in models) {
Delete(m.Id); // throws an exception
}
scope.Complete();
}
For whatever reason, an exception is thrown by the above code while trying to execute the Delete operation:
quote
System.Transactions.TransactionAbortedException: The transaction has aborted. ---> System.Transactions.TransactionPromotionException: Failure while attempting to promote transaction. ---> System.Data.SqlClient.SqlException: There is already an open DataReader associated with this Command which must be closed first. ---> System.ComponentModel.Win32Exception: The wait operation timed out
quote
Now, I have confirmed that if I either set MultipleActiveResultSets=true or Pooling=false on the ConnectionString, that then the above application code will work without error. However, it doesn't seem like I should need to set either of those. If I open two connections simultaneously, should they not be separate connections? Why then am I getting an error from the Delete connection saying that there's an open DataReader?
Please help <3
By far the easiest fix here is to simply load all the models outside the transaction before you go deleting any. Eg
var models = GetModels().ToList();
using(var scope = new TransactionScope()) {
foreach(var m in models) {
Delete(m.Id); // throws an exception
}
scope.Complete();
}
Even fetching the models inside the transaction shold work
using(var scope = new TransactionScope()) {
var models = GetModels().ToList();
foreach(var m in models) {
Delete(m.Id); // throws an exception
}
scope.Complete();
}
so long as you don't leave the connection open during the iteration. If you allow the connection in GetModels() to close, it will be returned to the connection pool, and be available for use for subsequent methods that are enlisted in the same transaction.
In the current code the connection in GetModels() is kept open during the foreach loop and Delete(id) has to open a second connection and try to create a distributed transaction, which is failing.
Without MultipleActiveResultsets, the GetModels connection can't be promoted to a distributed transaction in the middle of returning query results. Setting pooling=false will not make this error go away.
Here's a simplified repro to play with:
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
using System.Transactions;
namespace SqlClientTest
{
class Program
{
static void Main(string[] args)
{
Setup();
var topt = new TransactionOptions();
topt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (new TransactionScope(TransactionScopeOption.Required, topt ))
{
foreach (var id in GetIds())
{
Delete(id);
}
}
}
static string constr = #"server=.;database=tempdb;Integrated Security=true;TrustServerCertificate=true;";
public static void Setup()
{
using (var con = new SqlConnection(constr))
{
con.Open();
var cmd = con.CreateCommand();
cmd.CommandText = "drop table if exists ids; select object_id id into ids from sys.objects";
cmd.ExecuteNonQuery();
}
}
public static IEnumerable<int> GetIds()
{
using (var con = new SqlConnection(constr))
{
con.Open();
var cmd = con.CreateCommand();
cmd.CommandText = "select object_id id from sys.objects";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
yield return reader.GetInt32(0);
}
}
}
}
public static void Delete(int id)
{
using (var con = new SqlConnection(constr))
{
con.Open();
var cmd = con.CreateCommand();
cmd.CommandText = "insert into ids(id) values (#id)";
cmd.Parameters.Add(new SqlParameter("#id", id));
cmd.ExecuteNonQuery();
}
}
}
}
And here's what Profiler shows when run:
The main reason here as far as I understand is your yielding iteration.
So the DB connection has not yet called disposed as it's still being used in your iteration (foreach). If for example, you called .ToList() at that point it should return all the entries and then dispose of the connection.
See here for a better explanation on how yield works in an iteration: https://stackoverflow.com/a/58449809/3329836
I am trying to make multiple parallel updates to database using Task.WhenAll. The code flow goes like this.
In the main method, i have created a transaction scope and created clones of the main transactions and passed to the child. Main transactions is blocked until child is completed
using (var scope = DalcHelper.GetTransactionScope())
{
DependentTransaction transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
var task1= Dalc.UpdateDetails1(transaction );
DependentTransaction transaction1 = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
var task2 = Dalc.UpdateDetails2(transaction1);
await Task.WhenAll(task1, task2 ).ConfigureAwait(false);
scope.Complete();
}
The DalcMethod goes like this. Here the clone created from the outer transaction goes as a parameter. The dependent transaction is completed notifying the main transaction that the dependent is completed
try
{
using (SqlCommand databaseCommand = DalcHelper.GetCommand(SPName))
using (var scope = new TransactionScope(dependentCloneTransaction, TransactionScopeAsyncFlowOption.Enabled))
{
-- Update database
scope.Complete();
}
}
finally
{
//Call complete on the dependent transaction
dependentCloneTransaction.Complete();
}
Dalc methods are asynchronous methods which returns Task
I am getting the below exception
The transaction has aborted.Failure while attempting to promote transaction.There is already an open DataReader associated with this Command which must be closed first.The wait operation timed out
. Can anyone tell me what am i doing wrong here?
namespace Playground
{
static class DalcHelper
{
public static TransactionScope GetTransactionScope()
{
return new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
}
public async static Task ReadDetails1(DependentTransaction transaction,SqlConnection conn)
{
try
{
string commandText = "SELECT * FROM dbo.Persons"; // some table, say Persons
using (SqlCommand cmd = new SqlCommand(commandText, conn))
{
cmd.CommandType = System.Data.CommandType.Text;
SqlDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (reader.Read())
{
int Id = reader.GetInt32("Id");
Console.WriteLine("Id " + Id);
}
reader.Close();
}
transaction.Complete();
return;
}
catch (Exception ex)
{
Console.WriteLine("Task 1"+ ex.Message);
}
}
public async static Task ReadDetails2(DependentTransaction transaction1, SqlConnection conn)
{
try
{
string commandText = "SELECT * FROM dbo.Persons";
using (SqlCommand cmd = new SqlCommand(commandText, conn))
{
cmd.CommandType = System.Data.CommandType.Text;
SqlDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (reader.Read())
{
int age = reader.GetInt32("Age");
Console.WriteLine("Age " + age);
}
reader.Close();
}
transaction1.Complete();
return;
}
catch (Exception ex)
{
Console.WriteLine("Task 2" + ex.Message);
}
}
}
class Program
{
static void Main(string[] args)
{
string connectionString = "YourConnectionString";
_ = RunMe(connectionString);
}
private async static Task RunMe(string connectionString)
{
try
{
Task task1 = Task.Run( async()=> {
using (TransactionScope scope = DalcHelper.GetTransactionScope())
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
DependentTransaction transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
conn.Open();
await DalcHelper.ReadDetails1(transaction, conn);
/*
* add more tasks if you wish to
*/
Console.WriteLine("Completed task 1");
conn.Close();
}
scope.Complete();
}
});
Task task2 = Task.Run(async () =>
{
using (TransactionScope scope = DalcHelper.GetTransactionScope())
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
DependentTransaction transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
conn.Open();
await DalcHelper.ReadDetails2(transaction, conn);
/*
may be update some column of table based on previous op.
// await DalcHelper.UpdateDetails2(transaction, conn);
*/
Console.WriteLine("Completed task 2");
conn.Close();
}
/*
calling `Complete` method will commit all the changes within the transaction scope(including the UpdateDetails2 method)
need not dispose transaction scope explicitly, `using` block takes care of that
*/
scope.Complete();
}
});
await Task.WhenAll(task1, task2);// at this point every task added is complete
Console.WriteLine("completed both tasks");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
Some important points to remember when working with transaction scope
It is required to dispose TransactionScope within the same thread it was created or might get an error thrown like Transaction already aborted.
Any update operations is persisted only if TransactionScope.Complete() method is called.
Make sure you open individual connections for each thread and close it after its usage.That being said, I am not sure from a performance standpoint about using an individual connection for each thread. I am happy to get educated more on this and I'll update my answer. However, this solution should help you solve your problem.
Do read some useful answers already posted related to the topic
Dependent transaction
Transaction aborted issues
Thread based connection
I am using gridview to select multiple records and then play loop over it to individually send each record ID to database and update it but I don't find it very good way to implement because it opens and close connection each time so what is the good way ?
foreach (GridViewRow r in grdViewLastHearingDates.Rows)
{
int CaseHearingID = Convert.ToInt32(r.Cells[0].Text);
CheckBox chkBox = r.FindControl("chkBoxIsConveyed") as CheckBox;
TextBox txtboxConvenienceRemarks = r.FindControl("txtBoxConvenienceRemarks") as TextBox;
string ConvenienceRemarks = txtboxConvenienceRemarks.Text;
MngCaseHearings.UpdateCasesIsConveyed(CaseHearingID, ConvenienceRemarks, chkBox.Checked);
}
MngCaseHearings.UpdateCasesIsConveyed sends and executes this code every time for updating each ID. Please review and give suggestions
public Boolean UpdateCasesIsConveyed(int CaseHearingID, string ConvenienceRemarks, bool IsConveyed)
{
try
{
SqlCommand SqlCom = new SqlCommand("UpdateCasesIsConveyed", DatabaseConnection.OpenConnection());
SqlCom.CommandType = CommandType.StoredProcedure;
SqlCom.Parameters.AddWithValue("#pk_CaseHearings_ID ", CaseHearingID);
SqlCom.Parameters.AddWithValue("#IsConveyed", IsConveyed);
SqlCom.Parameters.AddWithValue("#ConvenienceRemarks", ConvenienceRemarks);
SqlParameter SqlParamReturnStatus = new SqlParameter("#ReturnStatus", SqlDbType.Bit);
SqlCom.Parameters.Add(SqlParamReturnStatus);
SqlParamReturnStatus.Direction = ParameterDirection.Output;
SqlParameter SqlParamReturnStatusMessage = new SqlParameter("#ReturnStatusMessage", SqlDbType.VarChar, -1);
SqlCom.Parameters.Add(SqlParamReturnStatusMessage);
SqlParamReturnStatusMessage.Direction = ParameterDirection.Output;
SqlCom.ExecuteNonQuery();
string ReturnStatusMessage = Convert.ToString(SqlParamReturnStatusMessage);
Boolean ReturnStatus = Convert.ToBoolean(SqlParamReturnStatus.Value);
return ReturnStatus;
}
catch (Exception)
{
throw;
}
finally
{
DatabaseConnection.CloseConnection();
}
this would waste the resources so if any good work around ?
Since the SqlConnection and SqlCommand objects both implement IDisposable you should probably dispose of them when you're done. The simplest way would be via the using statement:
using (var conn = DatabaseConnection.OpenConnection())
using (var SqlCom = new SqlCommand("UpdateCasesIsConveyed", conn))
{
// setup and execute the SP, can return from in here
}
This will ensure that the resources used by the objects are properly closed as soon as you are done. While this isn't strictly necessary - the Dispose method will be called when the garbage collector destroys the objects - it will ensure that you aren't holding open the database objects any longer than necessary. Depending on how often this is called you can end up with resource shortages on the SQL server, excessive handle usage, etc.
In general anything that implements IDisposable should be disposed as soon as practical.
(Sorry, missed the loop part of the question)
This type of create/destroy cycle is fine for single operations, but becomes wasteful when used to update a lot of records. I would put the loop in the middle of the code rather than repeatedly calling this code from outside.
I would create a record class or struct that holds the SP parameters and pass an IEnumerable of that record class to your update method. This way you can do the setup once, process all of the updates, then tear down the database objects after all of the changes have been made. Throw a transaction in too so you can undo it all if one of the records fails.
Something like:
public struct UpdateCaseConveyanceRec
{
public int CaseHearingID;
public string ConvenienceRemarks;
public bool IsConveyed;
}
public bool UpdateCasesIsConveyed(IEnumerable<UpdateCaseConveyanceRec> uopdates)
{
using (SqlConnection conn = DatabaseConnection.OpenConnection())
using (SqlCommand cmd = new SqlCommand("UpdateCasesIsConveyed", conn))
using (SqlTransaction trans = conn.BeginTransaction("UpdateCasesIsConveyed"))
{
cmd.CommandType = CommandType.StoredProcedure;
var pID = cmd.Parameters.Add("#pk_CaseHearings_ID", SqlDbType.Int);
var pConveyed = cmd.Parameters.Add("#IsConveyed", SqlDbType.Bit);
var pRemarks = cmd.Parameters.Add("#ConvenienceRemarks", SqlDbType.VarChar, -1);
var retStatus = cmd.Parameters.Add("#ReturnStatus", SqlDbType.Bit);
retStatus.Direction = ParameterDirection.Output;
var retStatusMsg = cmd.Parameters.Add("#ReturnStatusMessage", SqlDbType.VarChar, -1);
retStatusMsg.Direction = ParameterDirection.Output;
try
{
foreach (var row in updates)
{
pID.Value = row.CaseHearingID;
pConveyed.Value = row.IsConveyed;
pRemarks.Value = row.ConvenienceRemarks;
cmd.ExecuteNonQuery();
if (!Convert.ToBoolean(retStatus))
{
trans.Rollback();
return false;
}
}
trans.Commit();
}
catch ()
{
trans.Rollback();
throw;
}
return true;
}
}
You can then feed that with a LINQ to Objects query:
var source =
from r in grdViewLastHearingDates.Rows.OfType<GridViewRow>()
select new UpdateCaseConveyanceRec
{
CaseHearingID = Convert.ToInt32(r.Cells[0].Text),
ConvenienceRemarks = (r.FindControl("txtBoxConvenienceRemarks") as TextBox).Text;
IsConveyed = (r.FindControl("chkBoxIsConveyed") as CheckBox).Checked
};
bool updated = UpdateCasesIsConveyed(source);
I am trying to use the Data Entity Framework to create my Data Access Layer. In the work I have done up to now I have used ADO.Net. I am trying to get my head around how transactions work in EF. I ave read loads but its all confussed me even more than I was before!! I would usually do somethink like (simplified for example):
using (SqlConnection conn = new SqlConnection(_connString))
{
using (SqlTransaction trans = conn.BeginTransaction())
{
try
{
using (SqlCommand cmd = new SqlCommand("usp_CreateNewInvoice", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("#InvoiceName", Invoice.invoicename));
cmd.Parameters.Add(new SqlParameter("#InvoiceAddess", Invoice.invoiceaddress));
_invoiceid = Convert.ToInt32(cmd.ExecuteScalar());
}
foreach (InvoiceLine inLine in Invoice.Lines)
{
using (SqlCommand cmd = new SqlCommand("usp_InsertInvoiceLines", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("#InvNo", _invoiceid));
cmd.Parameters.Add(new SqlParameter("#InvLineQty", inLine.lineqty));
cmd.Parameters.Add(new SqlParameter("#InvLineGrossPrice", inLine.linegrossprice));
cmd.ExecuteNonQuery();
}
}
trans.Commit();
}
catch (SqlException sqlError)
{
trans.Rollback();
}
}
}
Now I want to do the same in EF:
using (TransactionScope scope = new TransactionScope())
{
try
{
var DbContext = new CCTStoreEntities();
CCTInvoice invHead = new CCTInvoice();
invHead.Name = Invoice.invoicename;
invHead.Address = Invoice.invoiceaddress;
DbContext.CCTInvoices.Add(invHead);
DbContext.SaveChanges(false);
_iid = invHead.InvoiceId;
foreach (InvoiceLine inLine in Invoice.Lines)
{
CCTInvoiceLine invLine = new CCTInvoiceLine();
invLine.InvoiceNo = _iid;
invLine.Quantity = inLine.lineqty;
invLine.GrossPrice = inLine.linegrossprice;
DbContext.CCTInvoiceLines.Add(invHead);
DbContext.SaveChanges(false);
}
DbContext.SaveChanges();
scope.Complete();
}
catch
{
//Something went wrong
//Rollback!
}
}
From what I read SaveChanges(false) means the changes being made will continue to be tracked. But how do I rollback the transaction if something goes wrong?
You don't need to do anything on your catch block. Just by not calling DbContext.SaveChanges no changes will be sent to the database, and they will be lost once DbContext is disposed.
You do have a problem though. DbContext must be wrapped on a using block as follows to be properly disposed. BTW, I don't think DbContext.SaveChanges(false); is needed, your code should work with just the final DbContext.SaveChanges();. EF will take care of wiring up all your Foreign Keys, so you don't need to do that explicitly.
using (TransactionScope scope = new TransactionScope())
{
try
{
using (var DbContext = new CCTStoreEntities())
{
CCTInvoice invHead = new CCTInvoice();
invHead.Name = Invoice.invoicename;
invHead.Address = Invoice.invoiceaddress;
DbContext.CCTInvoices.Add(invHead);
DbContext.SaveChanges(false); // This is not needed
_iid = invHead.InvoiceId; // This is not needed
foreach (InvoiceLine inLine in Invoice.Lines)
{
CCTInvoiceLine invLine = new CCTInvoiceLine();
invLine.InvoiceNo = _iid; // This is not needed
invLine.Quantity = inLine.lineqty;
invLine.GrossPrice = inLine.linegrossprice;
DbContext.CCTInvoiceLines.Add(invHead);
DbContext.SaveChanges(false); // This is not needed
}
DbContext.SaveChanges();
scope.Complete();
}
}
catch
{
//Something went wrong
//Rollback!
}
}
The rollback mechanism in a TransactionScope is implicit.
Basically, if you don't call Complete before the TransactionScope gets disposed it will automatically rollback. See the Rolling back a transaction section in Implementing an Implicit Transaction using Transaction Scope.
So technically, you don't even need to use a try...catch here (unless you want to perform some other action like logging).
Hi just reading about using transaction scrope, previously I am used to making transactions inside a single DB class like
try
{
con.Open();
tran = con.BeginTransaction();
OleDbCommand myCommand1 = new OleDbCommand(query1, con);
OleDbCommand myCommand2 = new OleDbCommand(query2, con);
myCommand .Transaction = tran;
// Save Master
myCommand1.ExecuteNonQuery();
// Save Childred
myCommand2.ExecuteNonQuery();
// Commit transaction
tran.Commit();
}
catch (OleDbException ex)
{
tran.Rollback();
lblError.Text = "An error occured " + ex.ToString();
}
finally
{
if (con != null)
{
con.Close();
}
}
But now I come to know that I can execute transaction inside the Business Logic layer simply by using a transaction scope object and using separate DB classes like
public static int Save(Employee myEmployee)
{
using (TransactionScope myTransactionScope = new TransactionScope())
{
int RecordId = EmpDB.Save(myEmployee);
foreach (Address myAddress in myEmployee.Addresses)
{
myAddress.EmployeeId = EmployeeId;
AddressDB.Save(myAddress);
}
foreach (PhoneNumber myPhoneNumber in myEmployee.PhoneNumbers)
{
myPhoneNumber.EmployeeId = EmployeeId;
PhoneNumberDB.Save(myPhoneNumber);
}
myTransactionScope.Complete();
return EmployeeId;
}
}
Which one is the recommended coding practice and why ? Is using a Transaction Scope safe ? Is it the latest way to do things ? I am confused about both methods.
Thanks in advance.
One of the nice things about Transaction scope is that you don't need the try/catch block. You just have to call Complete on the scope in order to commit the transaction, and it rollsback automatically if an exception does occur.
You can also use other components that are able to participate in transactions, not just the DB connection. This is because the components (including the connection) look for a Transaction on the current thread. It is this transaction that is created by the call to
using (TransactionScope myTransactionScope = new TransactionScope())