sonyps4.ru

Реализация неявной транзакции с использованием области транзакции. Транзакции

Давным-давно вышел ADO.NET 2.0, а вместе с ним и сборка System.Transactions, содержащая класс TransactionScope - путеводитель в мир легкого и непринужденного использования транзакций. В сегодняшней статье я рассмотрю некоторые нюансы, возникающие при использовании этой дырявой, но такой симпатичной абстракции.

Итак, начиная с ADO.NET 2.0, для того чтобы заключить свой код в транзакцию, разработчику достаточно расположить его внутри блока TransactionScope:

Using (var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }) { //код внутри транзакции transactionScope.Complete(); }

Я использовал в конструкторе наиболее важные параметры - давайте их рассмотрим (в обратном порядке).

IsolationLevel

Вопрос использования или неиспользования RequiresNew и Suppress, безусловно, определяется требованиями алгоритма, но у меня на этот счет есть некоторые предубеждения. Дело в том, что TransactionScope в режимах RequiresNew и Suppress при наличии модифицирующих состояние базы данных операций делает невозможным использование старого трюка, когда код интеграционного теста заключается в транзакцию, которая по окончании теста откатывается, тем самым восстанавливая состояние базы данных:

Public void void IntegrationTest() { using (new TransactionScope()) { //код теста //не вызываем Complete } }

Если в тестируемом коде создаются TransactionScope в режиме Requires, то они подцепятся к тестовому TransactionScope, а значит мы сможем откатить все изменения. Если же в коде есть TransactionScope в режиме RequiresNew или Suppress, то откатить результат их работы из тестового TransactionScope мы не сможем. Стоит отметить, что наличие логики, завязанной на момент коммита транзакции (как в предыдущем примере), тоже делает невозможным использование этого приема.

Напоследок отмечу, что TransactionScope локален по отношению к потоку (потому что его реализация базируется на ThreadStatic-переменной). Если же вам необходимо использовать одну транзакцию из нескольких потоков, - воспользуйтесь классом

Введение

Транзакция - это последовательность операций, выполняемых как единое целое. Благодаря объединению взаимосвязанных операций в транзакцию гарантируется согласованность и целостность данных в системе, несмотря на любые ошибки, которые могли возникнуть в ходе транзакции. Для успешного выполнения транзакции необходимо успешное завершение всех ее операций.

У транзакции есть начало и конец, определяющие ее границы (transaction boundaries), внутри которых транзакция может охватывать различные процессы и компьютеры. Все ресурсы, используемые в ходе данной транзакции, считаются участвующими в этой транзакции. Для поддержания целостности используемых ресурсов транзакция должна обладать свойствами ACID: Atomicity (атомарность), Consistency (целостность), Isolation (изоляция) и Durability (отказоустойчивость). Подробнее об основах обработки транзакций см. Processing Transactions (EN) в Microsoft .NET Framework SDK и Transaction Processing в Microsoft Platform SDK.

В этой статье мы покажем, как выполнять локальные и распределенные транзакции в приложениях Microsoft .NET.

Локальные и распределенные транзакции

Локальной называется транзакция, областью действия которой является один ресурс, поддерживающий транзакции, - база данных Microsoft ® SQL Server™, очередь сообщений MSMQ и др. Например, отдельно взятая СУБД может вводить в действие правила ACID, когда в ее распоряжении имеются все данные, участвующие в транзакции. В SQL Server предусмотрен внутренний диспетчер транзакций (transaction manager), предоставляющий функциональность для фиксации (commit) и отката (rollback) транзакций.

Распределенные транзакции могут использовать гетерогенные ресурсы, поддерживающие транзакции, включать самые разнообразные операции, например выборку информации из базы данных SQL Server, считывание сообщений Message Queue Server и запись в другие базы данных. Программирование распределенных приложений упрощается программным обеспечением, способным координировать фиксацию и откат, а также восстановление данных, хранящихся в различных ресурсах. Одной из таких технологий является DTC (Microsoft Distributed Transaction Coordinator). DTC реализует протокол двухфазной фиксации (two-phase commit protocol), гарантирующий непротиворечивость результатов транзакции во всех ресурсах, участвующих в этой транзакции. DTC поддерживает только приложения, в которых реализуются совместимые с ним интерфейсы управления транзакциями. Эти приложения называются диспетчерами ресурсов (Resource Managers) (дополнительную информацию по этой теме см. в Distributed Transactions (EN) в.NET Framework Developer’s Guide). В настоящее время существует довольно много таких приложений - MSMQ, Microsoft SQL Server, Oracle, Sybase и др.

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

Вызов хранимой процедуры (stored procedure), которая заключает необходимые операции в операторы BEGIN TRANSACTION и COMMIT/ROLLBACK TRANSACTION, дает наилучшую производительность, позволяя выполнить транзакцию с разовым обменом данными с сервером (single round-trip) . Кроме того, транзакции баз данных могут быть вложенными, т. е. внутри активной транзакции можно начать выполнение новой транзакции.

В следующем фрагменте кода оператор BEGIN TRANSACTION начинает новую транзакцию. Транзакцию можно завершить двумя способами: либо фиксацией изменений в базе данных оператором COMMIT TRANSACTION, либо (при возникновении какой-либо ошибки) отменой всех изменений оператором ROLLBACK TRANSACTION.

CREATE PROCEDURE Proc1 … AS -- Начинаем транзакцию BEGIN TRANSACTION -- Выполняем операции транзакции … -- Проверяем наличие ошибок If @@Error <> 0 -- Откатываем транзакцию ROLLBACK TRANSACTION … -- Фиксируем транзакцию COMMIT TRANSACTION

Показанная ниже хранимая процедура принимает в качестве входного параметра XML-представление информации о заказе (order). Для выполнения соответствующих вставок в таблицы Orders и OrderDetails хранимая процедура загружает и анализирует XML с помощью системной хранимой процедуры sp_xmlpreparedocument . Как видно из исходного кода, все операции хранимой процедуры включены в явно выполняемую транзакцию, поэтому при неудачном завершении любой операции все внесенные изменения отменяются (откатываются).

Заметьте, что процедура устанавливает флаг XACT_ABORT в ON, указывая, что SQL Server должен автоматически откатить транзакцию, если выполнить какой-нибудь оператор не удастся.

CREATE PROCEDURE InsertOrder @Order NVARCHAR(4000) = NULL , @OrderId int Output AS SET NOCOUNT ON DECLARE @hDoc INT DECLARE @PKId INT -- Указываем, что SQL Server должен автоматически откатывать текущую -- транзакцию, если оператор Transact-SQL генерирует ошибку периода -- выполнения (run-time error). SET XACT_ABORT ON -- Начинаем транзакцию BEGIN TRANSACTION -- Загружаем и анализируем содержимое входного XML-представления -- информации о заказе, а затем помещаем его в XMLDocument EXEC sp_xml_preparedocument @hDoc OUTPUT, @Order -- Выбираем заголовок заказа из XMLDocument-узла Orders -- и вставляем его в таблицу Orders INSERT Orders(CustomerId, OrderDate, ShipToName, ShipToAddressId, OrderStatus) SELECT CustomerId, CONVERT(DateTime,OrderDate), ShipToName, ShipToAddressId, OrderStatus FROM OPENXML(@hDoc, "/NewDataSet/Orders") WITH (CustomerId int "CustomerId", OrderDate nvarchar(23) "OrderDate", ShipToName nvarchar(40) "ShipToName", ShipToAddressId int "ShipToAddressId", OrderStatus int "OrderStatus") -- Выбираем OrderId заказа, только что вставленного в таблицу Orders -- для использования при вставке позиций заказа (order details) SELECT @PKId = @@IDENTITY -- Выбираем позиции заказа из XMLDocument-узла Details -- и вставляем их в таблицу OrderDetails INSERT OrderDetails (OrderId, ItemId, UnitPrice, Quantity) SELECT @PKId as OrderId, ItemId, UnitPrice, Quantity FROM OPENXML(@hDoc, "/NewDataSet/Details") WITH (ItemId int "ItemId", UnitPrice money "UnitPrice", Quantity int "Quantity") -- Присваиваем значение выходному параметру Select @OrderId = @PKId -- Фиксируем транзакцию COMMIT TRANSACTION EXEC sp_xml_removedocument @hDoc RETURN 0 GO

Хотя такой подход обеспечивает хорошую производительность, при его использовании приходится программировать на Transact SQL, а это сложнее, чем на языке, совместимом с.NET.

Транзакции вручную

Транзакции, выполняемые вручную (manual transactions), позволяют явно управлять границами транзакции с помощью команд начала и конца транзакции. В этой модели также поддерживаются вложенные транзакции, т. е. вы можете начинать новую транзакцию в рамках активной транзакции. Расплата за возможность такого управления заключается в том, что на вас ложится бремя включения в транзакцию нужных ресурсов данных и их координация. Встроенной поддержки распределенных транзакций нет, поэтому управление распределенной транзакцией вручную потребует взять на себя массу обязанностей. Вам придется управлять каждым подключением к ресурсу и его использованием, а также реализовать поддержку свойств ACID в транзакции.

Транзакции ADO.NET, выполняемые вручную

Транзакции вручную поддерживают оба провайдера данных Microsoft ADO.NET, которые предоставляют набор объектов, позволяющих создавать соединение с хранилищем данных, начинать транзакцию, фиксировать или откатывать ее и, наконец, закрывать соединение. В своих примерах мы будем использовать управляемый ADO. NET-провайдер SQL (ADO.NET SQL managed provider).

Для выполнения операций в рамках единой транзакции нужно создать объект SQLTransaction , начать транзакцию с помощью объекта SQLConnection , добиться, чтобы все операции над базой данных проходили в этой транзакции, и зафиксировать или отменить транзакцию. Объект SQLTransaction предоставляет целый ряд свойств и методов для управления транзакцией. При успешном выполнении всех операций транзакции вы можете зафиксировать изменения в базе данных методом Commit . Для отката изменений применяется метод Rollback объекта SQLTransaction .

Для выполнения SQL-команды в транзакции свойство Transaction объекта Command необходимо установить на уже начатую транзакцию.

Visual Basic .NET

Dim conn as SQLConnection Dim cmd as SQLCommand Dim txn As SQLTransaction conn = New SQLConnection("ConnString") cmd = New SQLCommand " Открываем соединение conn.Open() " Начинаем транзакцию txn = conn.BeginTransaction() cmd.Transaction = Txn

Visual C# .NET

SQLConnection Conn = New SQLConnection("ConnString"); SQLCommand Cmd = New SQLCommand; // Открываем соединение Conn.Open(); // Начинаем транзакцию SQLTransaction Txn = Conn.BeginTransaction(); // Настраиваем свойство Transaction на транзакцию, где выполняется // SQL-команда Cmd.Transaction = Txn;

В примере, представленном ниже, мы выполняем в рамках транзакции две SQL-команды. Первая вставляет заголовок заказа (order header) в таблицу Orders и возвращает OrderId только что созданного заказа. Этот OrderId используется во второй команде, которая вставляет позиции этого заказа в таблицу OrderDetails. Транзакция отменяется, если хотя бы одна из двух команд терпит неудачу; при этом строки в базу данных не добавляются.

Visual Basic .NET

Dim conn As SqlConnection Dim cmd As SqlCommand Dim tran As SqlTransaction " Создаем новое соединение conn = New SqlConnection("ConnString") " Открываем соединение conn.Open() " Создаем объект Command cmd = New SqlCommand() " Создаем транзакцию tran = conn.BeginTransaction " Настраиваем свойство Transaction на транзакцию, где выполняется " SQL-команда cmd.Transaction = tran Try " Вставляем заголовок заказа. " Настраиваем свойства Command With cmd .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderHeader" .Connection = conn " Добавляем входные и выходные параметры.Parameters.Add("@CustomerId", SqlDbType.Int) .Parameters("@CustomerId").Direction = ParameterDirection.Input … " Устанавливаем значения параметров.Parameters("@CustomerId").Value = 1 … " Выполняем команду .ExecuteNonQuery() " Получаем OrderId добавленного заголовка заказа OrderId = .Parameters("@OrderId").Value " Очищаем параметры для следующей команды.Parameters.clear() End With " Вставляем позиции заказа " Настраиваем свойства Command With cmd .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderDetail" .Connection = conn " Добавляем параметры.Parameters.Add("@OrderId", SqlDbType.Int) .Parameters("@OrderId").SourceColumn = "OrderId" .Parameters("@OrderId").Direction = ParameterDirection.Input … " Устанавливаем значения параметров.Parameters("@OrderId").Value = OrderId .Parameters("@ItemId").Value = 100 … " Выполняем команду .ExecuteNonQuery() " Повторяем показанные выше строки для каждой позиции заказа End With " Фиксируем транзакцию tran.Commit() Catch " Откатываем транзакцию tran.Rollback() Finally " Код очистки. " Закрываем соединение. conn.Close() End Try

Как видите, две команды выполняются как часть одной транзакции. Если одна из них терпит неудачу, транзакция отменяется, и любые изменения в базе данных откатываются. Заключив код в блок try/catch/finally, вы гарантируете корректное выполнение транзакции: она фиксируется в самом конце блока try после успешного выполнения обеих SQL-команд. Любое исключение перехватывается в блоке catch, где транзакция отменяется и изменения, внесенные в ходе этой транзакции, откатываются.

Управление транзакциями через объекты ADO.NET приводит к менее эффективному блокированию, чем при использовании явных транзакций в хранимых процедурах. Причина в том, что при транзакциях ADO.NET, выполняемых вручную, требуется как минимум столько же двусторонних обменов данными с СУБД, сколько операций выполняется в транзакции плюс два обмена в ее начале и конце. Блокировки удерживаются в течение всего времени передачи вызовов из кода ADO.NET на сервер базы данных и обратно.

Транзакции MSMQ, выполняемые вручную

NET Framework предусматривает два вида поддержки транзакций MSMQ: внутреннюю (для транзакций вручную) и внешнюю (для автоматических транзакций). В первом случае в рамках транзакции возможен прием или передача нескольких сообщений. Во втором - сообщения участвуют в транзакциях DTC (Distributed Transaction Coordinator).

Транзакции MSMQ, выполняемые вручную, поддерживаются классом MessageQueueTransaction и обрабатываются исключительно ядром MSMQ. Подробности см. в статье Дункана Мак-Кензи (Duncan Mackenzie) (EN) .

Автоматические транзакции

Поддержка автоматических транзакций в.NET Framework опирается на службы MTS/COM+. COM+ использует DTC в качестве диспетчера и координатора транзакций при выполнении транзакций в распределенной среде. Это позволяет приложениям.NET выполнять транзакции, охватывающие разнообразные операции над множеством ресурсов, например вставку заказа в базу данных SQL Server, запись сообщения в очередь MSMQ (Microsoft Message Queue), отправку сообщения электронной почты и считывание информации из базы данных Oracle.

Предоставляя модель программирования на основе декларативных транзакций (declarative transactions), COM+ резко упрощает выполнение транзакций, в которых участвуют гетегрогенные ресурсы. Но учтите, что за это приходится расплачиваться снижением производительности, связанным с издержками взаимодействия DTC и COM; кроме того, поддержка вложенных транзакций отсутствует.

Страницы ASP.NET, методы Web-сервисов и. NET-классы можно помечать как транзакционные, присваивая им атрибут декларативной транзакции (declarative transaction attribute).

ASP.NET

<@ Page Transaction="Required" > Web-сервис ASP.NET <%@ WebService Language="VB" Class="Class1" %> <%@ assembly name="System.EnterpriseServices" %> … Public Class Class1 Inherits WebService <WebMethod(TransactionOption:= TransactionOption.RequiresNew) > _ Public Function Method1() …

Для участия в автоматических транзакциях. NET-класс должен наследовать от System.EnterpriseServices.ServicedComponent , который обеспечивает выполнение класса в COM+. Если вы сделаете именно так, COM+, взаимодействуя с DTC, создаст распределенную транзакцию и подключит к ней все необходимые ресурсы без вашего участия. Кроме того, вам нужно присвоить классу атрибут декларативной транзакции, чтобы определить его поведение при выполнении транзакции.

Visual Basic .NET

Visual C# .NET

public class Class1: ServicedComponent { … }

Транзакционный атрибут класса принимает одно из следующих значений:

  • Disabled. Указывает, что объект никогда не создается в транзакции COM+. Для поддержки транзакций объект может обращаться к DTC напрямую.
  • NotSupported. Указывает, что объект никогда не создается в транзакции.
  • Supported. Указывает, что объект выполняется в контексте транзакции своего создателя. Если объект сам является корневым или если его создатель не выполняется в транзакции, объект создается вне транзакции.
  • Required. Указывает, что объект выполняется в контексте транзакции своего создателя. Если объект сам является корневым или если его создатель не выполняется в транзакции, при создании такого объекта создается новая транзакция.
  • RequiresNew. Указывает, что объекту нужна транзакция и что при его создании создается новая транзакция.

В следующем коде содержится. NET-класс, настроенный на выполнение в COM+. Кроме того, атрибутам сборки присваиваются значения, необходимые для конфигурирования свойств COM+-приложения.

Visual Basic .NET

Imports System Imports System.Runtime.CompilerServices Imports System.EnterpriseServices Imports System.Reflection " Детали регистрации. " Имя COM+-приложения в том виде, в каком оно присутствует " в каталоге COM+ " Строгое имя (strong name) для сборки (assembly) Public Class Class1 Inherits ServicedComponent Public Sub Example1() … End Sub End Class

Visual C# .NET

using System; using System.Runtime.CompilerServices; using System.EnterpriseServices; using System.Reflection; // Детали регистрации. // Имя COM+-приложения в том виде, в каком оно присутствует // в каталоге COM+ // Строгое имя для сборки public class Class1: ServicedComponent { public void Example1() { … } }

<Assembly: ApplicationName(“Class1”) > указывает имя COM+-приложения, в которое устанавливаются компоненты сборки. <Assembly: ApplicationActivation(ActivationOption.Server) > определяет, является ли это приложение сервером или библиотекой. Когда вы указываете ApplicationActivation(ActivationOption.Server) , сборку необходимо установить в GAC (global assembly cache) с помощью утилиты командной строки gacutil (GacUtil.exe).

Для преобразования сборки в библиотеку типов, регистрации библиотеки типов и ее установки в заданное COM+-приложение можно использовать утилиту командной строки Regsvcs.exe. Кроме того, эта утилита настраивает свойства, добавленные в сборку программным способом. Например, если в сборке указано ApplicationActivation(ActivationOption.Server) , утилита создаст серверное приложение. Если вызванная сборка еще не установлена в COM+, исполняющая среда создаст и зарегистрирует библиотеку типов, а затем установит ее в COM+. COM+-приложение, созданное для сборки, можно просмотреть и настроить в оснастке Component Services.

Процесс создания, регистрации и использования обслуживаемых компонентов (serviced components) подробно рассматривается в разделе Writing Serviced Components (EN) руководства.NET Framework Developer’s Guide.

В следующем коде показывается транзакционный класс, сконфигурированный для запуска под управлением COM+, в котором в рамках транзакции выполняются две SQL-команды. Первая вставляет заголовок заказа в таблицу заказов и возвращает OrderId добавленного заказа. Этот OrderId используется второй командой при вставке позиций заказа в таблицу OrderDetails. Транзакция отменяется, если не удалось выполнить хотя бы одну из двух команд; при этом записи в базу данных не добавляются.

Visual Basic .NET

<Transaction(TransactionOption.Required) > Public Class Class1 Inherits ServicedComponent Public Sub Example1() … Try " Создаем новое соединение conn = New SqlConnection("ConnString") " Открываем соединение conn.Open() " Создаем новый объект Command cmd = New SqlCommand() " Вставляем заголовок заказа " Присваиваем значения свойствам Command With cmd1 .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderHeader" .Connection = conn " Добавляем входные и выходные параметры.Parameters.Add("@CustomerId", SqlDbType.Int) … .ExecuteNonQuery() " Очищаем параметры для следующей команды .Parameters.clear() End With " Вставляем позиции заказа " Настраиваем свойства Command With cmd .CommandType = CommandType.StoredProcedure .CommandText = "InsertOrderDetail" .Connection = conn " Добавляем параметры.Parameters.Add("@OrderId", SqlDbType.Int) … " Выполняем команду .ExecuteNonQuery() " Повторяем эти строки для каждой позиции заказа End With " Фиксируем транзакцию ContextUtil.SetComplete() Catch " Откатываем транзакцию ContextUtil.SetAbort() Finally " Код очистки End Try End Sub

Используя класс System.EnterpriseServices.ContextUtil , можно получить информацию о контексте COM+-объекта. Этот класс предоставляет методы SetComplete и SetAbort , позволяющие явным образом фиксировать и откатывать транзакцию. Легко догадаться, что метод ContextUtil.SetComplete вызывается в самом конце блока try, когда все операции выполнены успешно и нужно зафиксировать транзакцию. Все исключения перехватывается в блоке catch, где транзакция отменяется с помощью ContextUtil.SetAbort .

Кроме того, с помощью класса-атрибута (attribute class) System.EnterpriseServices.AutoComplete можно добиться, чтобы обслуживаемый компонент автоматически определял, фиксировать или откатывать транзакцию. Компонент «голосует» за фиксацию транзакции, если вызов метода завершился успешно. Если вызов метода привел к генерации исключения, транзакция автоматически отменяется; явный вызов ContextUtil.SetAbort не нужен. Чтобы воспользоваться этой возможностью, вставьте атрибут перед методом класса:

Visual Basic .NET

Public Class Class1 Inherits ServicedComponent Public Sub Example1() … End Sub End Class

Visual C# .NET

public class Class1: ServicedComponent { public void Example1() { … } }

Атрибут <AutoComplete > предлагает самый простой способ программирования транзакций, позволяя обходиться без явных фиксации и отката транзакций. Вы получите точно такую же функциональность, что и в предыдущем примере, где для отмены транзакции явно вызывался метод ContextUtil.SetAbort в блоке catch. Недостаток этого способа в том, что выполнение транзакции неочевидно и при сопровождении кода о ней можно забыть. Кроме того, нет возможности вывода дружественного к пользователю сообщения, если транзакция терпит неудачу. В таких случаях следует явно перехватывать все исключения, вызывать ContextUtil.SetAbort и показывать требуемое сообщение.

В системах, где нужно выполнять транзакции, использующие MSMQ и другие ресурсы, единственно возможный выбор - применение транзакций DTC или COM+. DTC координирует все диспетчеры ресурсов, участвующие в распределенной транзакции, а также управляет деятельностью, связанной с транзакциями. Пример распределенной транзакции MSMQ и SQL Server см. в статье Дункана Мак-Кензи Reliable Messaging with MSMQ and .NET (EN) .

Заключение

При использовании каждой из технологий работы с транзакциями приходится идти на компромис между производительностью приложения и удобством сопровождения кода. Запуск реализованной в хранимой процедуре транзакции базы данных обеспечивает наилучшую производительность, так как требуется лишь один двусторонний обмен информацией с базой данных. Кроме того, обеспечивается гибкость управления транзакциями, поскольку явно указываются начало и завершение транзакции. Но, хотя этот способ дает хорошую производительность и гибкость, приходится программировать на Transact SQL, что не так легко, как на языках.NET.

Транзакции, выполняемые вручную с помощью транзакционных объектов ADO.NET, просты в программировании. Благодаря явному применению инструкций начала и завершения транзакции, возможно гибкое управление границами транзакции. Платой за эту легкость и гибкость является снижение производительности за счет дополнительных двусторонних обменов информацией с базой данных при выполнении транзакции.

Автоматическая транзакция - единственно возможный выбор, когда транзакция использует несколько диспетчеров ресурсов, поддерживающих транзакции, например базы данных SQL Server, очереди сообщений MSMQ и т. д. Они значительно упрощают разработку приложений и предъявляют более низкие требования к квалификации программиста. Однако из-за того, что всю работу по координации выполняет служба COM+, возможны дополнительные издержки.

Транзакцией называется выполнение последовательности команд (SQL-конструкций) в базе данных, которая либо фиксируется при успешной реализации каждой команды, либо отменяется при неудачном выполнении хотя бы одной команды. Большинство современных СУБД поддерживают механизм транзакций, и подавляющее большинство клиентских приложений, работающих с ними, используют для выполнения своих команд транзакции.

Возникает вопрос - зачем нужны транзакции? Представим себе, что в базу данных BDTur_firm2 требуется вставить связанные записи в две таблицы - «Туристы» и «Информация о туристах». Если запись, вставляемая в таблицу «Туристы», окажется неверной, например, из-за неправильно указанного кода туриста, база данных не позволит внести изменения, но при этом в таблице «Информация о туристах» появится ненужная запись. Рассмотрим такую ситуацию на примере.

Запустим Management Studio, в новом бланке введем запрос для добавления двух записей:




VALUES (8, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548);

Две записи успешно добавляются в базу данных:

(1 row(s) affected) //или (строк обработано: 1)
(1 row(s) affected) //или (строк обработано: 1)

Теперь спровоцируем ошибку - изменим код туриста только во втором запросе:

INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество)
VALUES (8, "Тихомиров", "Андрей", "Борисович");
INSERT INTO [Информация о туристах]([Код туриста], [Серия паспорта], Город, Страна, Телефон, Индекс)
VALUES (9, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548);

Появляется сообщение о невозможности вставки первой записи с уже имеющимся значением ключевого поля. Вторая запись, однако, добавляется в таблицу:

Сообщение 2627, уровень 14, состояние 1, строка 1

The statement has been terminated.
(строк обработано: 1)

Извлечем содержимое обеих таблиц следующим двойным запросом:

SELECT * FROM Туристы
SELECT * FROM [Информация о туристах]

В таблице «Информация о туристах» последняя запись добавилась безо всякой связи с записью таблицы «Туристы» (рис. 99). Для того чтобы избегать подобных ошибок, нужно применить транзакцию.

Рис. 99. Содержимое таблиц «Туристы» и «Информация о туристах» - нарушение связи

Удалим все внесенные записи из обеих таблиц и оформим исходные SQL-конструкции в виде транзакции:

BEGIN TRAN

INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество)
VALUES (8, "Тихомиров", "Андрей", "Борисович");


Город, Страна, Телефон, Индекс)
VALUES (8, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548); SELECT @OshibkiTabliciInfoTourists=@@ERROR IF @OshibkiTabliciTourists=0 AND @OshibkiTabliciInfoTourists=0 COMMIT TRAN ELSE
ROLLBACK TRAN

Начало транзакции объявляется с помощью команды BEGIN TRAN. Далее создаются два параметра - @OshibkiTabliciTourists, OshibkiTabliciInfoTourists для сбора ошибок. После первого запроса возвращаем значение, которое встроенная функция @@ERROR присваивает первому параметру:

SELECT @OshibkiTabliciTourists=@@ERROR

То же самое делаем после второго запроса для другого параметра:

SELECT @OshibkiTabliciInfoTourists=@@ERROR

Проверяем значения обоих параметров, которые должны быть равными нулю при отсутствии ошибок:

IF @OshibkiTabliciTourists=0 AND @OshibkiTabliciInfoTourists=0

В этом случае подтверждаем транзакцию (в данном случае внесение изменений) при помощи команды COMMIT TRAN. В противном случае - если значение хотя бы одного из параметров @OshibkiTabliciTourists и @OshibkiTabliciInfoTourists оказывается отличным от нуля, отменяем транзакцию при помощи команды ROLLBACK TRAN.

После выполнения транзакции появляется сообщение о добавлении двух строк:

(строк обработано: 1)
(строк обработано: 1)

Снова изменим код туриста во втором запросе:

BEGIN TRAN
DECLARE @OshibkiTabliciTourists int, @OshibkiTabliciInfoTourists int
INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество)
VALUES (8, "Тихомиров", "Андрей", "Борисович");
SELECT @OshibkiTabliciTourists=@@ERROR
INSERT INTO [Информация о туристах]([Код туриста], [Серия паспорта],
Город, Страна, Телефон, Индекс)
VALUES (9, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548);
SELECT @OshibkiTabliciInfoTourists=@@ERROR
IF @OshibkiTabliciTourists=0 AND @OshibkiTabliciInfoTourists=0
COMMIT TRAN
ELSE
ROLLBACK TRAN

Запускаем транзакцию - появляется в точности такое же сообщение, что и в случае применения обычных запросов:

Сообщение 2627, уровень 14, состояние 1, строка 3
Violation of PRIMARY KEY constraint ‘PK_Туристы". Cannot insert duplicate key in object ‘dbo.Туристы".
The statement has been terminated. (строк обработано: 1)

Однако теперь изменения не были внесены во вторую таблицу (рис. 100).

Рис. 100. Содержимое таблиц «Туристы» и «Информация о туристах» после выполнения неудачной транзакции

Сообщение (1 row(s) affected), указывающее на «добавление» одной записи, в данном случае оно всего лишь означает, что вторая SQL-конструкция была верной, и запись могла быть добавлена в случае успешного выполнения транзакции.

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

Транзакции в ADO . NET

Перейдем теперь к рассмотрению транзакций в ADO .NET. Создадим новое консольное приложение Easy Transaction. Поставим задачу: передать те же самые данные в две таблицы - «Туристы» и «Информация о туристах».

Полный листинг данного приложения выглядит следующим образом:

using System;
using System.Data.SqlClient;
namespace EasyTransaction {
class Class1 {
static void Main(string args)
{
//Создаем соединение
SqlConnection conn = new SqlConnection();
conn.ConnectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=" +
@"D:\ВМИ\For ADO\BDTur_firmSQL2.mdf" +
"integrated Security=True;Connect Timeout=30;User Instance=True";
conn.Open();
SqlCommand myCommand = conn.CreateCommand();
//Создаем транзакцию
myCommand.Transaction = conn.BeginTransaction(System.Data.IsolationLevel.Serializable);
try {
myCommand.CommandText =
"INSERT INTO Туристы ([Код туриста], Фамилия, Имя, Отчество) " +
"VALUES (9, "Тихомиров", "Андрей", "Борисович")";
myCommand.CommandText = "INSERT INTO [Информация о туристах]" +
" ([Код туриста], [Серия паспорта], Город, Страна, Телефон, Индекс) " +
"VALUES (9, "CA 1234567", "Новосибирск", "Россия", 1234567, 996548)";
myCommand.ExecuteNonQuery();
//Подтверждаем транзакцию
Console.WriteLine("Передача данных успешно завершена");
}
catch(Exception ex) {
//Отклоняем транзакцию
Console.WriteLine("При передаче данных произошла ошибка: "+ ex.Message);
}
finally {
conn.Close();
}
} end Main
} end Class
} end namespace

Перед запуском приложения снова удаляем все добавленные записи из таблиц. При успешном выполнении запроса появляется соответствующее сообщение, а в таблицы добавляются записи (рис. 101).

Рис. 101. Приложение EasyTransaction. Транзакция выполнена

Повторный запуск этого приложения приводит к отклонению транзакции - нельзя вставлять записи с одинаковыми значениями первичных ключей (рис. 102).

Рис. 102. Приложение EasyTransaction. Транзакция отклонена

В виде транзакции можно оформлять выполнение одной или нескольких хранимых процедур, - в самом деле, общая конструкция имеет следующий вид:

//Создаем соединение
... см. в примере приложения EasyTransaction
//Создаем транзакцию
myCommand.Transaction = conn.BeginTransaction();
try {
//Выполняем команды, вызываем одну или несколько хранимых процедур
//Подтверждаем транзакцию
myCommand.Transaction.Commit();
}
catch(Exception ex) {
//Отклоняем транзакцию
myCommand.Transaction.Rollback();
}
finally {
//Закрываем соединение
conn.Close();
}

При выполнении транзакций несколькими пользователями одной базы данных могут возникать следующие проблемы:

1. Dirty reads - «грязное» чтение. Первый пользователь начинает транзакцию, изменяющую данные. В это время другой пользователь (или создаваемая им транзакция) извлекает частично измененные данные, которые не являются корректными.

2. Non-repeatable reads - неповторяемое чтение. Первый пользователь начинает транзакцию, изменяющую данные. В это время другой пользователь начинает и завершает другую транзакцию. Первый пользователь при повторном чтении данных (например, если в его транзакцию входит несколько инструкций SELECT) получает другой набор записей.

3. Phantom reads - чтение фантомов. Первый пользователь начинает транзакцию, выбирающую данные из таблицы. В это время другой пользователь начинает и завершает транзакцию, вставляющую или удаляющую записи. Первый пользователь получит другой набор данных, содержащий фантомы - удаленные или измененные строки.

Для решения этих проблем разработаны четыре уровня изоляции транзакции:

1. Read uncommitted. Транзакция может считывать данные, с которыми работают другие транзакции. Применение этого уровня изоляции может привести ко всем перечисленным проблемам.

2. Read committed. Транзакция не может считывать данные, с которыми работают другие транзакции. Применение этого уровня изоляции исключает проблему «грязного» чтения.

3. Repeatable read. Транзакция не может считывать данные, с которыми работают другие транзакции. Другие транзакции также не могут считывать данные, с которыми работает эта транзакция. Применение этого уровня изоляции исключает все проблемы, кроме чтения фантомов.

4. Serializable. Транзакция полностью изолирована от других транзакций. Применение этого уровня изоляции полностью исключает все проблемы.

По умолчанию установлен уровень Read committed. В справке Microsoft SQL Server 2005 приводится таблица, иллюстрирующая различные уровни изоляции (рис. 103).

Рис. 103. Уровни изоляции Microsoft SQL Server 2005

Использование наибольшего уровня изоляции (Serializable) означает наибольшую безопасность и вместе с тем наименьшую производительность - все транзакции выполняются в виде серии, последующая вынуждена ждать завершения предыдущей. И наоборот, применение наименьшего уровня (Read uncommitted) означает максимальную производительность и полное отсутствие безопасности. Впрочем, нельзя дать универсальных рекомендаций по применению этих уровней - в каждой конкретной ситуации решение будет зависеть от структуры базы данных и характера выполняемых запросов.

Для установки уровня изоляции применяется следующая команда:

SET TRANSACTION ISOLATION LEVEL
READ UNCOMMITTED
или READ COMMITTED
или REPEATABLE READ
или SERIALIZABLE

Например, в транзакции, добавляющей две записи, уровень изоляции указывается следующим образом:

BEGIN TRAN SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
DECLARE @OshibkiTabliciTourists int, @OshibkiTabliciInfoTourists int
...
ROLLBACK TRAN

В ADO .NET уровень изоляции можно установить при создании транзакции:

myCommand.Transaction = conn.BeginTransaction(System.Data.IsolationLevel.Serializable); Дополнительно поддерживаются еще два уровня (рис. 104):

1. Chaos. Транзакция не может перезаписать другие непринятые транзакции с большим уровнем изоляции, но может перезаписать изменения, внесенные без использования транзакций. Данные, с которыми работает текущая транзакция, не блокируются;

2. Unspecified. Отдельный уровень изоляции, который может применяться, но не может быть определен. Транзакция с этим уровнем может применяться для задания собственного уровня изоляции.

Рис. 104. Определение уровня транзакции

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

Существует два основных вида сделок: транзакции подключения и внешние транзакции. Сделка соединения (например, SqlTransaction) привязана непосредственно к соединению db (например, SqlConnection), что означает, что вам нужно продолжать передавать соединение - в некоторых случаях ОК, но не разрешает «создавать/использовать/выпускать», использование и не разрешает работу с несколькими db. Пример (отформатирован для пробела):

Using (IDbTransaction tran = conn.BeginTransaction()) { try { //your code tran.Commit(); } catch { tran.Rollback(); throw; } }

Не слишком грязно, но ограничивается нашей связью «conn». Если мы хотим обратиться к различным методам, нам нужно передать «conn».

Альтернативой является внешняя транзакция; новый в.NET 2.0, объект TransactionScope (система.Transactions.dll) позволяет использовать по всему диапазону операций (подходящие провайдеры будут автоматически зачисляться в транзакцию с окружающими). Это позволяет легко встраиваться в существующий (не транзакционный) код и разговаривать с несколькими провайдерами (хотя DTC будет задействован, если вы поговорите с более чем одним).

Например:

Using(TransactionScope tran = new TransactionScope()) { CallAMethodThatDoesSomeWork(); CallAMethodThatDoesSomeMoreWork(); tran.Complete(); }

Обратите внимание, что эти два метода могут обрабатывать свои собственные соединения (open/use/close/dispose), но они беззвучно станут частью внешней транзакции без необходимости пропускать что-либо.

Если ваши ошибки кода, Dispose() будет вызываться без Complete (), поэтому он будет откат. Ожидаемое вложение и т. Д. Поддерживается, хотя вы не можете откатить внутреннюю транзакцию, но завершите внешнюю транзакцию: если кто-то недоволен, транзакция прерывается.

Другим преимуществом TransactionScope является то, что он не привязан к базам данных; любой поставщик транзакций может использовать его. WCF, например. Или есть даже некоторые модели объектов, совместимых с TransactionScope (например, классы.NET с возможностью отката - возможно, проще, чем память, хотя я никогда не использовал этот подход самостоятельно).

В общем, очень, очень полезный объект.

Некоторые оговорки:

  • В SQL Server 2000 TransactionScope немедленно переключится на DTC; это исправлено в SQL Server 2005 и выше, оно может использовать LTM (намного меньше служебных), пока вы не поговорите с 2 источниками и т. д., когда он повышен до DTC.
  • Существует сбой , что означает, что вам может потребоваться настроить строка подключения

Protected void Button1_Click(object sender, EventArgs e) { using (SqlConnection connection1 = new SqlConnection("Data Source=.\\SQLEXPRESS;AttachDbFilename=|DataDirectory|\\Database.mdf;Integrated Security=True;User Instance=True")) { connection1.Open(); //Start a local transaction. SqlTransaction sqlTran = connection1.BeginTransaction(); //Enlist a command in the current transaction. SqlCommand command = connection1.CreateCommand(); command.Transaction = sqlTran; try { //Execute two separate commands. command.CommandText = "insert into (drname,drspecialization,drday) values ("a","b","c")"; command.ExecuteNonQuery(); command.CommandText = "insert into (drname,drspecialization,drday) values ("x","y","z")"; command.ExecuteNonQuery(); //Commit the transaction. sqlTran.Commit(); Label3.Text = "Both records were written to database."; } catch (Exception ex) { //Handle the exception if the transaction fails to commit. Label4.Text = ex.Message; try { //Attempt to roll back the transaction. sqlTran.Rollback(); } catch (Exception exRollback) { //Throws an InvalidOperationException if the connection //is closed or the transaction has already been rolled //back on the server. Label5.Text = exRollback.Message; } } } }

Локальные транзакции Local Transactions

В этой статье

В ADO.NET ADO.NET транзакции используются для связи нескольких задач, чтобы они выполнялись как одно целое. Transactions in ADO.NET ADO.NET are used when you want to bind multiple tasks together so that they execute as a single unit of work. Например, пусть приложение выполняет две задачи. For example, imagine that an application performs two tasks. Во-первых, оно заносит в таблицу сведения о заказе. First, it updates a table with order information. Во-вторых, обновляет таблицу, содержащую список товаров на складе, списывая заказанные элементы. Second, it updates a table that contains inventory information, debiting the items ordered. При сбое любой задачи, затем оба обновления откатываются. If either task fails, then both updates are rolled back.

Определение типа транзакции Determining the Transaction Type

Транзакция считается локальной транзакции при состоит из одной фазы и обрабатывается непосредственно базой данных. A transaction is considered to be a local transaction when it is a single-phase transaction and is handled by the database directly. Транзакция считается распределенной транзакции при координируется монитором транзакций и используется для разрешения транзакций резервные механизмы (например, двухфазную фиксацию). A transaction is considered to be a distributed transaction when it is coordinated by a transaction monitor and uses fail-safe mechanisms (such as two-phase commit) for transaction resolution.

Для выполнения локальных транзакций каждый поставщик данных платформы .NET Framework .NET Framework имеет свой собственный объект Transaction . Each of the .NET Framework .NET Framework data providers has its own Transaction object for performing local transactions. Если требуется выполнить транзакцию в базе данных SQL Server, выбирается транзакция System.Data.SqlClient . If you require a transaction to be performed in a SQL Server database, select a System.Data.SqlClient transaction. Для транзакции Oracle используйте поставщик System.Data.OracleClient . For an Oracle transaction, use the System.Data.OracleClient provider. Кроме того, DbTransaction класс, который доступен для написания независимого от поставщика кода, который требуется транзакций. In addition, there is a DbTransaction class that is available for writing provider-independent code that requires transactions.

Примечание

Транзакции наиболее эффективны, если они выполняются на сервере. Transactions are most efficient when they are performed on the server. При работе с базой данных SQL Server, интенсивно использующей явные транзакции, следует рассмотреть возможность их записи в виде хранимых процедур при помощи инструкции Transact-SQL BEGIN TRANSACTION. If you are working with a SQL Server database that makes extensive use of explicit transactions, consider writing them as stored procedures using the Transact-SQL BEGIN TRANSACTION statement.

Выполнение транзакций с использованием одного соединения Performing a Transaction Using a Single Connection

В ADO.NET ADO.NET транзакции управляются объектом Connection . In ADO.NET ADO.NET , you control transactions with the Connection object. Инициировать транзакцию можно с помощью метода BeginTransaction . You can initiate a local transaction with the BeginTransaction method. После начала транзакции при помощи свойства Transaction объекта Command к ней можно прикрепить команду. Once you have begun a transaction, you can enlist a command in that transaction with the Transaction property of a Command object. Затем в зависимости от успеха или ошибки компонентов транзакции можно зафиксировать или откатить изменения, сделанные в источнике данных. You can then commit or roll back modifications made at the data source based on the success or failure of the components of the transaction.

Примечание

Метод EnlistDistributedTransaction не должен использоваться для локальной транзакции. The EnlistDistributedTransaction method should not be used for a local transaction.

Область действия транзакции ограничена соединением. The scope of the transaction is limited to the connection. В следующем примере выполняется явная транзакция, состоящая из двух отдельных команд в блоке try . The following example performs an explicit transaction that consists of two separate commands in the try block. Команды выполняют инструкции INSERT для таблицы Production.ScrapReason в образце базы данных AdventureWorks в SQL Server, которые будут зафиксированы при отсутствии исключений. The commands execute INSERT statements against the Production.ScrapReason table in the AdventureWorks SQL Server sample database, which are committed if no exceptions are thrown. При возникновении исключения код в блоке catch произведет откат транзакции. The code in the catch block rolls back the transaction if an exception is thrown. При отмене транзакции или обрыве соединения до выполнения транзакции она откатывается автоматически. If the transaction is aborted or the connection is closed before the transaction has completed, it is automatically rolled back.

Пример Example

Чтобы осуществить транзакцию, выполните указанные ниже действия. Follow these steps to perform a transaction.

    Вызовите метод BeginTransaction объекта SqlConnection для отметки начала транзакции. Call the BeginTransaction method of the SqlConnection object to mark the start of the transaction. Метод BeginTransaction возвращает ссылку на транзакцию. The BeginTransaction method returns a reference to the transaction. Эта ссылка назначается объектам SqlCommand , прикрепленным к транзакции. This reference is assigned to the SqlCommand objects that are enlisted in the transaction.

    Присвойте объект Transaction свойству Transaction объекта SqlCommand . Assign the Transaction object to the Transaction property of the SqlCommand to be executed. Исключение вызывается, если команда выполняется при соединении с активной транзакцией, а объект Transaction не был назначен свойству Transaction объекта Command . If a command is executed on a connection with an active transaction, and the Transaction object has not been assigned to the Transaction property of the Command object, an exception is thrown.

    Выполните требуемые команды. Execute the required commands.

    Для выполнения транзакции вызовите метод Commit объекта SqlTransaction , для завершения транзакции вызовите метод Rollback . Call the Commit method of the SqlTransaction object to complete the transaction, or call the Rollback method to end the transaction. Транзакция откатывается, если соединение закрывается или пропадает до выполнения метода Commit либо Rollback . If the connection is closed or disposed before either the Commit or Rollback methods have been executed, the transaction is rolled back.

Следующий пример кода демонстрирует транзакционную логику, используемую ADO.NET ADO.NET с Microsoft SQL Server. The following code example demonstrates transactional logic using ADO.NET ADO.NET with Microsoft SQL Server.

Using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); // Start a local transaction. SqlTransaction sqlTran = connection.BeginTransaction(); // Enlist a command in the current transaction. SqlCommand command = connection.CreateCommand(); command.Transaction = sqlTran; try { // Execute two separate commands. command.CommandText = "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong size")"; command.ExecuteNonQuery(); command.CommandText = "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong color")"; command.ExecuteNonQuery(); // Commit the transaction. sqlTran.Commit(); Console.WriteLine("Both records were written to database."); } catch (Exception ex) { // Handle the exception if the transaction fails to commit. Console.WriteLine(ex.Message); try { // Attempt to roll back the transaction. sqlTran.Rollback(); } catch (Exception exRollback) { // Throws an InvalidOperationException if the connection // is closed or the transaction has already been rolled // back on the server. Console.WriteLine(exRollback.Message); } } } Using connection As New SqlConnection(connectionString) connection.Open() " Start a local transaction. Dim sqlTran As SqlTransaction = connection.BeginTransaction() " Enlist a command in the current transaction. Dim command As SqlCommand = connection.CreateCommand() command.Transaction = sqlTran Try " Execute two separate commands. command.CommandText = _ "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong size")" command.ExecuteNonQuery() command.CommandText = _ "INSERT INTO Production.ScrapReason(Name) VALUES("Wrong color")" command.ExecuteNonQuery() " Commit the transaction sqlTran.Commit() Console.WriteLine("Both records were written to database.") Catch ex As Exception " Handle the exception if the transaction fails to commit. Console.WriteLine(ex.Message) Try " Attempt to roll back the transaction. sqlTran.Rollback() Catch exRollback As Exception " Throws an InvalidOperationException if the connection " is closed or the transaction has already been rolled " back on the server. Console.WriteLine(exRollback.Message) End Try End Try End Using

См. также See also

  • Транзакции и параллельность Transactions and Concurrency
  • Распределенные транзакции Distributed Transactions
  • Интеграция System.Transactions с SQL Server System.Transactions Integration with SQL Server
  • Центр разработчиков наборов данных и управляемых поставщиков ADO.NET ADO.NET Managed Providers and DataSet Developer Center


Загрузка...