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(…)