Сообщений 0    Оценка 75        Оценить  
Система Orphus

Введение в ADO.Net 2.0 на примере использования OLE DB-провайдера для Firebird

Автор: Андрей Меркулов
Источник: RSDN Magazine #3-2006
Опубликовано: 06.12.2006
Исправлено: 10.12.2016
Версия текста: 1.1
Предисловие
IBProvider – OLE DB-провайдер для Firebird
Методы подключения к базе данных
Параметры строки подключения
Способы хранения строк подключения
Для облегчения написания примеров был создан класс ConnectionProvider, который инкапсулирует в себе все описанные методы подключения. Команды
ExecuteScalar
ExecuteReader
ExecuteNonQuery
Параметры команд
Вызов хранимых процедур
Работа с массивами
Работа с BLOB-полями
MARS – Multiple Active Result Sets
Использование транзакций
Автоматическое управление транзакциями
Уровни изоляции транзакций
Вложенные транзакции
TransactionScope и распределенные транзакции в Net 2.0
Управление транзакциями через SQL
Использование именованных точек сохранения
Обработка ошибок
Класс OleDbException
Событие InfoMessage
Свойства объектов OLE DB
Компонент для управления свойствами
Пул подключений
Отсоединенная модель, DataSet
Заполнение объекта DataSet
DataTableReader
Запись изменений в БД
Поддержка новых возможностей Firebird 2
Специальная поддержка DML (Data Manipulation Language)
Другие изменения
Поддержка визуальных средств VS 2005
Создание подключения в Server Explorer
Создание каркаса приложения
Класс TableAdapter
Передача изменений в базу данных через TableAdapter
BindingSource
Добавление логики управления данными
Схемы метаданных БД
DDL-запросы CREATE, ALTER и DROP
Управляющие последовательности ODBC
Заключение
Полезные ссылки
Список литературы

Примеры к статье

Предисловие

В данном обзоре будет описан один из способов работы с базой данных Firebird в среде .Net при помощи управляемого OLE DB-провайдера. Несомненным преимуществом сервера баз данных Firebird является его бесплатность. Firebird можно использовать в различных системах - начиная от однопользовательских настольных приложений со встроенной базой данных (Embed Database), до клиент-серверных приложений различного уровня.

Средства и технологии, используемые в статье:

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-провайдера необходимо подключить к проекту соответствующее пространство имен:

        using System.Data.OleDb;

Управление подключением к 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-файл, выполните следующие шаги:


Рисунок 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 также умеет работать в режиме автоматического старта/подтверждения транзакции. Подробнее о транзакциях будет рассказано ниже.

Чтобы выполнить запрос к БД, необходимо выполнить следующую последовательность действий:

ПРИМЕЧАНИЕ

При использовании режима автоматического подтверждения транзакций пункты 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-провайдера.

Чтобы добавить параметр, нужно воспользоваться:

Если не указан тип параметра, при добавлении будет использоваться 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";

Вызов хранимых процедур

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

Для первого способа используется обычная 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-запросы (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, предоставляет дополнительную информацию:

В общем случае обработка ошибок выглядит следующим образом:

        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);
    }

  }
}

privatevoid 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:

Полный список свойств из группы 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()
{
  constint 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> privatedouble 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); 
        
    // Печать данных из DataTablethis.PrintDBDataReader(new DataTableReader(tbl));

    // Чтение данных полученных с помощью OleDbDataReaderthis.PrintDBDataReader(
      new OleDbCommand("select * from EMPLOYEE", con, trans).ExecuteReader());

    trans.Commit();
  }
}

// / <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]);
  }

  reader.Close(); 
}

Запись изменений в БД

После изменения данных в DataSet их необходимо передать обратно в базу. Для этого у объекта OleDbDataAdapter есть метод Update(). Прежде чем его использовать, необходимо настроить адаптер. В этом поможет класс OleDbCommandBuilder. Он позволяет сгенерировать команды для операций вставки, обновления и удаления, а также создать соответствующую коллекцию параметров команд. Ниже приведен пример передачи изменений из DataSet в базу данных:

        public
        void UpdateDataSet()
{
  DataSet dataSet = new DataSet();
  DataTable tbl = dataSet.Tables.Add("EMPLOYEE");

  using(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 cmdBuilder = new OleDbCommandBuilder(adapter);
    adapter.DeleteCommand = cmdBuilder.GetDeleteCommand();
    adapter.UpdateCommand = cmdBuilder.GetUpdateCommand();
    adapter.InsertCommand = cmdBuilder.GetInsertCommand(); 
 
    // обновление данных
    adapter.Update(tbl); 

    // откат сделанных изменений
    adapter.SelectCommand.Transaction.Rollback();
  }
}

Поддержка новых возможностей Firebird 2

Специальная поддержка DML (Data Manipulation Language)

Во второй версии Firebird появилось несколько нововведений. Одно из них – это инструкция EXECUTE BLOCK. Она позволяет выполнить блок инструкций на стороне сервера. Следующий пример демонстрирует использование ресурсов сервера БД для выполнения простого арифметического действия:

        public
        void ExecuteBlockSQLTest()
{
  OleDbConnection con = OpenFB2Connection();
  OleDbTransaction trans = con.BeginTransaction();

  // текст командыstring executeBlockData =
  "EXECUTE BLOCK (X INTEGER = :X) \n" +
  "RETURNS (Y INTEGER)           \n" +
  "AS                            \n" +
  "BEGIN                         \n" +
  "    Y = X * 2;                \n" +
  "SUSPEND;                      \n" +
  "END                           \n";

  // входящий параметрint inParameterX = 2;

  OleDbCommand cmd = new OleDbCommand(executeBlockData, con, trans);
  cmd.Parameters.AddWithValue("X", in_parameter_X);

  // выполнение команды EXECUTE BLOCK
  Assert.AreEqual((int)cmd.ExecuteScalar(),inParameterX * 2);
  trans.Commit();

  con.Close();
}

Еще одно новшество, которое подарил нам FB2 – это инструкция INSERT RETURNING. Она позволяет выполнить операцию вставки данных и прочитать значения, которые были добавлены в процессе этой операции. Это особенно актуально для получения сгенерированного значения идентификатора новой записи:

        public
        void InsertReturning()
{
  OleDbConnection con = OpenFB2Connection();
  OleDbTransaction trans = con.BeginTransaction();

  // новая команда INSERT RETURNING
  OleDbCommand cmd = new OleDbCommand(
    "insert into customer (cust_no, customer)\n" 
    + "  values(GEN_ID(CUST_NO_GEN, 1), :customer_name)\n" 
    + "  RETURNING cust_no", con, trans);

  cmd.Parameters.AddWithValue("customer_name", "New customer");
  // добавляем один выходной параметр
  cmd.Parameters.Add("customer_no", OleDbType.Integer)
    .Direction =ParameterDirection.Output;
 
  Assert.AreEqual(1, cmd.ExecuteNonQuery());   

  // удаляем запись, используя значение, полученное // через INSERT .. RETURNING
  OleDbCommand cmdDelete = new OleDbCommand(
    "delete from customer where cust_no=?", con, trans);
     
  cmdDelete.Parameters.AddWithValue("?", cmd.Parameters["customer_no"].Value);
  Assert.AreEqual(1, cmdDelete.ExecuteNonQuery());

  trans.Commit();
  con.Close();
}

ROLLBACK RETAIN – позволяет откатить транзакцию на момент старта или до последнего вызова COMMIT_RETAIN, оставляя возможность её дальнейшего использования. Давайте рассмотрим это на примере:

        public
        void RollbackRetainTest()
{
  OleDbConnection con = OpenFB2Connection();
  OleDbTransaction trans = con.BeginTransaction();

  // вставка записи 
  OleDbCommand cmd = new OleDbCommand(
    "insert into customer (cust_no, customer)"
      + "  values(GEN_ID(CUST_NO_GEN, 1), 'New customer')", con, trans);

  Assert.AreEqual(1, cmd.ExecuteNonQuery());
  
  new OleDbCommand("ROLLBACK RETAIN", con, trans).ExecuteNonQuery();

  // Транзакция будет активной, и мы сможем выполнить еще какие-либо команды
  cmd = new OleDbCommand(
    "select count(*) from customer", con, trans);
       
  Assert.IsTrue((int)cmd.ExecuteScalar() > 0); 

  trans.Commit();
  con.Close();
}

Ключевое слово ROWS соответствует последним стандартам ANSI SQL и является альтернативой FIRST/SKIP. Оно позволяет указать количество обрабатываемых записей. Может быть использовано в UNION, любых подзапросах, а также в командах DELETE и UPDATE. Следующий пример читает из базы данных записи с первой по третью:

        public
        void RowsKeywordTest()
{
  OleDbConnection con = OpenFB2Connection();
  OleDbTransaction trans = con.BeginTransaction();

  // Команда возвратит 3 записи
  OleDbCommand cmd = new OleDbCommand(
    "select * from customer rows 1 to 3", con, trans);

  short rec_count = 0;

  using (OleDbDataReader reader = cmd.ExecuteReader())
    while (reader.Read())
rec_count++;

  Assert.AreEqual(3, rec_count);    

  trans.Commit();
  con.Close();
}

Я привел примеры только некоторых изменений в DML Firebird 2, требующих специальной поддержки со стороны OLE DB-провайдера. Для изучения полного списка изменений рекомендую обратиться к документу Firebird 2 release notes: http:// www.firebirdsql.org/rlsnotes20/. Там действительно есть много интересного для разработчиков: производные таблицы, новые функции, улучшенный UNION, инструкция NEXT VALUE FOR, поддержка планов для операций обновления и удаления, и много чего другого.

Другие изменения

Новое свойство источника данных «IB Database creation date» позволяет узнать дату создания БД. Прочитать его значение можно при помощи класса OleDbProperties, который подробно рассмотрен в разделе «Свойства объектов OLE DB».

Поддержка визуальных средств VS 2005

Встроенные инструменты Visual Studio 2005 могут оказаться незаменимым подспорьем при написании БД-приложений. Давайте разберем по шагам создание примера простого приложения JobManager.

Создание подключения в Server Explorer


Рисунок 8.

Выберем команду «Add Connection». Откроется диалог с выбором источника данных. В списке Data Source выбираем значение <other>, а в списке Data provider.Net Framework Data Provider for OLE DB:


Рисунок 9.

Далее появится уже знакомый нам диалог:


Рисунок 10.

Выберите из списка OLEDB-провайдеров IBProvider третьей версии и нажмите кнопку Data Links. Откроется диалог, показанный на рисунке 11.


Рисунок 11.

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

Если все сделано правильно, в списке подключений Server Explorer появится новое подключение, для которого будет доступен список объектов базы данных (рисунок 12)


Рисунок 12.

Создание каркаса приложения

Создайте новую форму JobForm. Добавьте на форму ComboBox и перейдите в редактор ComboBox Tasks:


Рисунок 13.

Далее необходимо в свойстве Data Source выбрать действие «Add Project Data Source». Откроется мастер создания источников данных:


Рисунок 14.

Выбираем тип источника Database и идем далее. В списке подключений будет уже созданное ранее в Server Explorer подключение:


Рисунок 15.

Делаем все, как показано на рисунке, и переходим на следующий шаг:


Рисунок 16.

Мастер предлагает нам сохранить параметры источника данных в файле конфигурации приложения. По умолчанию строка подключения будет сохранена в области «Application Settings» и будет недоступна для редактирования внутри приложения. Это можно изменить, установив для настройки свойство Scope = User. А пока разрешим создание секции в конфигурационном файле и двинемся далее:


Рисунок 17.

На завершающем шаге нам предлагают создать DataSet. Выберем для него все доступные таблицы БД и нажмем кнопку «Finish».

Теперь в качестве источника данных для ComboBox выбираем таблицу EMPLOYEE. Свойству DisplayMember установим значение FULL_NAME:


Рисунок 18.

После завершения операции на форму будут добавлены три новых компонента. Это DataSet, TableAdapter и BindingSource. DataSet нам уже знаком, а вот два других объекта появились только в .Net 2.0 и будут рассмотрены далее.

Класс TableAdapter

Этот класс является ключевым звеном в цепочке связи данных с пользовательскими элементами управления. Если провести аналогию с терминами М. Фаулера [1], то TableAdapter является шлюзом таблицы данных для DataTable. Он инкапсулирует в себе логику обновления, загрузки и поиска данных, и относится к Data Access Layer. Что же касается DataSet и DataTable, то их можно отнести к уровню бизнес-логики (Business Layer)

Visual Studio .Net 2005 сама позаботилась о генерации кода этого класса. Давайте посмотрим, что же она нам предлагает. Итак:

Также у нас есть возможность создать дополнительные запросы к базе данных. Для этого понадобится инструмент Search Criteria Builder. Для его запуска необходимо выбрать пункт меню «Add Query» адаптера:


Рисунок 19.

Вводим название запроса (фактически название нового метода в TableAdapter), а также его текст, либо вручную, указав условие выборки, либо используя инструмент Query Builder:


Рисунок 20.

ПРИМЕЧАНИЕ

При записи условия выборки используйте позиционные параметры (символ «?»). Код метода будет сгенерирован автоматически, так что особых неудобств это не доставит.

После всех операций будет сгенерирован соответствующий метод. Кроме этого, дизайнер VS 2005 добавит к форме компонент ToolStrip (инструментальную панель) с кнопкой запуска этого метода и полем для задания фильтра. Мне кажется это излишним, но может быть, кому-нибудь понравится.

СОВЕТ

Visual Studio 2005 разделяет код, используемый дизайнером, и пользовательский код за счет partial-классов. В приложении наверняка понадобится расширить логику TableAdapter. Сделать это можно, описав partial-класс, соответствующий классу конкретного TableAdapter-a, сгенерированного дизайнером.

Передача изменений в базу данных через TableAdapter

Отдельно необходимо остановиться на методах передачи изменений обратно в БД. VS 2005 умеет генерировать код запросов для методов insert, update, delete. В некоторых случая этого может оказаться достаточно, но, как показывает опыт, полученные SQL-выражения пытаются претендовать на универсальность, и поэтому неоптимальны, а порой неработоспособны.

Приведу пример. Вместе с Firebird поставляется БД employee.fdb. В ней есть таблица SALES. Поле AGED этой таблицы доступно только для чтения, т.к. вычисляется с помощью выражения:

        COMPUTED
        BY (ship_date - order_date)

Если указать для Select Command текст:

        SELECT * FROM SALES 

то колонка AGED будет добавлена во все команды обновления. При попытке передать изменения в БД будет сгенерировано исключение. Необходимо вручную отредактировать текст запросов для команд insert, update и delete, и убрать из команд обновления данную колонку.

Отредактировать SQL-выражения можно с помощью дизайнера DataSet, вызвать который можно с помощью пункта меню «Edit Queries in DataSet designer»:


Рисунок 21.

В открывшемся дизайнере необходимо выбрать нужный TableAdapter (в данном случае это SALESTTableAdapter):


Рисунок 22.

В списке свойств появятся необходимые объекты OleDbCommand:


Рисунок 23.

Читатель может подумать, что данный случай представляет собой исключение, но это не так. Если вы выберете такой способ создания слоя доступа к данным, то будьте готовы постоянно вмешиваться в процесс генерации SQL-запросов.

BindingSource

С выходом .Net 2.0 технология Data Binding получила свое дальнейшее развитие. В частности, появился новый класс BindingSource, который является прокси-объектом между поставщиками данных и элементами управления, отображающими эти данные. Теперь элемент управления привязывается не к объектам, поставляющим данные (DataTable, DataSet, DataView), а к объекту BindingSource. Это позволяет использовать привязку ещё незагруженных данных, а также синхронизировать данные в случае использования общего BindingSource для нескольких элементов управления.

Приведу пример использования объекта BindingSource.

Добавим на форму новый элемент управления DataGridView. Попробуем отобразить в нем список проектов, в которых служащий, выбранный в ComboBox-e, был руководителем. Известно, что таблицы PROJECT и EMPLOYEE связаны между собой через поле TEAM_LEADER по внешнему ключу INTEG_36. Воспользуемся уже знакомым механизмом связи элемента управления и источника данных:


Рисунок 24.

Необходимо найти в списке возможных источников данных уже использованный ранее BindingSource для таблицы служащих EMPLOYEE и запросить дизайнер создать новый BindingSource для связи между проектами и служащими по внешнему ключу INTEG_36.

Добавим элемент TextBox для отображения описания проекта, которое хранится в BLOB-поле PROJECT_DESCR таблицы PROJECT. Чтобы связать его с текущим проектом в списке DataGridView, необходимо установить Binding для свойства Text.


Рисунок 25.

Добавление логики управления данными

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

Единственным редактируемым полем является описание текущего проекта. Для записи изменений, сделанных в нем, мы воспользуемся методом TableAdapter.Update() для таблицы PROJECT. Добавим элемент Button на форму, а в обработчике события Click напишем следующий код:

        private
        void btnSaveChanges_Click(object sender, EventArgs e)
{
  try
  {
    this.pROJECTTableAdapter.Update(this.jobDataSet.PROJECT);
    MessageBox.Show("Save was successful");    
  }
  catch (Exception exception)
  {
    MessageBox.Show(exception.Message);    
  }
}

Так же неплохо было бы иметь возможность откатить сделанные изменения. Добавим ещё одну кнопку, и в обработчике события Click поместим код, который будет отменять все изменения в DataSet, произведенные с момента последнего сохранения:

        private
        void btnUndoChanges_Click(object sender, EventArgs e)
{
  this.jobDataSet.RejectChanges();
  // требуется для обновления содержимого TextBoxthis.iNTEG36BindingSource.CurrencyManager.Refresh();   
}

В процессе написания кода передачи изменений в БД я столкнулся со следующей проблемой: при редактировании связанных данных через TextBox изменения не передавались в DataSet, и метод DataSet.HasChanges() всегда возвращал false. По всей видимости, даже при потере фокуса компонент TextBox не завершает редактирование текущей связанной записи и, хотя изменения, произведенные в TextBox, видны в DataSet, строка остается в режиме неподтвержденных изменений. Для решения этой проблемы в обработчик события TextBox.Validate нужно добавить следующий код:

        private
        void textBox1_Validated(object sender, EventArgs e)
{
  this.iNTEG36BindingSource.EndEdit();  
}

Готовое приложение JobManager доступно в архиве с примерами к статье.

Схемы метаданных БД

Неотъемлемой частью всех OLE DB-провайдеров являются схемы метаданных. Они используются клиентами для получения описания БД: списка хранимых процедур, структур таблиц, зарегистрированных доменов, ограничений, первичных и внешних ключей и т.д. По этой ссылке расположен список схем, которые поддерживает IBProvider: http://www.ibprovider.com/rus/documentation.html

Запросить определенную схему можно по её названию. Для этого у объекта OleDbConnection есть метод GetSchema(). В .Net 2.0 появился метод GetOleDbSchema(), который в качестве аргумента принимает одно из значений OleDbSchemaGuid. У каждой схемы есть набор колонок, по которым можно отфильтровать возвращаемый результат. Например, в схеме COLUMNS можно наложить ограничения по полям TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME и COLUMN_NAME. Чтобы получить описание всех колонок для таблицы EMPLOYEE, нужно использовать схему COLUMNS следующим образом:

DataTable schema_table = 
  connection.GetSchema("COLUMNS", newstring[] { null, null, "EMPLOYEE" });

Аналогично для метода GetOleDbSchema():

DataTable schema_table =
  connnection.GetOleDbSchemaTable(
    OleDbSchemaGuid.Columns, newobject[] { null, null, "EMPLOYEE" });

DDL-запросы CREATE, ALTER и DROP

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

В Firebird все DDL-запросы, за исключением CREATE DATABASE и DROP DATABASE, могут выполняться как в режиме автоматического подтверждения, так и в контексте транзакции. По умолчанию фиксирование изменений, произведенных DDL-запросами, отключено. Это сделано из соображений безопасности. Чтобы включить подтверждение DDL-запросов, необходимо установить свойство auto_commit_ddl. Его описание есть в разделе «Методы подключения к базе данных» этой статьи.

Приведу пример использования DROP DATABASE для удаления базы данных:

      private
      void DropDatabase()
{ 
   OleDbConnectionStringBuilder builder = 
     CreateConnectionStringBuilderForSample();
   
   // отключаем использование пула для этого подключения
   builder.OleDbServices = OleDbServicesValues.EnableAll 
     & ~OleDbServicesValues.ResourcePooling;
  
   try
   {
     // пытаемся подключиться к БД, которую необходимо удалитьusing(OleDbConnection con = new OleDbConnection(builder.ToString()))
     {
        con.Open();
        new OleDbCommand("drop database", con).ExecuteNonQuery();
        con.Close();
     }
   }
   // перехватываем возможные ошибки удаления БД
catch (Exception exception) {}
    
  
}

Обратите внимание на то, что для подключения, которое будет использоваться для удаления базы данных, мы отключили использование пула ресурсов. IBProvider умеет информировать сервисы OLE DB о ставших недоступными подключениях, поэтому в данном случае это действие является избыточным. Но оно оставлено здесь для решения возможных проблем при использовании OLE DB-провайдеров других производителей.

Теперь DDL для создания новой базы данных:

      private OleDbConnection CreateDatabase()
{
  // подключение к существующей БД employee.gdb
  OleDbConnection con = ConnectionProvider.CreateConnection();
  con.Open();

  // создание новой базы данныхnew OleDbCommand(
    "create database '" + server_name + ":" + databasePath + "'\n"
    + "USER   '" + user_name + "'  \n"
    + "PASSWORD '" + password + "' \n", con).ExecuteNonQuery();
            
  con.Close();

  returnnew OleDbConnection(
    CreateConnectionStringBuilderForSample().ToString());
}

И, наконец, полный пример, который сначала удаляет БД, потом создает на её месте новую и определяет в ней две таблицы, связанные внешним ключом:

      public
      void CreateNewDBSample()
{  
  DropDatabase();

  OleDbConnection con = CreateDatabase();
  con.Open();      
  OleDbTransaction trans = con.BeginTransaction();

  // создаем таблицу SAMPLE_TABLE с двумя колонками
  ExecuteDDL(
    "CREATE TABLE SAMPLE_TABLE(           " 
    + " ID       INTEGER    NOT NULL,      " 
    + " NAME       VARCHAR(64),            "   
    + "CONSTRAINT PK_SAMPLE_TABLE PRIMARY KEY(ID)  )", trans);

  // создаем SAMPLE_TABLE_2, связанную с SAMPLE_TABLE через FOREIGN KEY
  ExecuteDDL(
    "CREATE TABLE SAMPLE_TABLE_2 (          " 
    + " ID       INTEGER    NOT NULL,      "   
    + " PARENT     INTEGER    NOT NULL,      " 
    + "CONSTRAINT PK_SAMPLE_TABLE_2 PRIMARY KEY(ID),   " 
    + "CONSTRAINT FK_SAMPLE_TABLE_PARENT        " 
    + "FOREIGN KEY(PARENT) REFERENCES SAMPLE_TABLE(ID))", trans);

  trans.Commit();
  con.Close();
}

Управляющие последовательности ODBC

Управляющие последовательности позволяют преобразовывать текст запроса в процессе выполнения. Последовательность включается в текст запроса в фигурных скобках. Например {fn CURDATE} – выражение будет преобразовано в значение серверного времени.

В ODBC определены управляющие последовательности для следующих характеристик:

Данное расширение активно используется такими продуктами, как MS SQL Server, Crystal Reports, различными OLAP-средствами.

Чтобы включить поддержку ODBC-расширений в IBProvider, необходимо задать свойству инициализации support_odbc_call значение true. По умолчанию поддержка отключена.

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

Следующий пример демонстрирует применение управляющих последовательностей в тексте SQL-запросов:

      public
      void ODBCQueriesTest()
{
  OleDbConnectionStringBuilder builder = 
   ConnectionProvider.GetConnectionStringBuilderFromUDL();
  builder.Provider = "LCPI.IBProvider.2";
  builder.Add("support_odbc_query", "true");
 
  OleDbConnection con = new OleDbConnection(builder.ToString());
  con.Open();
  OleDbTransaction trans = con.BeginTransaction();

  // select current day name
  OleDbCommand cmd = new OleDbCommand(
    "select " +
    + "{fn dayname({fn now()})} as DAY_NAME, "
    + "{fn dayofweek({fn now()})} as DAY_OF_WEEK, "
    + "{fn dayofmonth({fn now()})} as DAY_OF_MONTH, "
    + "{fn dayofyear({fn now()})} as DAY_OF_YEAR "
    + "from RDB$DATABASE", con, trans);

  using (OleDbDataReader rdr = cmd.ExecuteReader())
    if (rdr.Read())
      for (int i = 0; i < rdr.FieldCount; i++)
        Console.WriteLine(rdr.GetName(i) + ": " + rdr[i].ToString()); 
        
  trans.Commit();
  con.Close(); 
}

На момент написания статьи ODBC-запросы поддерживались в 1 и 2 версии IBProvider, и ещё не были реализованы в третьей. Так что для использования этой возможности необходимо указывать в строке подключения Provider = "LCPI.IBProvider.2"

За более подробной информацией по использованию управляющих последовательностей ODBC можно обратиться к документу «ODBC Programmer’s Reference» и изучить раздел «Escape Characters in ODBC»: http:// windowssdk.msdn.microsoft.com/en-us/library/ms715364.aspx

Заключение

В этом обзоре я рассмотрел наиболее часто используемые возможности библиотеки ADO.Net на примере OLE DB-провайдера.

Основное преимущество OLE DB-провайдеров перед управляемыми (.Net Data Providers) – это возможность их использования не только в среде .Net Framework, но и практически в любых средах, поддерживающих COM.

Использование IBProvider в качестве поставщика данных Firebird позволяет:

IBProvider поддерживает все существующие версии Interbase/Yaffil/Firebird. Он содержит встроенный менеджер управления памятью и SWAP для пользовательских данных, поддерживает многопоточность, а также серверные и клиентские курсоры.

Надеюсь, данное руководство позволит вам расширить круг применяемых возможностей ADO.Net при работе не только с Firebird, но и с другими СУБД.

Полезные ссылки

Список литературы

  1. М.Фаулер. Архитектура корпоративных программных приложений. Изд. Москва – Спб – Киев. 2004 г.
  2. Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. Приемы объектно-ориентированного проектирования. Паттерны проектирования. Изд. Питер. 2006 г.
  3. А. Ковязин, С.Востриков. Мир InterBase. Архитектура, администрирование и разработка приложений баз данных в InterBase/Firebird/Yaffil (+ CD-ROM). Изд. КУДИЦ-Образ, Питер. 2005 г.
  4. Б.Бошемин. Основы ADO .Net. Изд. Вильямс. 2003 г.


Эта статья опубликована в журнале RSDN Magazine #3-2006. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 75        Оценить