Транзакции баз данных

85

Транзакция — это набор операций в базе данных, которые должны быть либо все выполнены, либо все не выполнены. Транзакции применяются для обеспечения безопасности, верности и непротиворечивости данных в таблице.

Транзакции очень важны тогда, когда при работе с базой данных требуется взаимодействие с несколькими таблицами или несколькими хранимыми процедурами (или с сочетанием неделимых объектов базы данных). Классический пример транзакции — процесс перевода денежных средств с одного банковского счета на другой. Например, если вам понадобилось перевести $500 с депозитного счета на текущий счет, то нужно выполнить в режиме транзакции следующие шаги:

Вряд ли вам бы понравилось, если бы деньги были сняты с депозитного счета, но не переведены (из-за какой-то банковской ошибки) на текущий счет. Но если эти шаги упаковать в транзакцию базы данных, то СУБД гарантирует, что все взаимосвязанные шаги будут выполнены как единое целое. Если любая часть транзакции выполнится неудачно, то будет произведен откат (rollback) всей транзакции в исходное состояние. А если все шаги будут выполнены успешно, то транзакция будет зафиксирована (committed).

Если вы уже читали о транзакциях, то, возможно, вам встречалось сокращение ACID. Оно означает четыре ключевых свойства классической транзакции: атомарность (Atomic — все или ничего), целостность (Consistent — на протяжении транзакции данные остаются в непротиворечивом состоянии), изолированность (Isolated — транзакции не мешают одна другой) и устойчивость (Durable — транзакции сохраняются и протоколируются).

Оказывается, в платформе .NET есть несколько способов поддержки транзакций. Здесь мы рассмотрим объект транзакции для поставщика данных ADO.NET (SqlTransaction в случае System.Data.SqlClient). Библиотеки базовых классов ADO.NET также обеспечивают поддержку транзакций в многочисленных API-интерфейсах, которые перечислены ниже:

System.EnterpriseServices

Это пространство имен (из сборки System.EnterpriseServices.dll) содержит типы, позволяющие выполнить интеграцию с уровнем времени выполнения СОМ+, в том числе и поддержку распределенных транзакций.

System.Transactions

Это пространство имен (из сборки System.Transactions.dll) содержит классы, позволяющие писать собственные транзакционные приложения и диспетчеры ресурсов для различных служб (MSMQ, ADO.NET, СОМ+ и т.д.).

Windows Communication Foundation

WCF API предоставляет службы для работы с транзакциями с различными классами распределенного связывания.

Windows Workflow Foundations

WF API предоставляет транзакционную поддержку для рабочих потоков.

Кроме встроенной поддержки транзакций в библиотеках базовых классов .NET, можно пользоваться и возможностями языка SQL используемой СУБД. Например, можно написать хранимую процедуру, в которой задействованы операторы BEGIN TRANSACTION, ROLLBACK и COMMIT.

Основные члены объекта транзакции ADO.NET

Типы для работы с транзакциями существуют во всех библиотеках базовых классов, но мы будем рассматривать объекты транзакции, которые имеются в поставщиках данных ADO.NET — все они порождены от DBTransaction и реализуют интерфейс IDbTransaction. Вспомните, что IDbTransaction определяет ряд членов:

public interface IDbTransaction : IDisposable
{
   IDbConnection Connection { get; }
   IsolationLevel IsolationLevel { get; }
   void Commit();
   void Rollback();
}

Обратите внимание на свойство Connection, которое возвращает ссылку на объект подключения, инициировавший данную транзакцию (как мы увидим, объект транзакции можно получить от данного объекта подключения). Метод Commit() вызывается, если все операции в базе данных завершились успешно. При этом все ожидающие изменения фиксируются в хранилище данных. А метод Rollback() можно вызвать при возникновении исключения времени выполнения, чтобы сообщить СУБД, что все ожидающие изменения следует отменить и оставить первоначальные данные без изменений.

Свойство IsolationLevel объекта транзакции позволяет указать степень защиты транзакции от действий параллельных транзакций. По умолчанию транзакции полностью изолируются до их фиксации. Полную информацию о значениях перечисления IsolationLevel можно найти в документации по .NET Framework 4.0 SDK.

Кроме членов, определенных интерфейсом IDbTransaction, в типе SqlTransaction определен дополнительный член Save(), который предназначен для определения точек сохранения (save point). Эта концепция позволяет откатить неудачную транзакцию до указанной точки, не выполняя откат всей транзакции. При вызове метода Save() с помощью объекта SqlTransaction можно задать произвольный строковый псевдоним.

А при вызове Rollback() можно указать этот псевдоним в качестве аргумента, чтобы выполнить частичный откат (partial rollback). При вызове Rollback() без аргументов будут отменены все ожидающие изменения.

Добавление таблицы CreditRisks в базу данных AutoLot

А теперь рассмотрим, как можно использовать транзакции в ADO.NET. Вначале откройте окно Server Explorer из Visual Studio 2010 и добавьте в базу данных AutoLot новую таблицу с именем CreditRisks, которая содержит точно такие же столбцы, что и таблица Customers, созданная ранее: CustID (первичный ключ), FirstName и LastName. Эта таблица предназначена для отсеивания нежелательных клиентов с плохой кредитной историей. После добавления новой таблицы в диаграмму базы данных AutoLot ее реализация будет такой:

Новая диаграмма таблиц

Как и предыдущий пример с пересылкой денег с депозита на текущий счет, этот пример, где подозрительный клиент перемещается из таблицы Customers в таблицу CreditRisks, должен работать под недремлющим оком транзакционной области (ведь вы хотите запомнить идентификаторы и имена некредитоспособных клиентов). А именно, необходимо гарантировать, что либо будет выполнено успешное удаление текущих кредитных рисков из таблицы Customers с последующим их добавлением в таблицу CreditRisks, либо ни одна из этих операций не будет выполнена.

В производственной среде вам не понадобится создавать совершенно новую таблицу базы данных для подозрительных клиентов. Достаточно добавить в таблицу Customers логический столбец IsCreditRisk. Эта новая таблица предназначена просто для опробования работы с простыми транзакциями.

Добавление метода транзакции в InventoryDAL

А теперь посмотрим, как работать с транзакциями ADO.NET программным образом. Откройте созданный ранее проект AutoLotDAL Code Library и добавьте в класс InventoryDAL новый общедоступный метод ProcessCreditRisk(), предназначенный для работы с кредитными рисками (в данном примере для простоты не используется параметризованный запрос, но в производственном методе его следует задействовать):

public void ProcessCreditRisk(bool throwEx, int custId)
        {
            // Выборка имени по идентификатору клиента
            string fName = string.Empty;
            string lName = string.Empty;
            SqlCommand cmdSelect = 
                new SqlCommand(string.Format("Select * from Customers where CustID = {0}", custId), connect);
            using (SqlDataReader dr = cmdSelect.ExecuteReader())
            {
                if (dr.HasRows)
                {
                    dr.Read();
                    fName = (string)dr["FirstName"];
                    lName = (string)dr["LastName"];
                }
                else return;
            }

            // Создание объектов команд для каждого шага операции.
            SqlCommand cmdRemove = new SqlCommand(
                string.Format("Delete from Customers where CustID = {0}", custId), connect);
            SqlCommand cmdInsert = new SqlCommand(string.Format("Insert Into CreditRisks" + "(CustID, FirstName, LastName) Values" +
                "({0}, '{1}', '{2}')", custId, fName, lName), connect);
            // Получаем из объекта подключения.
            SqlTransaction tx = null;
            try
            {
                tx = connect.BeginTransaction();
                // Включение команд в транзакцию
                cmdInsert.Transaction = tx;
                cmdRemove.Transaction = tx;
                // Выполнение команд.
                cmdInsert.ExecuteNonQuery();
                cmdRemove.ExecuteNonQuery();
                // Имитация ошибки.
                if (throwEx)
                {
                    throw new ApplicationException("Ошибка базы данных! Транзакция завершена неудачно.");
                }
                // Фиксация.
                tx.Commit();
             }
             catch (Exception ex)
             {
                 Console.WriteLine(ex.Message);
                 // При возникновении любой ошибки выполняется откат транзакции.
                 tx.Rollback();
             }

        }

Здесь используется входной параметр типа bool, который указывает, нужно ли генерировать произвольное исключение при попытке обработки нежелательного клиента. Это позволит имитировать непредвиденные ситуации, которые могут привести к неудачному завершению транзакции. Понятно, что здесь это сделано лишь в демонстрационных целях; в реальности метод транзакции не должен позволять вызывающему процессу разрушать всю логику по своему усмотрению!

Мы используем два объекта SqlCommand, представляющие каждый шаг предстоящей транзакции. После получения имени и фамилии клиента по входному параметру custID с помощью метода BeginTransaction() объекта подключения получаем нужный объект SqlTransaction. Если этого не сделать, логика вставки/удаления не будет выполняться в транзакционном контексте.

Если (и только если) значение логического параметра равно true, то после вызова ExecuteNonQuery() для обеих команд генерируется исключение. В этом случае все ожидающие подтверждения операции базы данных аннулируются. Если исключение не было сгенерировано, оба шага фиксируются в таблицах базы данных при вызове Commit(). Скомпилируйте измененный проект AutoLotDAL и проверьте, нет ли ошибок.

Пройди тесты
Лучший чат для C# программистов