Keywords: Firebird C#, Firebird 2.0, Firebird ADO Net провайдер, работа с Firebird, альтернатива Borlan DataProvider (BDP), доступ к Firebird, подключение к Firebird
Предисловие
В данном руководстве будет описан способ работы с базами данных Interbase и Firebird в ADO .Net при помощи управляемого Ole Db провайдера.
Firebird (Interbase) можно использовать для систем практически любого уровня, начиная от однопользовательских настольных приложений со встраиваемой базой данных (Embed Database), до клиент-серверных приложений уровня корпорации.
Ole Db провайдер представляет собой драйвер для доступа к БД через стандартные OLE DB интерфейсы.
OLE DB провайдеры можно использовать совместно со многими клиентскими библиотеками: ADO, dbGo (ADO Express), ADO .Net, и др.
Для доступа к Firebird и Interbase в качестве OLE DB драйвера можно использовать IBProvider.
Он поддерживает автоматическую настройку на работу с Firebird и Interbase всех версий, кроме того
стандарт OLE DB позволяет использовать его в качестве Firebird .Net провайдера, а так же Interbase .Net провайдера.
Пожалуй, это самый универсальный драйвер среди всех компонентов Firebird и Interbase, помимо ADO .Net, с его помощью можно разрабатывать
сценарии автоматизации на скриптовых языках (VBScript, JavaScript), использовать в автоматизации Microfost Office, подключать базы Firebird (Interbase)
к MS SQL Linked Server и к средствам
Business Intelligence, и реализовывать множество других преимуществ, которые доступны для стандартных интерфейсов доступа.
Параметры строки подключения для работы с Firebird, Interbase
Для доступа к Firebird, Interbase, а так же к любой другой БД из ADO .Net через OLE DB провайдер,
необходимо подключить соответствующее пространство имен к проекту Visual Studio:
using System.Data.OleDb;
Управление подключением к Ole Db источникам данных осуществляется с помощью класса OleDbConnection.
Самый простой способ подключения к базе данных – прямое указание строки подключения в конструкторе этого класса:
OleDbConnection con = new OleDbConnection(connectionString);
con.Open();
con.Close();
Для формирования строки подключения в Net 2.0 появился класс OleDbConnectionStringBuilder:
Существует определенный набор свойств инициализации IBProvider-a, который необходимо установить перед выполнением соединения с БД:
Обязательные свойства инициализации (параметры подключения) IBProvider:
Свойство
Описание
Location
Путь к базе данных на сервере.
Provider
Имя Ole Db провайдера
User ID
Имя пользователя базы данных
Password
Пароль пользователя
Ctype
Кодировка определяет, символы какого национального алфавита будут использоваться.
Для русского и английского алфавита можно использовать кодировку WIN1251
Некоторые необязательные свойства инициализации IBProvider:
Свойство
Описание
Data Source
Данное свойство используется для задания user friendly имени для базы данных,
например "Employee DB". Если свойство Loсation не определено, то предполагается, что в Data Source
указано расположение базы данных.
db_client_type
Тип клиента сервера базы данных. Есть только в IBProvider v3.
db_client_library
DLL с клиентом сервера
auto_commit
Режим автоматического подтверждения транзакций. Для его включения в строке подключения необходимо указать “auto commit =true”.
Всегда включайте в параметр Location имя сервера базы данных.
Это позволит обеспечить совместимость со всем версиями Firebird
Способы хранения строк подключения
В реальных приложениях никто не прописывает строки подключения к базе данных в коде.
Гораздо эффективнее использовать для этой цели либо настройки приложения (технология Settings), либо отдельный файл подключения.
Для хранения параметров подключения в Windows существует специальный тип файлов
Microsoft Data Link – это файл с расширением udl.
С этим расширением ассоциирован универсальный редактор подключений.
IBProvider поддерживает свои собственные табы, которые предоставляют удобный интерфейс для формирования параметров соединения.
Для того чтобы использовать udl файл в своем приложении выполните следующие шаги:
Создайте пустой файл с расширением .udl
Откройте файл (Enter), появится связанный с данным расширением диалог для настройки подключения:
В списке OleDb провайдеров выберете IBProvider v3:
Задайте параметры подключения по аналогии с рисунком и нажмите кнопку «Проверить подключение»:
На вкладке "Дополнительно" вы можете задать расширенные свойства подключения:
Нажмите «Ок» для записи информации о подключении в файл
Теперь для того чтобы использовать подключение, описанное udl файлом достаточно явно или через OleDbConnectionStringBuilder
задать свойство File Name
OleDbConnectionStringBuilder cb = new OleDbConnectionStringBuilder();
cb.FileName = AppDomain.CurrentDomain.BaseDirectory + @"\employee.udl";
OleDbConnection con = new OleDbConnection(cb.ToString());
con.Open();
Второй способ хранения строки подключения – это поместить её в конфигурационный файл приложения:
В свойствах проекта выберите вкладку Settings и создайте новое свойство с именем ConnectionString и типом (Connection string):
При редактировании свойства запустится встроенный в VS 2005 - 2008 редактор строки подключения:
ПРИМЕЧАНИЕ
Если нажать на кнопку “Data Links” появится уже знакомый нам диалог конфигурации Microsoft Data Link
Для того чтобы прочитать строку подключения из файла конфигурации необходимо создать экземпляр класса настроек вашего приложения:
Properties.Settings s = new Properties.Settings();
//чтение свойства с именем ConnectionString
Console.WriteLine(s.ConnectionString);
Для облегчения написания примеров был создан класс ConnectionProvider, который инкапсулирует в себе все, описанные методы подключения.
Шифрование строки подключения. Data Protection API
Один из вариантов защитить строку подключения в своем конфигурационном файле это воспользоваться Data Protection API (DAPI). Начиная с Windows 2000, DAPI является частью операционной системы.
Допустим, нам необходимо зашифровать данные, хранящиеся в секции connectionStrings.
Для этого мы воспользуемся классом DataProtectionConfigurationProvider:
Добавим к проекту ссылку на System.Configuration.dll и используем следующий код:
Данных хранящиеся в секции могут быть расшифрованы только на том компьютере, на котором были зашифрованы.
Таким образом, процедуру шифрования данных необходимо вызывать на компьютере конечного пользователя.
Данный пример как раз подходит для этих целей. При установке приложения мы помещаем в папку с программой ещё не зашифрованный App.Config.
При первом запуске приложения данная процедура его зашифрует и, в последствии, программа будет работать уже с защищенной секцией.
Можно так же вызывать процедуру шифрования во время установки приложения.
К сожалению, нет стандартных средств для защиты UDL файлов, поэтому стоит
воспользоваться способом хранения защищенных строк подключения в файле конфигурации.
Команды
Команды предназначены для передачи запросов базе данных. Для Ole Db провайдеров команда реализуется классом OleDbCommand.
Команда всегда выполняется для заданного открытого подключения к базе данных в контексте транзакции.
Для того чтобы выполнить запрос к базе данных необходимо выполнить следующую последовательность действий:
Создать подключение к БД и открыть его
Создать активную транзакцию из текущего подключения – метод OleDbConnection.BeginTransaction()
Создать объект OleDbCommand, либо используя один из вариантов перегруженного конструктора, либо метод OleDbConnection.CreateCommand()
Установить свойство команды Transaction, если оно не было задано в конструкторе
Задать текст команды CommandText
Для обращения к базе данных у команды есть три метода ExecuteScalar, ExecuteReader и ExecuteNonQuery.
Завершить транзакцию OleDbTransaction.Commit() или откатить OleDbTransaction.Rollback() и закрыть подключение.
ExecuteScalar
Возвращает единственное значение первой колонки первой строки. Остальные результаты игнорируются.
Этот метод полезен для запросов, которые, к примеру, считают количество записей в таблице – соответственно возвращают только одно значение:
Данный метод возвращает объект OleDbDataReader, которые по своему назначению очень близок объекту
Recordset из классического ADO. Он использует однонаправленное ForwardOnly чтение данных,
реализуя подсоединенную модель доступа. Таким образом, при его использовании необходимо наличие отрытого подключения к базе.
Навигация по строкам результирующего множества осуществляется при помощи метода Read(), который возвращает true,
в случае если ещё остались строки и false в противном случае. При вызове метода команды ExecuteReader(),
созданный им объект OleDbDataReaderне спозиционированн на первой строке результирующего множества и
для её прочтения необходимо сначала вызвать метод Read().
Наиболее удобным способом чтения данных из результирующего множества является использование метода Read() совместно с конструкцией while:
publicvoid ExecuteReaderTest()
{
// Пример: ExecuteReader через Firebird и Interbase oledb драйвер; работа с Interbase c#
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
//Испольуем метод CreateCommand для создания команды
OleDbCommand cmd = con.CreateCommand();
cmd.Transaction = con.BeginTransaction();
cmd.CommandText = "select * from employee";
OleDbDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
//чтение данныхwhile (rdr.Read())
{
string tmp ="";
for(int i=0; i<rdr.FieldCount -1;i++)
{
if (tmp != "") tmp += "\t";
tmp += rdr[i].ToString();
}
Console.WriteLine(tmp);
}
rdr.Close();
//после вызова OleDbDataReader.Close() подключение к БД будет закрыто
Assert.AreEqual(ConnectionState.Closed,con.State);
}
ПРИМЕЧАНИЕ
Обратите внимание что после вызова метода OleDbDataReader.Close(), подключение к базе данных будет закрыто.
Это произошло потому, что я применил перегруженный метод ExecuteReader() с заданным параметром CommandBehavior.CloseConnection.
По умолчанию после выполнения метода OleDbDataReader.Close() подключение к базе данных остается открытым
ExecuteNonQuery
Метод применяется для выполнения запросов, которые возвращают количество обработанных записей,
таких как insert, update, delete, а так же для выполнения хранимых процедур, результат
которых помещается в OUT параметры команды:
publicvoid ExecuteNonQueryTest()
{
// Пример ExecuteNonQuery через Firebird oledb драйвер, .Net провайдер (c#)
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbTransaction trans = con.BeginTransaction();
//INSERT
OleDbCommand cmd = new OleDbCommand(
"insert into country (country,currency) values(:country,:currency) ",
con, trans);
cmd.Parameters.AddWithValue("country", "Russia");
cmd.Parameters.AddWithValue("currency", "Kopec");
//количество обработанных строк
Assert.AreEqual(1, cmd.ExecuteNonQuery());
//UPDATE
cmd = new OleDbCommand(
"update country set currency=:currency where country =:country", con, trans);
cmd.Parameters.AddWithValue("currency", "Rouble");
cmd.Parameters.AddWithValue("country", "Russia");
//количество обработанных строк
Assert.AreEqual(1, cmd.ExecuteNonQuery());
//DELETE
cmd = new OleDbCommand(
"delete from country where country =:country", con, trans);
cmd.Parameters.AddWithValue("country", "Russia");
//количество обработанных строк
Assert.AreEqual(1, cmd.ExecuteNonQuery());
trans.Commit();
con.Close();
}
Параметры команд
В большинстве случаев при выполнении команды требуется задать её параметры.
Параметры добавляются в коллекцию Parameters. Они могут быть именованные и позиционные. Пример команды с позиционными параметрами:
insertinto country (country,currency) values(?,?)
С именованными:
insertinto country (country,currency) values(:country,:currency)
IBProvider сам умеет формировать список параметров, производя анализ SQL выражения. Но, к сожалению,
в ADO .Net необходимо вручную добавлять эти параметры, т.к. команда не запрашивает их описание у Ole Db провайдера.
Если вспомнить ADO, то в нем список параметров прекрасно формировался без необходимости вмешиваться в этот участок кода.
Для того чтобы добавить параметр нужно воспользоваться:
для добавления именованного параметра и значения метод AddWithValue()
для добавления как именованных, так и неименованных параметров перегруженный метод Add()
Если не указан тип параметра, он будет добавлен с Ole Db типом VarWChar,
что соответствует .Net типу string, что кажется разумным.
Об этом не стоит беспокоиться т.к. IBProvider корректно обрабатывает приведение любых типов Firebird.
Нельзя не сказать о существующих ограничениях при использовании именованных параметров совместно с OleDbCommand.
В MSDN написано, что именованные параметры поддерживаются только для поставщиков данных MSSQL и Oracle, а для поставщиков данных
Ole Db и ODBC поддерживаются только позиционные параметры. Использовать именованные параметры все же можно,
но их добавление в коллекцию Parameters необходимо осуществлять в том же порядке, в каком они следуют в запросе.
К примеру, если текст команды:
update country set currency=:currency where country =:country
то сначала необходимо добавить параметр currency, а потом country:
new!Еще одной разновидностью второго способа вызова хранимой процедуры является - указание имени процедуры
в тексте запроса и установка значения OleDbCommand.CommandType = StoredProdedure
В следующем примере из Хранимой процедуры получается следующее значение генератора:
CREATE PROCEDURE Next_Id
RETURNS(Id Integer)AS
BEGIN
Id = Gen_Id(GenId,1);
END
ADO .Net может работать с любыми типами данных. Для тех типов Ole Db, у которых нет прямого отображения на типы данных .Net используется тип данных DBTYPE_VARIANT. Массивы относятся как раз к таким типам.
Следующий пример демонстрирует чтение и запись массива из 5 элементов:
publicvoid ArrayReadWriteTest()
{
// Описание: МАССИВЫ Firebird, Interbase, чтение и запись массивов c#, ADO Net
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbTransaction trans = con.BeginTransaction();
OleDbCommand cmd = new OleDbCommand(
"select job_code, job_grade, job_country, job_title, language_req from job",
con, trans);
OleDbCommand cmd_upd = new OleDbCommand(
"update job set language_req=:language_reg where \n" +
"job_code=:job_code and job_grade=:job_grade and job_country=:job_country",
con, trans);
cmd_upd.Parameters.Add("language_req", OleDbType.Variant);
cmd_upd.Parameters.Add("job_code", OleDbType.BSTR);
cmd_upd.Parameters.Add("job_grade", OleDbType.BSTR);
cmd_upd.Parameters.Add("job_country", OleDbType.BSTR);
using (OleDbDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
Console.WriteLine("JOB TITLE:" + rdr["job_title"].ToString());
//чтение массиваobject lang_obj_arr = rdr["language_req"];
if (lang_obj_arr != DBNull.Value)
{
//преобразование к массиву//используем Array.CreateInstance для создания массива//из 5 элементов, с адресацией начиная с 1-го элемента, а не с 0short arr_lower_bound = 1;
Array lang_str_arr = Array.CreateInstance(typeof(string), newint[] {5}, newint[] { arr_lower_bound });
//копирование элементов в массив
((Array)lang_obj_arr).CopyTo(lang_str_arr, arr_lower_bound);
for (int i = arr_lower_bound; i < lang_str_arr.Length + arr_lower_bound; i++)
{
//усечение символа \n на концах элементов массиваstring trimmed_value = lang_str_arr.GetValue(i).ToString().Replace("\n", "");
lang_str_arr.SetValue(trimmed_value, i);
//вывод значенияif (lang_str_arr.GetValue(i).ToString() != "")
Console.WriteLine(lang_str_arr.GetValue(i));
}
//запись новых значений элементов массива без символа \n
cmd_upd.Parameters["language_req"].Value = lang_str_arr;
cmd_upd.Parameters["job_code"].Value = rdr["job_code"];
cmd_upd.Parameters["job_grade"].Value = rdr["job_grade"];
cmd_upd.Parameters["job_country"].Value = rdr["job_country"];
//предача изменений в БД
Assert.IsTrue(cmd_upd.ExecuteNonQuery() == 1);
}
else
Console.WriteLine("No language specified");
Console.WriteLine("");
}
}
//откат сделанных изменений
trans.Rollback();
con.Close();
}
ПРИМЕЧАНИЕ
В примере использован базовый класс Array и метод CreateInstance для создания массива строк.
В C# адресация массивов начинается с нулевого элемента,
а в данном случае в базе данных записан массив, который проиндексирован начиная с первого элемента.
Array.CreateInstance() позволяет указать нижнюю границу массива элементов.
В случае массивов с нулевой адресацией достаточно использования типизированных
наследников класса Array например string[], int[] и т.д.
Работа с BLOB полями в Firebird, Interbase
IBProvider поддерживает работу с двумя типами BLOB полей: содержащих текст и бинарные данные.
Благодаря встроенному в IBProvider конвертеру типов, работа с BLOB полями происходит так же, как и с обычными типами данных:
publicvoid BLOBReadWriteTest()
{
// Описание: Работа с BLOB полями, чтение и запись блобов, Firebird c#, Interbase ADO Net
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbTransaction trans = con.BeginTransaction();
//BLOB Read command
OleDbCommand cmd = new OleDbCommand(
"select proj_id, proj_name,proj_desc from project", con, trans);
//BLOB write command
OleDbCommand cmd_update = new OleDbCommand(
"update project set proj_desc=:proj_desc where proj_id=:proj_id", con, trans);
//create parameters with BSTR type
cmd_update.Parameters.Add("proj_desc", OleDbType.BSTR);
cmd_update.Parameters.Add("proj_id", OleDbType.BSTR);
using (OleDbDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
//чтение BLOB
Console.WriteLine("PROJECT: " + rdr["proj_name"].ToString());
Console.WriteLine(rdr["proj_desc"].ToString());
//запись BLOB
cmd_update.Parameters["proj_id"].Value = rdr["proj_id"];
//каждый раз меняем регистр данных в BLOB полеstring new_project_description = rdr["proj_desc"].ToString();
if (new_project_description.ToUpper() != new_project_description)
new_project_description = new_project_description.ToUpper();
else
new_project_description = new_project_description.ToLower();
cmd_update.Parameters["proj_desc"].Value = new_project_description;
Assert.AreEqual(1, cmd_update.ExecuteNonQuery());
}
}
trans.Commit();
con.Close();
}
Заметьте - тип параметров команды обновления установлен в OleDbType.BSTR. В этом случае провайдер корректно распознает тип параметров и произведет их преобразование к типам базы данных.
СОВЕТ
В примере OleDbDataReader использован совместно с конструкцией using. Он поддерживает интерфейс IDisposeи после завершения работы сам позаботится о своем закрытии, а если в метод OleDbCommand.ExecuteReader()передать значение CommandBehavior.CloseConnection, то так же будет закрыто подключение к базе данных.
new! В качестве еще одного примера работы с BLOB полями в Firebird (Interbase),
я хочу предложить вам, ставший уже классическим, пример записи рисунка в BLOB поле.
MARS - Multiple Active Result Sets
В ADO .Net 2.0 появилась «новая» технология, которая получила название MARS.
В Net Framework 1.1. в одном контексте транзакции было невозможно держать открытый OleDbDataReader и
параллельно выполнять дополнительные запросы к базе данных или открывать ещё один OleDbDataReader.
При попытке выполнить этот трюк мы получали исключение вида:
«There is already an open DataReader associated with this Connection which must be closed first.»
Предыдущий пример работы с BLOB полями как раз и показывает применение технологии MARS.
В нем демонстрируется последовательное чтение данных и их одновременное их обновление.
Если обратиться к истории, то мы обнаружим, что технология эта совсем не новая, да и технологией назвать это сложно.
Если сравнить вторую версию ADO .Net с первой, то, конечно, разработчики добились определенных успехов.
Но возможность использовать несколько RecordSet в одной транзакции была реализована ещё в классическом ADO.
Скажу больше, там можно было использовать несколько RecordSet, связанных с одной командой.
Это достигалось за счет клонирования команды внутри себя, если обнаруживалось, что уже есть связанное с ней множество.
В ADO .NET команда тоже умеет клонировать саму себя. Для этого есть метод Clone(), который необходимо вызывать явно,
если вы хотите связать несколько OleDbDataReader с одной командой.
Таким образом, применение MARS возможно не только для MS SQL Server, как пишут во многих источниках информации,
но и для других баз данных.
Использование транзакций
Автоматическое управление транзакциями в IBProvider
Любая операция с базой данных должна выполняться в контексте транзакции.
В своих примерах я постоянно использовал метод OleDbConnection.BeginTransaction(),
так как предпочитаю всегда явно управлять транзакциями.
IBProvider может управлять транзакциями автоматически.
Для конфигурирования этого режима существуют несколько свойств инициализации,
которые могут быть заданы в строке подключения:
auto_commit - Включает режим автоматического управления транзакциями.
Допустимые значения: true, false
Значение по умолчанию: false
auto_commit_level - Задает уровень изоляции автоматических транзакций.
Допустимые значения: Read Comitted, Repeatable Read, Snapshot
Значение по умолчанию: Repeatable Read
auto_commit_ddl - Определяет режим выполнения DDL запросов.
Значение по умолчанию: 0
Допустимые значения: 0 - Поддержка DDL запросов отключена 1- Выполнять DDL запрос в выделенной транзакции. Игнорируется если установлено свойство auto_commit 2- Выполнять CommitRetaining после DDL запроса. Игнорируется если установлено свойство auto_commit
auto_commit_ddl_level - Задает уровень изоляции автоматических транзакций для DDL запросов.
Допустимые значения: Read Comitted, Repeatable Read, Snapshot
Значение по умолчанию: Read Commited
СПРАВКА
DDL запросы - запросы на модификацию структуры БД (CREATE/ALTER/DROP).
Они позволяют управлять сущностями базы данных: таблицами, триггерами, хранимыми процедурами и т.п.
Следующий пример демонстрирует включение режима автоматического управления транзакциями с уровнем изоляции RepeatableRead:
publicvoid AutoCommitSessionTest()
{
// Описание: AutoCommit, AutoCommitLevel, автоматическое управление транзакциями, Interbase c#, Firebird ADO Net
OleDbConnectionStringBuilder builder =
ConnectionProvider.GetConnectionStringBuilder();
builder.Add("auto_commit", true);
builder.Add("auto_commit_level", Convert.ToInt32(System.Data.IsolationLevel.RepeatableRead));
OleDbConnection con = new OleDbConnection(builder.ToString());
con.Open();
OleDbCommand cmd = new OleDbCommand("select count(*) from employee", con);
Assert.IsTrue((int)cmd.ExecuteScalar() > 0);
con.Close();
}
Уровни изоляции транзакций в Firebird, Interbase
Уровень изоляции транзакции фактически задает область видимости данных между параллельно выполняющимися транзакциями.
IBProvider поддерживает три уровня изоляции транзакций: Read Committed, Repeatable Read и Snapshot.
Что бы задать уровень изоляции для транзакции необходимо в метод OleDbConnection.BeginTransaction()
передать допустимое значение из перечисления System.Data.IsolationLevel.
СОВЕТ
В ADO .Net уровни изоляции транзакций определены в перечислении IsolationLevel.
Если уровень изоляции транзакции не задан, то будет установлен ReadComitted. Подробно, использование уровней изоляции ADO .Net
я описал в статье
Новые возможности Firebird 2.1 и ADO .Net
Вложенные транзакции в Firebird, Interbase
Одной из замечательных возможностей, которые поддерживает IBProvider это использование вложенных транзакций.
Уровень вложенности транзакций не ограничен. Для того чтобы включить использование вложенных транзакций необходимо
установить свойство инициализации nested_trans = true.
Приведем пример работы с вложенными транзакциями для Interbase, Firebird c#:
publicvoid InternalTransactionTest()
{
//Описание: NESTED TRANSACTIONS, Firebird ADO .Net, C#, Firebird .Net провайдер
OleDbConnectionStringBuilder builder = ConnectionProvider.GetConnectionStringBuilderFromUDL();
//включить вложенные транзакции
builder.Add("nested_trans", true);
OleDbConnection con1 = new OleDbConnection(builder.ToString());
con1.Open();
//основная транзакция
OleDbTransaction trans = con1.BeginTransaction();
//добавить новую запись
OleDbCommand cmd_insert = new OleDbCommand(
"insert into country (country,currency) values (:country,:currency)", con1, trans);
cmd_insert.Parameters.AddWithValue(":country", "Russia");
cmd_insert.Parameters.AddWithValue(":currency", "Ruble");
Assert.AreEqual(1, cmd_insert.ExecuteNonQuery());
//запустить вложенную транзакцию
OleDbTransaction internal_transaction = trans.Begin();
//запустить вложенную транзакцию внутри вложенной
OleDbTransaction internal_transaction2 = internal_transaction.Begin();
//удалить запись во вложенно транзакции третьего уровня
OleDbCommand cmd_delete = new OleDbCommand(
"delete from country where country=?", con1, internal_transaction2);
cmd_delete.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual(1, cmd_delete.ExecuteNonQuery());
//откатить вложенную транзакцию третьего уровня
internal_transaction2.Rollback();
//проверить что запись не была удалена в тразакции второго уровня
OleDbCommand cmd_check = new OleDbCommand(
"select count(*) from country where country=?", con1, internal_transaction);
cmd_check.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual (1, cmd_check.ExecuteScalar());
//удалить запись
cmd_delete.Transaction = internal_transaction;
Assert.AreEqual(1, cmd_delete.ExecuteNonQuery());
//зафиксировать изменения
internal_transaction.Commit();
//проверить в основной транзакции что запись удалена из БД
cmd_check.Transaction = trans;
Assert.AreEqual(0, cmd_check.ExecuteScalar());
trans.Commit();
con1.Close();
}
TransactionScope и распределенные транзакции в Net 2.0
В Net Framework 2 появилось новое пространство имен System.Transaction, которое предоставляет поддержку распределенных транзакций.
IBProvider поддерживает распределенные транзакции за счет расширения COM+ Microsoft Transaction Server (MTS).
Распределенные транзакции позволяют нам преодолеть границы базы данных и выполнять действия с различными БД в контексте
одной распределенной транзакции. Таким образом, гарантируется, что данные у всех участников распределенной транзакции будут согласованы.
Для организации распределенных транзакций служит объект TransactionsScope.
В случае если он обнаружит, что его в контексте используется несколько подключений, он будет использовать распределенную транзакцию, иначе локальную.
Следующий пример иллюстрирует поведение объекта TransactionsScope:
publicvoid TransactionScopeTest()
{
//TransactionScrope автоматически свяжет локальные транзакции с распределенной//В данном контексте будет две локальных транзакции на каждое подключение и одна //распределенная.using (TransactionScope scope = new TransactionScope())
{
//автоматически будет создана локальная транзакция
OleDbConnection con1 = ConnectionProvider.CreateConnection();
con1.Open();
//insert command
OleDbCommand cmd_insert = new OleDbCommand(
"insert into country (country,currency) values (:country,:currency)",con1);
cmd_insert.Parameters.AddWithValue("country", "Russia");
cmd_insert.Parameters.AddWithValue("currency", "Rouble");
Assert.AreEqual(1, cmd_insert.ExecuteNonQuery());
//автоматически будет создана ещё одна локальная транзакция
OleDbConnection con2 = ConnectionProvider.CreateConnection();
con2.Open();
cmd_insert.Connection = con2;
cmd_insert.Parameters["country"].Value = "Latvia";
cmd_insert.Parameters["currency"].Value = "Lat";
Assert.AreEqual(1, cmd_insert.ExecuteNonQuery());
//Фиксация распределенной транзакции //Для всех локальных транзакций будет вызван метод Commit()
scope.Complete();
//Если ранее распределенная транзакция не была завершена для всех локальных //транзакций будет вызван метод Rollback при вызове IDispose.Dispose()
}
//TransactionScope будет использовать локальную транзакцию//т.к. все команды выполняются в одном контекстеusing (TransactionScope scope = new TransactionScope())
{
OleDbConnection con1 = ConnectionProvider.CreateConnection();
con1.Open();
//проверяем, что в предыдущем контексте были добавлены записи
OleDbCommand cmd_select = new OleDbCommand(
"select count(*) from country where country=:country", con1);
cmd_select.Parameters.Add("country", OleDbType.BSTR);
//удаляем записи
OleDbCommand cmd_delete = new OleDbCommand(
"delete from country where country=:country", con1);
cmd_delete.Parameters.Add("country", OleDbType.BSTR);
cmd_select.Parameters["country"].Value = "Russia";
cmd_delete.Parameters["country"].Value = "Russia";
Assert.AreEqual(1, cmd_select.ExecuteScalar());
Assert.AreEqual(1, cmd_delete.ExecuteNonQuery());
cmd_select.Parameters["country"].Value = "Latvia";
cmd_delete.Parameters["country"].Value = "Latvia";
Assert.AreEqual(1, cmd_select.ExecuteScalar());
Assert.AreEqual(1, cmd_delete.ExecuteNonQuery());
scope.Complete(); //commit
} //rollback
}
Управление транзакциями Firebird, Interbase через SQL
Помимо управления транзакциями через Ole Db интерфейсы, IBProvider осуществляет специальную поддержку
SQL – запросов для управления транзакциями: SET TRANSACTION, COMMIT, COMMIT RETAIN, ROLLBACK
и для Firebird 2 – ROLLBACK RETAIN.
Данный метод позволяет указывать специфические параметры контекста транзакции, которые не стандартизированы в OLE DB,
но поддерживаются в Firebird. Следующий пример демонстрирует управление транзакциями через SQL:
publicvoid SQLTransactionTest()
{
//Описание: SET TRANSACTION READ ONLY, COMMIT RETAIN, Firebird ADO Net, C#
OleDbConnection con1 = ConnectionProvider.CreateConnection();
con1.Open();
OleDbCommand cmd = new OleDbCommand(
"SET TRANSACTION READ ONLY WAIT ISOLATION LEVEL READ COMMITTED", con1);
cmd.ExecuteNonQuery();
cmd.CommandText = "select count(*) from employee";
Assert.AreNotEqual(0, cmd.ExecuteScalar());
//поддтверждение транзакции с последующим использованием её контекста
cmd.CommandText = "COMMIT RETAIN";
cmd.ExecuteNonQuery();
cmd.CommandText = "select count(*) from employee";
Assert.AreNotEqual(0, cmd.ExecuteScalar());
cmd.CommandText = "COMMIT";
cmd.ExecuteNonQuery();
con1.Close();
}
Использование именованных точек сохранения
IBProvider позволяет использовать именованные точки сохранения внутри транзакций.
Для задания новой точки необходимо выполнить SQL запрос:
SAVEPOINT save_point_name
Для того чтобы откатить или зафиксировать транзакцию до определенной точки сохранения необходимо выполнить:
ROLLBACK TO SAVEPOINT save_point_name илиCOMMIT TO SAVEPOINT save_point_name
В следующем примере определяется одна точка сохранения между двумя командами:
publicvoid SavePointTest()
{
//Описание: SAVEPOINT, ROLLBACK TO SAVEPOINT, Firebird .Net провайдер, Interbase c#
OleDbConnection con1 = ConnectionProvider.CreateConnection();
con1.Open();
OleDbTransaction transaction = con1.BeginTransaction();
OleDbCommand cmd_insert = new OleDbCommand(
"insert into country (country,currency) values (:country,:currency)", con1, transaction);
cmd_insert.Parameters.AddWithValue(":country", "Russia");
cmd_insert.Parameters.AddWithValue(":currency", "Ruble");
Assert.AreEqual(1, cmd_insert.ExecuteNonQuery());
new OleDbCommand("SAVEPOINT AFTER_INSERT_POINT", con1, transaction).ExecuteNonQuery();
//delete country in using 3-level internal transaction context
OleDbCommand cmd_delete = new OleDbCommand(
"delete from country where country=?", con1, transaction);
cmd_delete.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual(1, cmd_delete.ExecuteNonQuery());
new OleDbCommand("ROLLBACK TO SAVEPOINT AFTER_INSERT_POINT", con1, transaction).ExecuteNonQuery();
//check what record was not deleted from Firebird database
OleDbCommand cmd_check = new OleDbCommand(
"select count(*) from country where country=?", con1, transaction);
cmd_check.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual(1, cmd_check.ExecuteScalar());
transaction.Rollback();
con1.Close();
}
Обработка ошибок в Visual Studio
Класс OleDbException. Пример на C#
Для обработки ошибок Ole Db в Ado .Net есть свой класс OleDbException, который в отличие от стандартного класса Exception предоставляет дополнительную информацию:
Код ошибки OleDb
Коллекцию ошибок OleDbError
В общем случае обработка ошибок OLE DB в c# выглядит следующим образом:
В примере умышленно пропущены обязательные параметры строки подключения User ID и Password. В результате выполнения данного кода будет сгенерировано Ole Db исключение. В коллекции ошибок будет содержаться два объекта OleDbError. На экран будет выведено следующее:
Предназначено для обработки предупреждений или информационных сообщений поступающих от Ole Db провайдера.
Свойства объектов Ole Db - OleDbProperties .Net
В предыдущих версиях библиотеки ADO у объектов Connection, Command и Recordset
был набор свойств Properties, при помощи, которого можно было устанавливать и считывать свойства
соответствующих Ole Db интерфейсов: IDBProperties, ISessionProperties, ICommandProperties, IRowsetIndex.
По непонятным мне причинам разработчики ADO .Net исключили чтение/запись свойств напрямую, лишив нас возможности получения
расширенной информации об источнике данных, а так же тонкой настройки поведения провайдера.
В результате исследования библиотеки ADO .Net было найдено решение данной проблемы, которое было оформлено в отдельный компонент:
publicvoid ReadPropertiesTest()
{
// Описание: работа с OleDbProperties в IBProvider, Firebird (Interbase) c#
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
//OleDbConnection properties
OleDbProperties properties = OleDbProperties.GetPropertiesFor(con);
Assert.AreNotEqual(0, properties.Count);
PrintProperties(properties);
//OleDbCommand properties
OleDbCommand cmd = new OleDbCommand("select * from country", con, con.BeginTransaction());
properties = OleDbProperties.GetPropertiesFor(cmd);
Assert.AreNotEqual(0, properties.Count);
PrintProperties(properties);
//Property from Rowset ole db group can be changed
properties["Use Bookmarks"].Value = true;
Assert.IsTrue(Convert.ToBoolean(properties["Use Bookmarks"].Value));
//OleDbDataReader propertiesusing (OleDbDataReader rdr = cmd.ExecuteReader())
{
properties = OleDbProperties.GetPropertiesFor(rdr);
PrintProperties(properties);
}
con.Close();
}
privatevoid PrintProperties(OleDbProperties properties)
{
foreach (OleDbProperty prop in properties.Values)
Console.WriteLine((prop.Required ==true ? "[r] " : "") +
prop.Name + "=" + prop.ValueString);
}
Описание компонента для управления свойствами OleDbProperties .Net
Решение было оформлено в виде компонента, который поставляется в виде исходных текстов вместе с примерами к статье.
Он реализован в виде словаря OleDbProperties индексированного по названию свойства,
в котором присуствует фабричный метод GetPropertiesFor():
Фабричный метод обращается к поставщикам свойств унаследованных от PropertyProviderBase.
В компоненте реализовано три поставщика для объектов OleDbCommand, OleDbConnection и OleDbDataReader:
Каждый из поставщиков возвращает свой набор свойств, соответствующий группам Ole Db:
поставщик для OleDbConnection возвращает свойства из наборов Data Source Information, Data Source и Initialization
поставщик для OleDbCommand и поставщик для OleDbDataReader возвращают свойства из группы Rowset с тем различием,
что для команды можно установить свойства, а для OleDbDataReader-а их можно только читать.
Пул подключений позволяет более эффективно управлять таким ресурсом как соединение с базой данных. Когда после завершения очередной операции с базой данных вы вызываете метод OleDbConnection.Close() соединение с базой отправляется в пул и остается открытым там ещё некоторое время. По умолчанию это время - 60 секунд. Когда клиент инициирует новое соединение с базой, провайдер сначала запрашивает его из пула и только потом, если такого не нашлось, создает новый ресурс. Подходит соединение или нет, определяется по точному соответствию свойства ConnectionString с учетом регистра.
Настройка пула подключений осуществляется при помощи параметра строки подключения «Ole Db Services». Значение параметра – это битовая комбинация следующих флагов:
Флаг
Знач.
OLE DB сервисы
DBPROPVAL_OS_ENABLEALL
-1
Используются все сервисы
DBPROPVAL_OS_RESOURCEPOOLING
1
Ресурсы должны помещаться в пул
DBPROPVAL_OS_TXNENLISTMENT
2
При необходимости сессии должны быть автоматически подключены к глобальной транзакции
DBPROPVAL_OS_AGR_AFTERSESSION
8
Поддержка операций за пределами сессии
DBPROPVAL_OS_CLIENTCURSOR
4
Поддержка клиентских курсоров на уровне OLE DB Services, если их не поддерживает управляемый провайдер
DBPROPVAL_OS_DISABLEALL
0
Все сервисы отключены
В примерах к статье класс OleDbServicesValues содержит константы для всех этих флагов. Для комбинации флагов можно использовать операцию побитового исключения (& ~) констант невостребованных сервисов из константы DBPROPVAL_OS_ENABLEALL.
Следующий пример тестирует производительность при использовании различных Ole Db сервисов:
publicvoid OleDbServicesTest()
{
constint connection_count = 50;
//хранит результат работы по всем операциям
Dictionary<string, double> timeResults = new Dictionary<string, double>();
OleDbConnectionStringBuilder builder =
ConnectionProvider.GetConnectionStringBuilderFromUDL();
//OLE DB SERVISES = Все сервисы включены
builder.OleDbServices = OleDbServicesValues.EnableAll;
timeResults.Add(
String.Format("OLE DB Services=EnableAll ({0})", builder.OleDbServices),
DoConnections(builder, connection_count));
//OLE DB SERVISES = все сервисы отключены
builder.OleDbServices = OleDbServicesValues.DisableAll;
timeResults.Add(
String.Format("OLE DB Services=DisableAll ({0})", builder.OleDbServices),
DoConnections(builder, connection_count));
//OLE DB SERVICES = все включено, за исключением клиентских курсоров//и работы за пределами сессии
builder.OleDbServices = (OleDbServicesValues.EnableAll &
~ OleDbServicesValues.ClientCursor &
~ OleDbServicesValues.AggregationAfterSession);
timeResults.Add(
String.Format("OLE DB Services=\n"+
"\tEnableAll & \n" +
"\t~ClientCursor & \n" +
"\t~AggregationAfterSession ({0})", builder.OleDbServices),
DoConnections(builder, connection_count));
foreach (string key in timeResults.Keys)
Console.WriteLine(key + ". Seconds elapsed: " + timeResults[key]);
}
/// <summary>/// Открывает и закрывает много подключений, а так же стартует транзакции/// </summary>/// <param name="builder"></param>/// <param name="cnt_connection"></param>/// <returns>Сколько секунд выполнялось</returns> privatedouble DoConnections(OleDbConnectionStringBuilder builder,
int cnt_connection)
{
DateTime startTime = DateTime.Now;
for (int i = 1; i <= cnt_connection; i++)
{
OleDbConnection con = new OleDbConnection(builder.ToString());
con.Open();
OleDbTransaction trans = con.BeginTransaction();
trans.Commit();
con.Close();
}
return DateTime.Now.Subtract(startTime).TotalSeconds;
}
Наиболее производительным будет вариант при использовании только пула ресурсов и автоматического подключения транзакций – это соответствует битовой маске OleDbServicesValues.EnableAll & ~OleDbServicesValues.ClientCursor &~ OleDbServicesValues.AggregationAfterSession (параметр “OLE DB Services =-13”) .
Чуть медленнее будет работать при использовании всех Ole Db сервисов. И наконец производительность значительнопадает (примерно в 20 раз) при полностью выключенных сервисах.
Отсоединенная модель. DataSet
Заполнение объекта DataSet
Класс DataSet служит для хранения данных, загруженных из базы, в памяти.
Фактически он представляет собой набор таблиц связанных отношениями и в идеальном случае копирует структуру исходной базы данных.
Он позволяет существенно сократить количество обращений к базе данных.
Это особенно критично для WEB-приложений, для которых частое подключение к базе данных не является оптимальным.
Существуют несколько способов заполнения объекта DataSet:
Первый способ появился ещё в Net Framework 1.0 - это использование класса OleDbDataAdapter:
publicvoid FillDataSetFromDataAdapter()
{
// Описание: Заполнение DataSet через OleDbDataAdapter, работа с Firebird, Interbase
DataSet ds = new DataSet();
using (System.Transactions.TransactionScope scope =
new System.Transactions.TransactionScope())
{
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbDataAdapter adapter =
new OleDbDataAdapter("select * from EMPLOYEE", con);
adapter.Fill(ds);
Assert.IsTrue(ds.Tables[0].Rows.Count > 0);
scope.Complete();
}
}
Второй способ появился только в ADO .Net 2.0 – это возможность заполнения DataSet используя OleDbDataReader:
publicvoid FillDataSetFromDBReaderTest()
{
// Описание: Заполнение DataSet через OleDbDataReader, ADO .Net компоненты Firebird и Interbase
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbCommand cmd =
new OleDbCommand("select * from EMPLOYEE",con, con.BeginTransaction());
DataSet ds = new DataSet();
DataTable tbl = ds.Tables.Add("EMPLOYEE");
using (OleDbDataReader reader =
cmd.ExecuteReader(CommandBehavior.CloseConnection))
{
ds.Load(reader, LoadOption.OverwriteChanges, tbl);
}
}
DataTableReader
Данный класс позволяет использовать объект DataTable в режиме однонаправленного ForwardOnly чтения.
Он, так же как и OleDbDataReader наследуется от базового класса DBDataReader.
Этот способ чтения таблиц DataSet может быть полезен, когда используются общие методы для отсоединенного источника
данных и данных, которые формируются в подсоединенном режиме (OleDbCommand.ExecuteReader()). К примеру, такая ситуация нередкость,
когда операции работы с базой данных нужно вынести в веб-сервис. В таком случае, чтобы не переписывать большое количество кода, используещего
OleDbDataRead, применяют DataTableReader к данным DataSet.
Следующий пример демонстрирует использование общего метода PrintDBDataReader() для подсоединенного и отсоединенного режимов работы:
publicvoid GetDBReaderFromDataTable()
{
// Описание: DataTableReader - последовательное, однонаправленное чтение виртуальной таблицы, ADO Net компоненты Interbase (Firebird)
DataSet ds = new DataSet();
DataTable tbl = ds.Tables.Add("EMPLOYEE");
//загрузка данных в dataSet
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbTransaction trans = con.BeginTransaction();
ds.Load(new OleDbCommand(
"select * from EMPLOYEE",con,trans).ExecuteReader(),
LoadOption.OverwriteChanges, tbl);
//используем DataTable в режиме однонаправленного чтенияthis.PrintDBDataReader(new DataTableReader(tbl));
//OleDbDataReader и подсоединенный режимthis.PrintDBDataReader(
new OleDbCommand("select * from EMPLOYEE", con, trans).ExecuteReader());
trans.Commit();
con.Close();
}
/// <summary>/// Выводит в консоль данные из DBDataReader/// </summary>/// <param name="reader"></param>publicvoid PrintDBDataReader(System.Data.Common.DbDataReader reader)
{
while (reader.Read())
{
Console.WriteLine("*********************************");
for (int i = 0; i < reader.FieldCount; i++)
Console.WriteLine(reader.GetName(i) + "=" + reader[i].ToString());
}
reader.Close();
}
Передача изменений обратно в базу данных
После того как в DataSet были внесены изменения, их необходимо передать обратно в базу.
Для этого у объекта OleDbDataAdapter есть метод Update().
Прежде чем его использовать, необходимо настроить наш адаптер. В этом нам поможет класс OleDbCommandBuilder.
Он позволяет сгенерировать команды для операций вставки, обновления и удаления, а так же создать соответствующую коллекцию параметров команд.
Ниже приведен пример передачи изменений из DataSet в базу данных:
publicvoid UpdateDataSet()
{
// Описание: OleDbDataAdapter.Update(DataTable) - передача изменений в СУБД Firebird, Interbase c#
DataSet ds = new DataSet();
DataTable tbl = ds.Tables.Add("EMPLOYEE");
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbDataAdapter adapter = new OleDbDataAdapter("select * from EMPLOYEE", con);
adapter.SelectCommand.Transaction = con.BeginTransaction();
adapter.Fill(tbl);
//вносим изменения в DataSetforeach (DataRow row in tbl.Rows)
row["FIRST_NAME"] = row["FIRST_NAME"].ToString().ToUpper();
//генерируем команды для операций update, insert и delete
OleDbCommandBuilder cmd_builder = new OleDbCommandBuilder(adapter);
adapter.DeleteCommand = cmd_builder.GetDeleteCommand();
adapter.UpdateCommand = cmd_builder.GetUpdateCommand();
adapter.InsertCommand = cmd_builder.GetInsertCommand();
//обновление данных
adapter.Update(tbl);
//откат сделанных изменений
adapter.SelectCommand.Transaction.Rollback();
con.Close();
}