Манипулирование транзакциями между .NET компонентами

Источник: progman

Нами будет рассмотрен вопрос о транзакциях, применимых как к Web-приложениям, так и к приложениям на базе Windows. 
Вопрос Я прочитал вашу колонку в выпуске (http://msdn.microsoft.com/msdnmag/issues/02/02/basics/default.aspx), посвященном COM+ , DCOM и MSMQ сериализации в .NET. Вы пишете, что если компонент выполняет транзакции над отдельной базой данных и вы предполагаете, что будете going against только одной базы данных, вам не обязательно требуется COM+, чтобы реализовать эти транзакции. Вы можете реализовать их с помощью ADO.NET. Это представляется серьезным изменением идеологии. 

Могли бы вы предоставить больше информации о том, как можно регулировать транзакции между .NET компонентами даже в том случае, если я имею дело с одной базой данных? Если я передаю connection string и сохраняю соединение (connection) открытым, не вызовет ли это издержек? 

Ответ. Для начала рассмотрим создание транзакций с помощью ADO.NET. Я создал пример приложения, которое вставляет от 1 до 4 order строк в таблицу Order Details в эталонной базе данных Northwind. Приложение использует SQL сервер и SQLClient провайдер данных. Практически то же самое можно сделать с помощью OLE DB провайдера в том случае, если ваша база данных поддерживает транзакции. 

Вместо того, чтобы просто сослаться на эталонный код транзакции в документации, поясняющей, как работают транзакции ADO.NET, мое эталонное приложение использует простой многоуровневый (multiplayer) подход. Весь код базы данных содержится в отдельном классе компонент (separate components class) в то время, как front end находится в Windows form в отдельном проекте. 

Компонент базы данных содержит один класс, которые именуется DBStuffDONET. Обратите внимание, что приватная переменная (private variable) содержит connection string. Это хорошо в том случае, когда компонент не требуется использовать с различными базами данных. Тем не менее, компонент имеет совмещенный конструктор (overloaded constructor), который принимает новую connection string, которая может специфицироваться, когда вы инстантиируете (instantiate) класс. 

После создания connection string variable я также создаю приватную переменную (private variable) для инстансов (Instances) SqlConnection и SqlTransaction. Детали будут рассмотрены позднее. 

Класс использует функцию RunSQLWithDataSet. Эта функция не участвует в транзакции, она возвращает транзакцию DataSet. В данном примере для простоты я использую статический SQL вместо хранимых процедур (stored procedures). 

Рассмотрим те функции, которые участвуют в транзакциях, а также некоторые из тех, которые не участвуют. Подпрограмма OpenConnectionTrans создает новый SqlConnection и затем открывает его. После этого он создает транзакцию базы данных, BeginTransaction и устанавливает переменную TransactionCurrent для ссылки (to reference) на транзакцию, как видно их данного примера: 

 
Public Sub OpenConnectionTrans()
  ConnectionCurrent = New SqlConnection(sConnectionString)
  ConnectionCurrent.Open()
  TransactionCurrent = ConnectionCurrent.BeginTransaction()
End Sub
Если вам нужна связь (connection) и не нужна поддержка транзакций, вы можете вызвать OpenConnection: 
Public Sub OpenConnection()
  ConnectionCurrent = New SqlConnection(sConnectionString)
  ConnectionCurrent.Open()
End Sub
Если пришло время выполнить транзакцию, вызывается следующая функция:
Public Sub CommitTransaction()
  TransactionCurrent.Commit()
End Sub
Если вам нужно откатить назад (roll back) транзакцию, вызыввется функция: 
Public Sub RollbackTransaction()
  TransactionCurrent.Rollback()
End Sub
Когда вы закончили работать с соединением (connection), вы должны закрыть его. Нижеследующая функция закрывает соединение и удаляет ссылку на объект соединения (connection object): 
Public Sub CloseConnection()
  If ConnectionCurrent Is Nothing Then
  Exit Sub
  End If
  If ConnectionCurrent.State = ConnectionState.Open Then
  ConnectionCurrent.Close()
  ConnectionCurrent = Nothing
  End If
End Sub

Рассмотрим теперь, что произойдет, если вам понадобится сделать вставку (insert) или обновление (update), которые являются частью транзакции. Именно для этой цели предназначена функция RunSQLNonQuery. Когда вы вызываете эту функцию, она создает новый инстанс класса SqlCommand, а затем устанавливает свойства соединения (Connection property) подобную текущему соединению (ConnectionCurrent), а свойства транзакции (Transaction property) такие как у текущей транзакции (TransactionCurrent). После этого выполняется инструкция SQL с помощью метода ExecuteNonQuery, который вызывает малые издержки, поскольку не возвращает никаких данных (см. Рисунок 2). 

Последняя функция в классе - это RunSQLScalar, которая выполняет инструкцию SQL и возвращает отдельный элемент информации (single piece of information). Эта функция полезна, если вам требуется взять один элемент данных, такой, например, как цена единицы товара (Unit Price). 

Если вы посмотрите на этот класс внимательнее, то вы увидите, что он stateful. Вы должны инстантиировать класс, открыть соединение (connection) с транзакцией, а затем выполнить код, который вы желаете сделать частью транзакции. После этого вы должны выполнить (commit) или откатить назад (roll back) эту транзакцию и, наконец, закрыть соединение. Все это не так сложно, поскольку вы инстантииируете класс, совершаете работу и закрываете соединение с помощью Windows-based или Web приложения. В моем примере приложение использует клиентский интерфейс на базе Windows и просто инстантиирует класс на время выполнения приложения. 

На Рисунке №3 показан простой интерфейс построенный на основе Windows forms. Вы выбираете Order из списка вверху, затем кликаете кнопки со стрелками и добавляете нужные строки с подробными данными (detail rows). На Рисунке 3 показана форма с двумя строками подробных данных. В данном примере имеются поля количество и дисконт (discount), которым для целей тестирования присвоены значения 1 и .15 соответственно.

После того, как вы ввели line items, кликните кнопку Insert - ADO.NET Trx для того, чтобы добавить новые элементы в таблицу Order Details (Подробные данные о заказе) вашей базы данных. 

Рассмотрим конструкцию формы. Orders list (cboOrders) и Products list (cboProducts) - это comboboxes, которые предоставляют список заказов и товаров. Вот мелочи, которые помогут вам сэкономить немного времени. Когда я строил форму, я спрятал все директивы ввода данных (data entry controls) и запустил каждую строку, как это требуется. Я присвоил свойству cboProducts.Visible значение False и попытался запустить его, когда запускал строку директив (row of controls). По какой-то причине каждый раз это порождает ошибку времени выполнения. Поэтому мне было интересно узнать, что произойдет, если поместить cboProducts в директиву Panel (Panel control). Я поместил Panel в форму (PnlProduct), поместил туда cboProducts, установил размер панели таким образом,чтобы он соответствовал cboProducts и задаю адрес (location) и видимость (visibility) для этой панели вместо того, чтобы задавать эти параметры для cboProducts. Это сработало хорошо.

Combobox контрол находится непосредственно под combobox контролом Orders- это cboProducts. CboProducts динамически помещается наверху первого textbox в строке (как, например, txtProductName_1), когда пользователь вводит данные в эту одну строку. 

Вы можете исследовать остальную часть интерфейса, выгрузив (downloading) эталонный код, находящийся по ссылке, приведенной в начале статьи. 

Рассмотрим код, который работает с транзакциями в клиенте (in the client). Когда пользователь кликает кнопку Injsert-ADO.NET Trx для того, чтобы вставить детали заказа (order details), выполняется событие cmdInsertADONet_Click. 

В первых трех строках события объявляются переменные: 
Dim sSQL As String
Dim sStatus As String = ""
Dim sOrderID As String
В этой точке вызывается метод CloseConnection (Закрыть соединение). Если соединения нет, ошибка не возникает, но если соединение имеется, то оно будет закрыто: 
oDB.CloseConnection()
Затем открывается соединение, и транзакция стартует: 
oDB.OpenConnectionTrans()
Следующие несколько строк кода довольно просты; они задают контролы, проверяют, было ли введено имя товара (product name), и получают текущий заказ: 
lblMessage.Text = ""
If txtProductName_1.Text <> "" Then
sOrderID = cboOrders.SelectedValue
If sOrderID = "" Then
lblMessage.Text = "You must select an order to add line items to"
Exit Sub
End If
Следующие несколько строк вставляют детали заказа в базу данных путем вызова функции InsertOrderDetail. Каждая из этих строчек вызывается в следующем формате, где значения из формы передаются в функцию: 
'Insert 1st order
sStatus = InsertOrderDetail(sOrderID, _
  txtProductID_1.Text, _
  txtUnitPrice_1.Text, _
  txtQuantity_1.Text, _
  txtDiscount_1.Text)
Единственные вещи, которые меняются для последующих вызовов, - это проверка sStatus с тем, чтобы удостовериться, что sStatus не содержит сообщений об ошибках, а директива (control) txtProductID не является пустой. Если эти критерии удовлетворены, осуществляется обращение к InsertOrderDetail: 
'Insert 2nd order
If sStatus = "" And txtProductID_2.Text <> "" Then
sStatus = InsertOrderDetail(sOrderID, _
  txtProductID_2.Text, _
  txtUnitPrice_2.Text, _
  txtQuantity_2.Text, _
  txtDiscount_2.Text)
End If
Я опустил вызовы для третьей и четвертой строк, т.к. они идентичны последней строки, за исключением того, что они ссылаются на разные директивы. 

После последнего обращения к InsertOrderDetail, вы можете очистить (clean up) транзакцию. Если sStatus ничего не содержит, возникает ошибка и транзакция откатывается назад (rolled back) путем вызова метода RollBackTransaction. В контроле lblMessage будут выданы сообщения об ошибке, как это можно видеть ниже: 
'Cleanup and rollback / commit
If sStatus <> "" Then
lblMessage.Text = _
  "Rolled back transaction due to error " _
  on row " _
  & " - " & sStatus
oDB.RollbackTransaction()
Exit Sub
End If
И, наконец, если ошибок не возникает, то для завершения транзакции вызывается метод CommitTransaction. LblMessage изменяется таким образом, чтобы отразить успешную запись в базу данных, и соединение закрывается: 
oDB.CommitTransaction()
lblMessage.Text = "Line items inserted ok"
oDB.CloseConnection()
End If
Функция InsertOrderDetail очень проста. Заголовок функции выглядит следующим образом: 
Function InsertOrderDetail(ByVal sOrderID As String, _
  ByVal sProductID As String, ByVal sUnitPrice As String, _
  ByVal sQuantity As String, ByVal sDiscount As String) _
  As String
Затем создаются две переменные 
Dim sSQL As String
Dim sInsertStatus As String
После этого создается инструкция SQL с использованием переданных параметров: 
sSQL = "INSERT INTO [Order Details] " _
  "(OrderID, ProductID, UnitPrice, Quantity," _ 
  "Discount) " 
sSQL &= "VALUES(" & sOrderID & "," & sProductID & "," _
  & sUnitPrice & ","
sSQL &= sQuantity & "," & sDiscount & ")"
Блок Try/Catch содержит вызов RunSQLNonQuery, которая действительно выполняет SQL. Обратите внимание, что генерируется исключение, если sInsertStatus устанавливается равным любому значению, отличному от пробела. Это дает возможность коду перехватывать в блоке Catch (Catch Block) все условия ошибки (error conditions). 
  Try
  sInsertStatus = oDB.RunSQLNonQuery(sSQL)
  If sInsertStatus <> "" Then
  Throw New System.Exception(sInsertStatus)
  End If
  Catch exc As Exception
  Return exc.Message
  End Try
End Function
Все очень просто. Вы можете видеть, что ADO.NET позволяет достаточно просто кодировать транзакции, если ваша база данных поддерживает их. 

Вторая часть вопроса месяца связана с производительностью (performance). Безусловно, поддержание соединения в открытом состоянии затрагивает с производительность. Как показано в данном примере, вы можете использовать ADO.NET для обработки транзакций и вам решать, сколько времени вы будете держать соединение открытым. ADO.NET также поддерживает пулинг соединений (connection pooling), поэтому открытие и закрытие соединений вызовет лишь незначительные издержки, если вы будете использовать одну и ту же строку соединения (connection string). 


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=23341