|
Введение в ADO.Net 2.0 на примере использования OLE DB-провайдера для Firebird
Примеры к статье
Предисловие
В данном обзоре будет описан один из способов работы с базой данных Firebird в среде .Net при помощи управляемого OLE DB-провайдера. Несомненным преимуществом сервера баз данных Firebird является его бесплатность. Firebird можно использовать в различных системах - начиная от однопользовательских настольных приложений со встроенной базой данных (Embed Database), до клиент-серверных приложений различного уровня.
Средства и технологии, используемые в статье:
ADO .Net 2.0.
Data Protection API (DAPI)
Visual Studio 2005 Professional
Firebird SQL Server 2.0
IBProvider v.3 и IBProvider v.2
IBProvider – OLE DB-провайдер для Firebird
Для взаимодействия с OLE DB-провайдерами в ADO .Net реализовано пространство имен System.Data.OleDb.При работе с Firebird я использую IBProvider (www.ibprovider.com) и в своем повествовании буду опираться, прежде всего, на его функциональность. IBProvider существует в двух вариантах – коммерческом и бесплатном. Список основных возможностей, а так же различий между версиями IBProvider вы можете изучить на сайте разработчиков из следующего документа: http:// www.ibprovider.com/rus/documentation/differences_between_versions.html
Методы подключения к базе данных
Параметры строки подключения
Для использования OLE DB-провайдера необходимо подключить к проекту соответствующее пространство имен:
Управление подключением к OLE DB-источникам данных осуществляется с помощью класса OleDbConnection. Самый простой способ подключения к БД – прямое указание строки подключения в конструкторе этого класса:
OleDbConnection con = new OleDbConnection(connectionString);
con.Open();
con.Close();
|
Для формирования строки подключения в Net 2.0 появился класс OleDbConnectionStringBuilder:
OleDbConnectionStringBuilder cb = new OleDbConnectionStringBuilder();
cb.Provider = "LCPI.IBProvider";
cb.Add("Location", @"localhost:d:\Program Files\Firebird\examples\EMPLOYEE.FDB");
cb.Add("User ID", "sysdba");
cb.Add("Password", "masterkey");
cb.Add("ctype", "win1251");
Console.WriteLine(cb)
|
Существует определенный набор свойств инициализации IBProvider, который необходимо установить перед выполнением соединения с БД:
Обязательные параметры подключения IBProvider
СвойствоОписание
| Data Source | Используется для задания дружественного имени для базы данных, например "Employee DB". Если не установлено свойство Location, то предполагается что в Data Source указано расположение базы данных. |
| Provider | Имя Ole Db провайдера |
| User ID | Имя пользователя базы данных |
| Password | Пароль пользователя |
| Ctype | Используемая кодировка |
Некоторые необязательные параметры подключения IBProvider
СвойствоОписание
| Location | Путь к базе данных на сервере. Если свойство Location не определено, то расположение базы данных берется из Data Source. |
| db_client_type | Тип клиента сервера базы данных. Есть только в IBProvider v3. |
| db_client_library | Клиентская DLL. |
| auto_commit | Режим автоматического подтверждения транзакций. Для его включения в строке подключения необходимо указать “auto commit =true”. |
| role | Роль пользователя. |
Более подробно о свойствах инициализации IBProvider можно прочитать здесь: http:// www.ibprovider.com/rus/documentation/property_001.html
СОВЕТ
Всегда включайте в параметр Location имя сервера БД. Это позволит обеспечить совместимость со всем версиями Firebird
|
Способы хранения строк подключения
В реальных приложениях никто не прописывает строки подключения к базе данных в коде. Гораздо эффективнее использовать для этой цели либо настройки приложения, либо отдельный файл подключения.
Для хранения параметров подключения в Windows существует специальный тип файлов Microsoft Universal Data Link – это файл с расширением udl. С этим расширением ассоциирован универсальный редактор подключений. IBProvider предоставляет удобный интерфейс для формирования параметров соединения. Чтобы использовать udl-файл, выполните следующие шаги:
Создайте пустой файл с расширением .udl.
Откройте файл (Enter), появится связанный с данным расширением диалог для настройки подключения.
В списке OleDb провайдеров выберете IBProvider v3:
Рисунок 1.
Задайте параметры подключения по аналогии с рисунком и нажмите кнопку «Проверить подключение»:
Рисунок 2.
На закладке дополнительных настроек задайте расширенные свойства подключения:
Рисунок 3.
Нажмите «ОК» для записи информации о подключении в файл.
Для использования подключения, описанного в 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):
Рисунок 4
При редактировании свойства запустится встроенный в VS 2005 редактор строки подключения:
Рисунок 5.
ПРИМЕЧАНИЕ
Если нажать на кнопку “Data Links”, появится уже знакомый диалог конфигурации Microsoft Data Link.
|
Прочитать строку подключения из файла конфигурации можно следующим образом:
// чтение свойства с именем ConnectionString
Console.WriteLine(Properties.Settings.Default.ConnectionString);
|
Для облегчения написания примеров был создан класс ConnectionProvider, который инкапсулирует в себе все описанные методы подключения. Команды
Команды предназначены для передачи запросов базе данных. Для OLE DB-провайдеров команда реализуется классом OleDbCommand. Команда всегда выполняется в контексте некоторого открытого подключения к БД в контексте транзакции. IBProvider также умеет работать в режиме автоматического старта/подтверждения транзакции. Подробнее о транзакциях будет рассказано ниже.
Чтобы выполнить запрос к БД, необходимо выполнить следующую последовательность действий:
Создать подключение к БД и открыть его.
Создать активную транзакцию из текущего подключения – метод OleDbConnection.BeginTransaction().
Создать объект OleDbCommand, используя либо один из вариантов перегруженного конструктора, либо метод OleDbConnection.CreateCommand().
Установить свойство команды Transaction, если оно не было задано в конструкторе.
Задать текст команды CommandText
Выполнить SQL-запрос, используя один из следующих методов: ExecuteScalar, ExecuteReader и ExecuteNonQuery.
Завершить (OleDbTransaction.Commit()) или откатить (OleDbTransaction.Rollback()) транзакцию, и закрыть подключение.
ПРИМЕЧАНИЕ
При использовании режима автоматического подтверждения транзакций пункты 2 и 7 необходимо пропустить. IBProvider сам позаботится о старте и завершении транзакции.
|
ExecuteScalar
Возвращает единственное значение первой колонки первой строки. Остальные результаты игнорируются. Этот метод полезен для запросов, которые, к примеру, подсчитывают количество записей в таблице – и, соответственно, возвращают только одно значение:
public void ExecuteScalarTest()
{
using(OleDbConnection con = ConnectionProvider.CreateConnection())
{
con.Open();
OleDbTransaction trans = con.BeginTransaction();
OleDbCommand cmd = new OleDbCommand(
"select count(*) from employee", con, trans);
Console.WriteLine("Record count:" + cmd.ExecuteScalar().ToString());
trans.Commit();
}
}
|
ExecuteReader
Данный метод возвращает объект OleDbDataReader. OleDbDataReader используется для последовательного считывания данных (ForwardOnly). При его использовании необходимо наличие открытого подключения к базе.
Навигация по строкам результирующего множества осуществляется при помощи метода Read(), который возвращает true, если строка была успешно считана в локальный буфер. После этого значения полей строки можно считать посредством метода GetValue. Перед первым вызовом метода Read() объект OleDbDataReader не позиционирован на первой строке результирующего множества, и для её прочтения необходимо сначала вызвать метод Read().
Наиболее удобным способом чтения данных из результирующего множества является использование метода Read() совместно с конструкцией while:
public void ExecuteReaderTest()
{
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), не возвращающих результирующих множеств. Несмотря на то, что с помощью этого метода нельзя выполнять простые запросы типа select, он позволяет выполнять хранимые процедуры, имеющие OUT-параметры:
public void ExecuteNonQueryTest()
{
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", "Copeck");
// количество обработанных строк
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. Они могут быть именованными и позиционными. Пример команды с позиционными параметрами:
insert into country (country, currency) values(?, ?)
|
С именованными:
insert into country (country, currency) values(:country, :currency)
|
IBProvider сам умеет формировать список параметров, производя анализ SQL выражения. К сожалению, в ADO.Net необходимо вручную добавлять эти параметры, т.к. команда не запрашивает их описание у OLE DB-провайдера.
Чтобы добавить параметр, нужно воспользоваться:
Методом 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:
cmd.Parameters.AddWithValue("currency", "Rouble");
cmd.Parameters.AddWithValue("country", "Russia");
|
Задавать значения параметров можно уже в произвольном порядке:
cmd.Parameters["country"].Value = "Latvia";
cmd.Parameters["currency"].Value = "Lat";
|
Вызов хранимых процедур
Существуют два способа возвратить результат запроса из хранимой процедуры:
Хранимая процедура возвращает результирующее множество.
Результат выполнения хранимой процедуры помещается в OUT-параметры команды.
Для первого способа используется обычная SQL-инструкция:
select * from stored_procedure_name(...)
|
Результат её выполнения обрабатывается при помощи объекта OleDbDataReader:
public void StoredProcedureResultSetTest()
{
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbTransaction trans = con.BeginTransaction();
// выбор хранимой процедуры
OleDbCommand cmdInParams =
new OleDbCommand("select cust_no from CUSTOMER", con, trans);
// Выбор адрес с помощью хранимой процедуры
OleDbCommand cmdStoredProc =
new OleDbCommand("select * from mail_label(:cust_no)", con, trans);
// добавление одного IN-параметра
cmdStoredProc.Parameters.Add("cust_no", OleDbType.Integer);
// Чтение
using (OleDbDataReader rdr =
cmdInParams.ExecuteReader(CommandBehavior.CloseConnection))
{
// Для каждого № покупателя
while (rdr.Read())
{
cmdStoredProc.Parameters["cust_no"].Value = rdr["cust_no"];
using (OleDbDataReader rdrOut = cmdStoredProc.ExecuteReader())
{
Console.WriteLine("Customer №" + rdr["cust_no"]);
while (rdr_out.Read())
for (int i = 0; i < rdrOut.FieldCount; i++)
Console.WriteLine(rdrOut.GetName(i) + "=" + rdrOut [i]);
Console.WriteLine();
}
}
}
}
|
Второй способ – вызов хранимой процедуры через инструкцию:
execute procedure stored_procedure_name
|
Результат выполнения помещается в OUT-параметры команды, которые предварительно необходимо описать:
public void StoredProcedureOutParamsTest()
{
OleDbConnection con = ConnectionProvider.CreateConnection();
con.Open();
OleDbTransaction trans = con.BeginTransaction();
OleDbCommand cmdInParams =
new OleDbCommand("select cust_no from CUSTOMER", con, trans);
// хранимая процедура
OleDbCommand cmdStoredProc =
new OleDbCommand("execute procedure mail_label(:cust_no)", con, trans);
// IN-параметр
cmdStoredProc.Parameters.Add("cust_no", OleDbType.BSTR);
// OUT-параметры
cmdStoredProc.Parameters.Add("line1", OleDbType.BSTR)
.Direction = ParameterDirection.Output;
cmdStoredProc.Parameters.Add("line2", OleDbType.BSTR)
.Direction = ParameterDirection.Output;
cmdStoredProc.Parameters.Add("line3", OleDbType.BSTR)
.Direction = ParameterDirection.Output;
cmdStoredProc.Parameters.Add("line4", OleDbType.BSTR)
.Direction = ParameterDirection.Output;
cmdStoredProc.Parameters.Add("line5", OleDbType.BSTR)
.Direction = ParameterDirection.Output;
cmdStoredProc.Parameters.Add("line6", OleDbType.BSTR)
.Direction = ParameterDirection.Output;
// чтение
using (OleDbDataReader rdr = cmdInParams.ExecuteReader())
{
// Для каждого № покупателя
while (rdr.Read())
{
cmdStoredProc.Parameters["cust_no"].Value = rdr["cust_no"];
cmdStoredProc.ExecuteNonQuery();
Console.WriteLine("Customer №" + rdr["cust_no"]);
Console.WriteLine(cmdStoredProc.Parameters["line1"].Value);
Console.WriteLine(cmdStoredProc.Parameters["line2"].Value);
Console.WriteLine(cmdStoredProc.Parameters["line3"].Value);
Console.WriteLine(cmdStoredProc.Parameters["line4"].Value);
Console.WriteLine(cmdStoredProc.Parameters["line5"].Value);
Console.WriteLine(cmdStoredProc.Parameters["line6"].Value);
Console.WriteLine("");
}
}
trans.Commit();
con.Close();
}
|
Работа с массивами
ADO.Net может работать с любыми типами данных. Для тех типов OLE DB, у которых нет прямого отображения на типы данных .Net, используется тип данных DBTYPE_VARIANT. Массивы относятся как раз к таким типам.
Следующий пример демонстрирует чтение и запись массива из 5 элементов:
public void ArrayReadWriteTest()
{
using(OleDbConnection con = ConnectionProvider.CreateConnection())
{
con.Open();
OleDbTransaction trans = con.BeginTransaction();
OleDbCommand cmd = new OleDbCommand(
"select job_code, job_grade, job_country, job_title, \n" +
"language_req from job", con, trans);
OleDbCommand cmdUpd = new OleDbCommand(
"update job set language_req = :language_reg where \n" +
"job_code = :job_code and job_grade = :job_grade and \n" +
"job_country = :job_country", con, trans);
cmdUpd.Parameters.Add("language_req", OleDbType.Variant);
cmdUpd.Parameters.Add("job_code", OleDbType.BSTR);
cmdUpd.Parameters.Add("job_grade", OleDbType.BSTR);
cmdUpd.Parameters.Add("job_country", OleDbType.BSTR);
using (OleDbDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
Console.WriteLine("JOB TITLE:" + rdr["job_title"].ToString());
// чтение массива
if (rdr["language_req"] != DBNull.Value)
{
Array strs = (Array)rdr["language_req"];
for (int i = strs.GetLowerBound(0);
i <= strs.GetUpperBound(0); i++)
{
// усечение символа \n на концах элементов массива
string trimmedValue =
strs.GetValue(i).ToString().Replace("\n", "");
strs.SetValue(trimmedValue, i);
// вывод значения
if (trimmedValue != "")
Console.WriteLine(trimmedValue);
}
// запись новых значений элементов массива без символа \n
cmdUpd.Parameters["language_req"].Value = strs;
cmdUpd.Parameters["job_code"].Value = rdr["job_code"];
cmdUpd.Parameters["job_grade"].Value = rdr["job_grade"];
cmdUpd.Parameters["job_country"].Value = rdr["job_country"];
// передача изменений в БД
Assert.IsTrue(cmdUpd.ExecuteNonQuery() == 1);
}
else
Console.WriteLine("No language specified");
Console.WriteLine("");
}
}
//откат сделанных изменений
trans.Rollback();
}
}
|
Работа с BLOB-полями
IBProvider поддерживает работу с BLOB-полями, содержащими текст и бинарные данные. При использовании этого провайдера работа с BLOB-полями происходит так же, как и с обычными типами данных:
public void BLOBReadWriteTest()
{
using(OleDbConnection con = ConnectionProvider.CreateConnection())
{
con.Open();
OleDbTransaction trans = con.BeginTransaction();
// Команда, читающая BLOB-поле
OleDbCommand cmd = new OleDbCommand(
"select proj_id, proj_name, proj_desc from project", con, trans);
// Команда, записывающая BLOB-поле
OleDbCommand cmdUpdate = new OleDbCommand(
"update project set proj_desc= :proj_desc where proj_id= :proj_id",
con, trans);
// Создание параметров типа BSTR
cmdUpdate.Parameters.Add("proj_desc", OleDbType.BSTR);
cmdUpdate.Parameters.Add("proj_id", OleDbType.BSTR);
using (OleDbDataReader rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
// чтение BLOB
Console.WriteLine("PROJECT: " + rdr["proj_name"]);
Console.WriteLine(rdr["proj_desc"]);
// запись BLOB
cmdUpdate.Parameters["proj_id"].Value = rdr["proj_id"];
// каждый раз меняем регистр в BLOB-поле
string newProjectDescription = rdr["proj_desc"].ToString();
if (new_project_description.ToUpper() != newProjectDescription)
newProjectDescription = newProjectDescription.ToUpper();
else
newProjectDescription = newProjectDescription.ToLower();
cmdUpdate.Parameters["proj_desc"].Value = newProjectDescription;
Assert.AreEqual(1, cmdUpdate.ExecuteNonQuery());
}
}
trans.Commit();
} //будет вызван метод IDispose.Dispose(). Соединение будет закрыто
}
|
Здесь тип параметров команды cmdUpdate установлен в OleDbType.BSTR. В этом случае провайдер корректно распознает тип параметров и произведет их преобразование к типам базы данных.
СОВЕТ
В примере OleDbConnection и OleDbDataReader использованы совместно с конструкцией using. Эти классы поддерживают интерфейс IDispose, который содержит метод Dispose(), отвечающий за разрушение объекта. В случае возникновения исключения в процессе выполнения операций с БД произойдет автоматический откат транзакции, и соединение будет закрыто.
|
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, как пишут во многих источниках информации, но и при работе с другими СУБД.
Использование транзакций
Автоматическое управление транзакциями
Любая операция с БД должна выполняться в контексте транзакции. В своих примерах я постоянно использовал метод OleDbConnection.BeginTransaction(), так как предпочитаю всегда явно управлять транзакциями.
IBProvider может управлять транзакциями автоматически. Для конфигурирования этого режима существуют несколько свойств инициализации, которые могут быть заданы в строке подключения:
auto_commit – включает режим автоматического управления транзакциями. Тип – boolean, по умолчанию – false.
auto_commit_level – задает уровень изоляции автоматических транзакций. Допустимые значения: Read Comitted, Repeatable Read, Snapshot. По умолчанию Repeatable Read. В ADO.Net уровни изоляции транзакций определены в перечислении IsolationLevel.
auto_commit_ddl – определяет режим выполнения DDL-запросов. Допустимые значения:
0 - Поддержка DDL-запросов отключена.
1 - Выполнять DDL-запрос в выделенной транзакции. Игнорируется, если установлено свойство auto_commit.
2 - Выполнять CommitRetaining после DDL-запроса. Игнорируется, если установлено свойство auto_commit.
Значение по умолчанию: 0.
ПРИМЕЧАНИЕ
DDL-запросы (CREATE/ALTER/DROP) позволяют управлять сущностями БД: таблицами, триггерами, хранимыми процедурами и т.п. Подробнее о DDL запросах читайте нже в соответствующем разделе.
|
auto_commit_ddl_level – задает уровень изоляции автоматических транзакций для DDL-запросов. Допустимые значения: Read Comitted, Repeatable Read, Snapshot. Значение по умолчанию: Read Commited.
Следующий пример демонстрирует включение режима автоматического управления транзакциями с уровнем изоляции RepeatableRead:
public void AutoCommitSessionTest()
{
OleDbConnectionStringBuilder builder =
ConnectionProvider.GetConnectionStringBuilder();
builder.Add("auto_commit", true);
builder.Add("auto_commit_level",
Convert.ToInt32(System.Data.IsolationLevel.RepeatableRead));
using(OleDbConnection con = new OleDbConnection(builder.ToString()))
{
con.Open();
OleDbCommand cmd =
new OleDbCommand("select count(*) from employee", con);
Assert.IsTrue((int)cmd.ExecuteScalar() > 0);
}
}
|
Уровни изоляции транзакций
IBProvider поддерживает три уровня изоляции транзакций: Read Committed, Repeatable Read и Snapshot. Чтобы задать уровень изоляции транзакции, необходимо в метод OleDbConnection.BeginTransaction() передать допустимое значение из перечисления System.Data.IsolationLevel.
Вложенные транзакции
Одной из замечательных возможностей IBProvider является использование вложенных транзакций. Уровень вложенности транзакций не ограничен. Чтобы включить использование вложенных транзакций, необходимо задать свойству инициализации nested_trans значение true. Следующий пример демонстрирует применение вложенных транзакций:
public void InternalTransactionTest()
{
OleDbConnectionStringBuilder builder =
ConnectionProvider.GetConnectionStringBuilderFromUDL();
// включить вложенные транзакции
builder.Add("nested_trans", true);
using(OleDbConnection con1 = new OleDbConnection(builder.ToString()))
{
con1.Open();
// основная транзакция
OleDbTransaction trans = con1.BeginTransaction();
// добавить новую запись
OleDbCommand cmdInsert = new OleDbCommand(
"insert into country (country, currency) values (:country, :currency)",
con1, trans);
cmdInsert.Parameters.AddWithValue(":country", "Russia");
cmdInsert.Parameters.AddWithValue(":currency", "Ruble");
Assert.AreEqual(1, cmdInsert.ExecuteNonQuery());
// запустить вложенную транзакцию
OleDbTransaction internalTransaction = trans.Begin();
// запустить вложенную транзакцию внутри вложенной
OleDbTransaction internalTransaction2 = internalTransaction.Begin();
// удалить запись во вложенной транзакции третьего уровня
OleDbCommand cmdDelete = new OleDbCommand(
"delete from country where country = ?", con1, internalTransaction2);
cmdDelete.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual(1, cmdDelete.ExecuteNonQuery());
// откатить вложенную транзакцию третьего уровня
internalTransaction2.Rollback();
// проверить, что запись не была удалена в транзакции второго уровня
OleDbCommand cmdCheck = new OleDbCommand(
"select count(*) from country where country = ?",
con1, internalTransaction);
cmdCheck.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual (1, cmdCheck.ExecuteScalar());
// удалить запись
cmdDelete.Transaction = internal_transaction;
Assert.AreEqual(1, cmdDelete.ExecuteNonQuery());
// зафиксировать изменения
internalTransaction.Commit();
// проверить в основной транзакции, что запись удалена из БД
cmd_check.Transaction = trans;
Assert.AreEqual(0, cmdCheck.ExecuteScalar());
trans.Commit();
}
}
|
TransactionScope и распределенные транзакции в Net 2.0
В .Net Framework 2 появилось новое пространство имен System.Transaction, предоставляющее поддержку распределенных транзакций. IBProvider поддерживает распределенные транзакции за счет расширения COM+ Microsoft Transaction Server (MTS). Распределенные транзакции позволяют, к примеру, выполнять действия с различными БД в контексте одной транзакции.
Для организации распределенных транзакций служит объект TransactionsScope. Если он обнаружит, что в контексте используется несколько подключений, он будет использовать распределенную транзакцию, иначе – локальную. Следующий пример иллюстрирует поведение объекта TransactionsScope:
public void TransactionScopeTest()
{
// TransactionScrope автоматически свяжет локальные транзакции с
// распределенной. В данном контексте будет две локальных транзакции
// на каждое подключение и одна распределенная.
using (TransactionScope scope = new TransactionScope())
{
// автоматически будет создана локальная транзакция
using(OleDbConnection con1 = ConnectionProvider.CreateConnection())
{
con1.Open();
// команда вставки
OleDbCommand cmdInsert = new OleDbCommand(
"insert into country (country, currency) values (:country, :currency)",
con1);
cmdInsert.Parameters.AddWithValue("country", "Russia");
cmdInsert.Parameters.AddWithValue("currency", "Rouble");
Assert.AreEqual(1, cmdInsert.ExecuteNonQuery());
// автоматически будет создана ещё одна локальная транзакция
OleDbConnection con2 = ConnectionProvider.CreateConnection();
con2.Open();
cmdInsert.Connection = con2;
cmdInsert.Parameters["country"].Value = "Latvia";
cmdInsert.Parameters["currency"].Value = "Lat";
Assert.AreEqual(1, cmdInsert.ExecuteNonQuery());
}
// Фиксация распределенной транзакции.
// Для всех локальных транзакций будет вызван метод Commit().
scope.Complete();
// Если ранее распределенная транзакция не была завершена, для всех локальных
// транзакций будет вызван метод Rollback при вызове IDispose.Dispose()
}
// TransactionScope будет использовать локальную транзакцию,
// т.к. все команды выполняются в одном контексте
using (TransactionScope scope = new TransactionScope())
{
using(OleDbConnection con1 = ConnectionProvider.CreateConnection())
{
con1.Open();
// Проверяем, что в предыдущем контексте были добавлены записи
OleDbCommand cmdSelect = new OleDbCommand(
"select count(*) from country where country= :country", con1);
cmdSelect.Parameters.Add("country", OleDbType.BSTR);
// Удаляем записи
OleDbCommand cmdDelete = new OleDbCommand(
"delete from country where country= :country", con1);
cmdDelete.Parameters.Add("country", OleDbType.BSTR);
cmdSelect.Parameters["country"].Value = "Russia";
cmdDelete.Parameters["country"].Value = "Russia";
Assert.AreEqual(1, cmdSelect.ExecuteScalar());
Assert.AreEqual(1, cmdDelete.ExecuteNonQuery());
cmdSelect.Parameters["country"].Value = "Latvia";
cmdDelete.Parameters["country"].Value = "Latvia";
Assert.AreEqual(1, cmdSelect.ExecuteScalar());
Assert.AreEqual(1, cmdDelete.ExecuteNonQuery());
}
scope.Complete(); // фиксирование транзакции
} // откат транзакции, в случае возникновения ошибки
}
|
Управление транзакциями через SQL
Помимо управления транзакциями через OLE DB-интерфейсы, IBProvider осуществляет специальную поддержку SQL-запросов для управления транзакциями: SET TRANSACTION, COMMIT, COMMIT RETAIN, ROLLBACK и, для Firebird 2, ROLLBACK RETAIN. Данный метод позволяет указывать специфические параметры контекста транзакции, которые не стандартизированы в OLE DB, но поддерживаются в Firebird. Следующий пример демонстрирует управление транзакциями через SQL:
public void SQLTransactionTest()
{
using(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();
}
}
|
СОВЕТ
Команды COMMIT RETAIN/ROLLBACK RETAIN позволяют фиксировать или откатывать изменения, произведенные внутри транзакции без закрытия её контекста. После выполнения этих команд транзакция может быть использована повторно.
|
Использование именованных точек сохранения
IBProvider позволяет использовать именованные точки сохранения внутри транзакций. Для задания новой точки необходимо выполнить SQL-запрос:
SAVEPOINT save_point_name
|
Чтобы откатить или зафиксировать транзакцию до определенной точки сохранения необходимо выполнить:
ROLLBACK TO SAVEPOINT save_point_name
|
или
COMMIT TO SAVEPOINT save_point_name
|
В следующем примере определяется одна точка сохранения между двумя командами:
public void SavePointTest()
{
using(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);
cmdInsert.Parameters.AddWithValue(":country", "Russia");
cmdInsert.Parameters.AddWithValue(":currency", "Ruble");
Assert.AreEqual(1, cmdInsert.ExecuteNonQuery());
new OleDbCommand("SAVEPOINT AFTER_INSERT_POINT",
con1, transaction).ExecuteNonQuery();
// удаляем запись, добавленную до точки сохранения
OleDbCommand cmdDelete = new OleDbCommand(
"delete from country where country = ?", con1, transaction);
cmdDelete.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual(1, cmdDelete.ExecuteNonQuery());
// отменяем операцию удаления, откатывая транзакцию до точки
// сохранения AFTER_INSERT_POINT
new OleDbCommand("ROLLBACK TO SAVEPOINT AFTER_INSERT_POINT",
con1, transaction).ExecuteNonQuery();
// проверяем, что запись не была удаления из БД
OleDbCommand cmdCheck = new OleDbCommand(
"select count(*) from country where country = ?", con1, transaction);
cmdCheck.Parameters.AddWithValue("?", "Russia");
Assert.AreEqual(1, cmdCheck.ExecuteScalar());
// откат всех сделанных изменений
transaction.Rollback();
}
}
|
Обработка ошибок
Класс OleDbException
Для обработки ошибок OLE DB в ADO.Net есть свой класс OleDbException, который, в отличие от стандартного класса Exception, предоставляет дополнительную информацию:
код ошибки OleDb.
коллекцию ошибок OleDbError.
В общем случае обработка ошибок выглядит следующим образом:
try
{
OleDbConnection con =
new OleDbConnection("Provider=LCPI.IBProvider;Data Source=Empty;");
con.Open();
con.Close();
}
catch (OleDbException oleEx)
{
foreach (OleDbError err in oleEx.Errors)
{
Console.WriteLine("Message: " + err.Message);
Console.WriteLine("Native Error: " + err.NativeError);
Console.WriteLine("Source: " + err.Source);
Console.WriteLine("SQL State : " + err.SQLState);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
|
В примере умышленно пропущены обязательные параметры строки подключения User ID и Password. В результате выполнения данного кода генерируется OLE DB-исключение. В коллекции ошибок будет содержаться два объекта OleDbError. На экран выводится следующее:
Message: Не определено обязательное свойство инициализации "Password".
Native Error: 0
Source: LCPI.IBProvider.2
SQL State :
Message: Не определено обязательное свойство инициализации "User ID".
Native Error: 0
Source: LCPI.IBProvider.2
SQL State :
|
Событие InfoMessage
В классе OleDbConnection определено событие InfoMessage, возникающее каждый раз, когда OLE DB-провайдер регистрирует информационные сообщения или предупреждения. Для обработки таких сообщений в пространстве имен System.Data.OleDb определен делегат OleDbInfoMessageEventHandler.
Свойства объектов OLE DB
В спецификации OLE DB определен ряд интерфейсов, позволяющих считывать или устанавливать дополнительную информацию объектов через наборы свойств. IBProvider активно использует эту возможность, позволяя задавать дополнительные параметры подключений, команд, транзакций и наборов рядов, а так же использует этот механизм для предоставления информации о БД (версия сервера, дата создания БД, используемая кодировка, размер страницы и т.д.). В ADO.Net нет стандартного способа прочитать или записать свойства, т.к. все интерфейсы, отвечающие за работу со свойствами, определены как internal sealed, т.е. доступны только для кода самого ADO.Net, и к тому же наследование от них запрещено. В результате исследования библиотеки был написан компонент, который позволяет считывать и устанавливать значения свойств таких объектов, как OleDbConnection, OleDbCommand, OleDbDataReader. Следующий пример демонстрирует применение этого компонента:
public void ReadPropertiesTest()
{
using(OleDbConnection con = ConnectionProvider.CreateConnection())
{
con.Open();
// Свойства OleDbConnection
OleDbProperties properties = OleDbProperties.GetPropertiesFor(con);
Assert.AreNotEqual(0, properties.Count);
PrintProperties(properties);
// Свойства OleDbCommand
OleDbCommand cmd =
new OleDbCommand("select * from country", con, con.BeginTransaction());
properties = OleDbProperties.GetPropertiesFor(cmd);
Assert.AreNotEqual(0, properties.Count);
PrintProperties(properties);
// Свойство из группы Rowset можно изменить
properties["Use Bookmarks"].Value = true;
Assert.IsTrue(Convert.ToBoolean(properties["Use Bookmarks"].Value));
// Свойства OleDbDataReader
using (OleDbDataReader rdr = cmd.ExecuteReader())
{
properties = OleDbProperties.GetPropertiesFor(rdr);
PrintProperties(properties);
}
}
}
private void PrintProperties(OleDbProperties properties)
{
foreach (OleDbProperty prop in properties.Values)
Console.WriteLine((prop.Required ? "[r] " : "")
+ prop.Name + "=" + prop.ValueString);
}
|
Компонент для управления свойствами
Компонент реализован в виде словаря OleDbProperties, индексированного по названию свойства, в котором присуствует фабричный метод GetPropertiesFor() (термин из [2]):
Фабричный метод обращается к поставщикам свойств, унаследованных от PropertyProviderBase. В компоненте реализовано три поставщика для объектов OleDbCommand, OleDbConnection и OleDbDataReader:
Рисунок 7.
Каждый из поставщиков возвращает свой набор свойств, соответствующий группам OLE DB:
Поставщик для OleDbConnection возвращает свойства из наборов Data Source Information, Data Source и Initialization.
Поставщики для OleDbCommand и OleDbDataReader возвращают свойства из группы Rowset с тем различием, что для команды можно задать значения свойств, а для OleDbDataReader их можно только читать.
Полный список свойств из группы Data Source Information, поддерживаемых IBProvider, доступен по адресу http://www.ibprovider.com/rus/documentation/property_003.html
Список свойств группы Initialization, которые могут быть заданы через параметры строки подключения, доступен по адресу http://www.ibprovider.com/rus/documentation/property_001.html
Свойства набора рядов из группы Rowset описаны в http://www.ibprovider.com/rus/documentation/property_005.html
Пул подключений
Пул подключений позволяет более эффективно управлять таким ресурсом, как соединение с БД. Когда после завершения очередной операции с БД вы вызываете метод 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-сервисов:
public void OleDbServicesTest()
{
const int connectionCount = 50;
// Хранит результат работы по всем операциям
Dictionary<string, double> timeResults = new Dictionary<string, double>();
OleDbConnectionStringBuilder builder =
ConnectionProvider.GetConnectionStringBuilderFromUDL();
// Все сервисы включены
builder.OleDbServices = OleDbServicesValues.EnableAll;
timeResults.Add(
String.Format("OLE DB Services=EnableAll ({0})", builder.OleDbServices),
DoConnections(builder, connectionCount));
// Все сервисы отключены
builder.OleDbServices = OleDbServicesValues.DisableAll;
timeResults.Add(
String.Format("OLE DB Services=DisableAll ({0})", builder.OleDbServices),
DoConnections(builder,connectionCount));
// Все включено, за исключением клиентских курсоров
// и работы за пределами сессии
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, connectionCount));
foreach (string key in timeResults.Keys)
Console.WriteLine(key + ". Seconds elapsed: " + timeResults[key]);
}
/// <summary>
/// Открывает и закрывает много подключений, а также начинает транзакции
/// </summary>
/// <param name="builder"></param>
/// <param name="cntConnectioncntConnection"></param>
/// <returns>Сколько секунд выполнялось</returns>
private double DoConnections(OleDbConnectionStringBuilder builder,
int connectionCount)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
sw.Start();
for (int i = 1; i <= connectionCount; i++)
{
using(OleDbConnection con = new OleDbConnection(builder.ToString()))
{
con.Open();
OleDbTransaction trans = con.BeginTransaction();
trans.Commit();
}
}
sw.Stop();
return sw.Elapsed.TotalSeconds;
}
|
Наиболее производительным будет вариант использования только пула ресурсов и автоматического подключения транзакций – это соответствует битовой маске OleDbServicesValues.EnableAll & ~OleDbServicesValues.ClientCursor &~ OleDbServicesValues.AggregationAfterSession (параметр “OLE DB Services =-13”).
Чуть ниже будет производительность при использовании всех OLE DB-сервисов. И наконец, производительность значительно падает (примерно в 20 раз) при полностью выключенных сервисах.
Подробнее об управлении пулом подключений можно прочитать в MSDN: http:// msdn.microsoft.com/library/en-us/oledb/htm/oledbdbprop_init_oledbservices.asp
Отсоединенная модель, DataSet
Заполнение объекта DataSet
Объект DataSet – основной элемент в поддержке отсоединенного сценария работы с распределенными данными в ADO.NET. DataSet – это представление данных в памяти, предоставляющее полноценную реляционную модель, независимо от источника данных. Его можно использовать с несколькими различными источниками данных, например, данными из БД и XML. Кроме того, он представляет собой довольно мощную структуру данных, пригодную для автономного использования. DataSet может содержать несколько таблиц, связанных ограничениями и реляционными отношениями.
Существует несколько способов заполнения объекта DataSet данными из БД. Первый из них, появившийся ещё в Net Framework 1.0 – это способ заполнения DataSet при помощи класса OleDbDataAdapter:
public void FillDataSetFromDataAdapter()
{
DataSet dataSet = 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(dataSet);
Assert.IsTrue(dataSet.Tables[0].Rows.Count > 0);
scope.Complete();
}
}
|
Второй способ появился только в ADO.Net 2.0 – это возможность заполнения DataSet с использованием OleDbDataReader:
public void FillDataSetFromDBReaderTest()
{
using(OleDbConnection con = ConnectionProvider.CreateConnection())
{
con.Open();
OleDbCommand cmd =
new OleDbCommand("select * from EMPLOYEE", con, con.BeginTransaction());
DataSet dataSet = new DataSet();
DataTable tbl = dataSet.Tables.Add("EMPLOYEE");
using (OleDbDataReader reader = cmd.ExecuteReader())
{
dataSet.Load(reader, LoadOption.OverwriteChanges, tbl);
}
}
}
|
DataTableReader
Класс DataTableReader является адаптером для классов DataTable или DataSet, позволяя использовать их в контекстах, где требуется ссылка на DbDataReader или IDataRecord.
Этот класс может быть полезен, для абстрагирования от конкретного источника данных, например, для реализации общего метода обработки данных, полученных от отсоединенного источника данных, и данных, которые формируются методом OleDbCommand.ExecuteReader() в подсоединенном режиме. Следующий пример демонстрирует использование общего метода PrintDBDataReader() для подсоединенного и отсоединенного режимов работы:
public void GetDBReaderFromDataTable()
{
DataSet dataSet = new DataSet();
DataTable tbl = dataSet.Tables.Add("EMPLOYEE");
// загрузка данных в DataSet
using(OleDbConnection con = ConnectionProvider.CreateConnection())
{
con.Open();
OleDbTransaction trans = con.BeginTransaction();
dataSet.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();
}
}
// / <summary>
// / Выводит в консоль данные из DBDataReader
// / </summary>
// / <param name="reader"></param>
public void 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]);
}
reader.Close();
}
|
Запись изменений в БД
После изменения данных в DataSet их необходимо передать обратно в базу. Для этого у объекта OleDbDataAdapter есть метод
www.rsdn.ru
Базы данных 19-03-2007 Oracle разработала архитектуру Oracle Application Integration Architecture for Communications 24-11-2007 Базы данных Подразделение Oracle Communications разработало архитектуру Oracle Application Integration Architecture for Communications и выпустило три первых пакета Process Integration Pack, предлагающих коммуникационным компаниям готовые средства интеграции процессов для приложений Oracle Siebel CRM, Oracle Communications Billing and Revenue Management и Oracle Financials. Также разработано решение Oracle Communications Unified Inventory Management. Oracle ...
Oracle ставит перед BEA ультиматум 24-10-2007 Базы данных Корпорация Oracle сообщила, что ее предложение о покупке компании BEA Systems действительно до предстоящего воскресенья, в противном случае Oracle просто купит несколько более мелких компаний, которые уже дали свое соглашение на поглощение.Напомним, что две недели назад BEA Systems распространила официальный ответ на предложение Oracle о покупке. В BEA сочли предложенные 6,66 млрд долларов недостаточными, а премию в 25% - слишком скромной. Ре...
Ларри Эллисон: Oracle будет активно поглощать компании 27-06-2007 Базы данных По словам основателя и главного исполнительного директора Oracle Ларри Эллисона, для того, чтобы выполнить стратегический план по продажам программных продуктов, компании предстоит проводить по одному поглощению в месяц на протяжении предстоящих двух с половиной лет.Выступая перед 6 000 менеджерами по продажам Oracle, Эллисон заявил, что компания намерена стать крупнейшим корпоративным покупателем в мире. Однако в результате столь агрессивной пол... |