Základní .NET pattern pro volání transakcí je poměrně jednoduchý a známý, ostatně je uveden i jako example v MSDN/SDK dokumentaci:
private static void ExecuteSqlTransaction(string connectionString)
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlCommand command = connection.CreateCommand();
SqlTransaction transaction;
// Start a local transaction.
transaction = connection.BeginTransaction("SampleTransaction");
// Must assign both transaction object and connection
// to Command object for a pending local transaction
command.Connection = connection;
command.Transaction = transaction;
try
{
command.CommandText =
"Insert into Region (RegionID, RegionDescription) VALUES (100, 'Description')";
command.ExecuteNonQuery();
command.CommandText =
"Insert into Region (RegionID, RegionDescription) VALUES (101, 'Description')";
command.ExecuteNonQuery();
// Attempt to commit the transaction.
transaction.Commit();
Console.WriteLine("Both records are written to database.");
}
catch (Exception ex)
{
Console.WriteLine("Commit Exception Type: {0}", ex.GetType());
Console.WriteLine(" Message: {0}", ex.Message);
// Attempt to roll back the transaction.
try
{
transaction.Rollback();
}
catch (Exception ex2)
{
// This catch block will handle any errors that may have occurred
// on the server that would cause the rollback to fail, such as
// a closed connection.
Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType());
Console.WriteLine(" Message: {0}", ex2.Message);
}
}
}
}
Po chvilce práce s transakcemi nás však začne trápit, že se poměrně značná část zdrojového kódu neustále opakuje a vlastní výkonné jádro ve změti řádek zaniká.
Jak by se Vám líbil následující způsob volání transakcí?
int myID = 5;
object result;
SqlDataAccess.ExecuteTransaction(
delegate(SqlTransaction transaction)
{
// uvnitř lze používat i lokální proměnné (samozřejmě i parametry, statické fieldy atp.)
SqlCommand cmd1 = new SqlCommand("command string");
cmd1.Transaction = transaction;
cmd1.Connection = transaction.Connection;
cmd1.Parameters.AddWithValue("@MyID", myID);
cmd1.ExecuteNonQuery();
SqlCommand cmd2 = new SqlCommand("another command");
cmd2.Transaction = transaction;
cmd2.Connection = transaction.Connection;
result = cmd2.ExecuteScalar();
});
Líbí? A přitom to není nic složitého, stačí využít delegátů a anonymních metod…
/// <summary>
/// Reprezentuje metodu, která vykonává jednotlivé kroky transakce.
/// </summary>
/// <param name="transaction">transakce, v rámci které mají být jednotlivé kroky vykonány</param>
public delegate void SqlTransactionDelegate(SqlTransaction transaction);
/// <summary>
/// Třída SqlDataAccess nám pomocí statických metod usnadňuje práci s SQL serverem.
/// </summary>
public static class SqlDataAccess
{
/// <summary>
/// Vykoná požadované kroky v rámci transakce.
/// Je spuštěna a commitována nová samostatná transakce.
/// </summary>
public static void ExecuteTransaction(SqlTransactionDelegate transactionWork)
{
ExecuteTransaction(transactionWork, null);
}
/// <summary>
/// Vykoná požadované kroky v rámci transakce.
/// Pokud je zadaná transakce <c>null</c>, je vytvořena, spuštěna a commitována nová.
/// Pokud zadaná transakce není <c>null</c>, jsou zadané kroky pouze v rámci transakce vykonány.
/// </summary>
/// <param name="transaction">transakce (vnější)</param>
public static void ExecuteTransaction(SqlTransactionDelegate transactionWork, SqlTransaction transaction)
{
SqlTransaction currentTransaction = transaction;
SqlConnection connection;
if (transaction == null)
{
// otevření spojení, pokud jsme iniciátory transakce
connection = SqlDataAccess.GetConnection(); // ponechávám na Vaší implementaci
connection.Open();
currentTransaction = connection.BeginTransaction();
}
else
{
connection = currentTransaction.Connection;
}
try
{
transactionWork(currentTransaction);
if (transaction == null)
{
// commit chceme jen v případě, že nejsme uvnitř vnější transakce
currentTransaction.Commit();
}
}
catch
{
try
{
currentTransaction.Rollback();
}
catch
{
// chceme vyhodit vnější výjimku, ne problém s rollbackem
}
throw;
}
finally
{
// uzavření spojení, pokud jsme iniciátory transakce
if (transaction == null)
{
connection.Close();
}
}
}
}
Jenom dodávám, že druhý overload umožňuje mimo vytvoření transakce nové (pokud je parametr transaction = null) i spuštění celé operace v rámci rozlehlejší transakce vnější, což může v reálu vypada nějak takto:
public class Order
{
...
public void Save(SqlTransaction transaction)
{
SqlDataAccess.ExecuteTransaction(
delegate(SqlTransaction currentTransaction)
{
this.DoSave(currentTransaction);
OrderItems.SaveAll(currentTransaction);
Customer.Save(currentTransaction);
}, transaction);
}
}
Implementace v HAVIT .NET Framework Extensions:
Havit.Data.SqlClient.SqlTransactionDelegate(…)
Havit.Data.SqlClient.SqlDataAccess.ExecuteTransaction(…)