17 июня 2012 г.

Управление транзакциями в C#


В прошлой статье мы познакомились с таким понятием, как транзакция. Было рассмотрено, каким образом можно управлять транзакциями на уровне SQL Server. Сегодня мы научимся создавать и использовать транзакции из программного кода C#.
В .NET существуют два типа транзакций: локальная транзакция и распределенная транзакция. Локальные транзакции работают в пределах одного соединения к базе данных, а распределенные способны отслеживать команды к нескольким источникам данных одновременно.
Сначала рассмотрим классический пример локальной транзакции на языке C#. В рамках локальной транзакции объявляется несколько команд, внутри одного соединения. После выполнения команд принимается решение, была ли транзакция успешной? Если да – данные фиксируются в базе, в противном случае – транзакция откатывается. Такой подход похож на хранимую процедуру, где, как известно, могут выполняться несколько команд в одной транзакции.

   1:  using (SqlConnection sqlConnection = new SqlConnection("ConnectionString"))
   2:  {
   3:      SqlTransaction transaction = 
   4:              sqlConnection.BeginTransaction(IsolationLevel.ReadCommitted);
   5:                  
   6:      SqlCommand command1 = new SqlCommand();
   7:      command1.CommandText = "INSERT INTO A VALUES (1, 'abc');";
   8:      command1.Connection = sqlConnection;
   9:      command1.Transaction = transaction;
  10:   
  11:      SqlCommand command2 = new SqlCommand();
  12:      command2.CommandText = "INSERT INTO B VALUES (1, 2, 3);";
  13:      command2.Connection = sqlConnection;
  14:      command2.Transaction = transaction;
  15:   
  16:      try
  17:      {
  18:          sqlConnection.Open();        
  19:          command1.ExecuteNonQuery();
  20:          command2.ExecuteNonQuery();
  21:   
  22:          transaction.Commit();
  23:      }
  24:      catch
  25:      {
  26:          transaction.Rollback();
  27:      }
  28:  }


В данном примере открывается соединение к базе данных, из которого мы получаем объект транзакции. Команда sqlConnection.BeginTransaction транслируется в команду SQL Server (BEGIN TRANSACTION). Параметр команды принимает уровень изоляции транзакции (полное описание изоляционных уровней смотри в предыдущей статье).
Далее мы создаем две команды с операциями вставки и устанавливаем им текущую транзакцию. После этого в блоке try-catch мы пытаемся выполнить обе команды и зафиксировать транзакцию. Если в процессе выполнения одной из команд возникнет исключение - обе команды будут отменены.
Напомню, что локальная транзакция может работать только в пределах одного соединения с базой данных. Если попробовать открыть несколько соединений и присвоить их командам одну транзакцию – будет сгенерировано исключение.
Для реализации задачи мониторинга нескольких соединений в .NET существуют распределенные транзакции. Рассмотрим пример такой транзакции:
 
   1:  using (TransactionScope distibutedTransaction = new TransactionScope())
   2:  {
   3:      using (SqlConnection sqlConnection1 = new SqlConnection("ConnectionString1"))
   4:      {
   5:          SqlCommand command1 = new SqlCommand();
   6:          command1.CommandText = "INSERT INTO A VALUES (1, 'abc');";
   7:          command1.Connection = sqlConnection1;
   8:   
   9:          command1.Connection.Open();
  10:          command1.ExecuteNonQuery();
  11:      }
  12:   
  13:      using (SqlConnection sqlConnection2 = new SqlConnection("ConnectionString2"))
  14:      {
  15:          SqlCommand command2 = new SqlCommand();
  16:          command2.CommandText = "INSERT INTO B VALUES (1, 2, 3);";
  17:          command2.Connection = sqlConnection2;
  18:   
  19:          command2.Connection.Open();
  20:          command2.ExecuteNonQuery();
  21:      }
  22:   
  23:      distibutedTransaction.Complete();
  24:  }

Как вы можете заметить, синтаксис использования распределенной транзакции немного отличается от кода локальной транзакции.
Сначала создается объект распределенной транзакции. В данном случае уже нет необходимости явно назначать каждой команде транзакцию. Объект TransactionScope будет отслеживать все соединения и команды, объявленные в коде программы до вызова метода Complete(). При возникновении исключения в любой команде вся распределенная транзакция будет отменена автоматически.
Распределенная транзакция доступна для всех провайдеров, которые реализуют интерфейс ITransaction.
Метод Complete() объекта TransactionScope говорит системе, что все операции в рамках данной распределенной транзакции завершены, однако это метод не фиксирует команды транзакции! Фиксация происходит при ликвидации объекта TransactionScope.
Если убрать команду using в коде выше, то данные транзакции будут зафиксированы только при обработке объекта TransactionScope сборщиком мусора, и до этого момента соединения с базами данных будут оставаться открытыми. Такая ситуация может крайне негативно отразится на производительности приложения. Поэтому рекомендуется использовать объект TransactionScope в директиве using, либо явно вызывать у него метод Dispose().
Если же не вызвать метод Complete() вовсе, то система будет думать, что транзакция выполнилась с ошибками и в любом случае будет выполнен ее откат.
В примере выше, распределенная транзакция используется для контроля двух команд к разным базам данных Microsoft SQL Server. Однако, провайдер доступа к базе данных не имеет значения. Вы можете использовать распределенную транзакцию для команд к базам данных Oracle, MySQl и т.д.
Рассмотрим пример распределенной транзакции при участии соединений с разными система управления базами данных:
 
   1:  using (TransactionScope distibutedTransaction = new TransactionScope())
   2:  {
   3:      using (SqlConnection MSSqlConnection = new SqlConnection("ConnectionString1"))
   4:      {
   5:          SqlCommand command = new SqlCommand();
   6:          command.CommandText = "INSERT INTO A VALUES (1, 'abc');";
   7:          command.Connection = MSSqlConnection;
   8:   
   9:          command.Connection.Open();
  10:          command.ExecuteNonQuery();
  11:      }
  12:   
  13:      using (OracleConnection oracleConnection = new OracleConnection("ConnectionString2"))
  14:      {
  15:          OracleCommand command = new OracleCommand();
  16:          command.CommandText = "INSERT INTO B VALUES (1, 2, 3);";
  17:          command.Connection = oracleConnection;
  18:   
  19:          command.Connection.Open();
  20:          command.ExecuteNonQuery();
  21:      }
  22:   
  23:      distibutedTransaction.Complete();
  24:  }

В данном примере внутри транзакции находятся команды к базам данным Microsoft SQL и Oracle. (код исключительно демонстрационный – такое соединение с Oracle больше не поддерживается в .NET Framework – необходимо использовать сторонние провайдеры)
В данной статье мы познакомились со средствами управления транзакциями баз данных из программного кода .NET приложения на языке C#.

1 комментарий:

  1. Евгений, всё конечно хорошо, но первый же твой листинг выкинет исключение InvalidOperationException при вызове функции BeginTransactioin(). Так как у тебя подключение закрыто. Ты листинги хоть проверяй перед публикацией.

    ОтветитьУдалить