Программирование в ADO.NET


Классы каркаса, предназначенного для работы с базами данных, собраны в ADO.NET. Класс DataSet (Набор данных) позволяет работать с реляционными данными реляционным же способом, независимо от того, есть ли в текущий момент соединение с источником данных. Разъединенный (disconnected) доступ к данным становится все более значимым в многоярусном и Internet-ориентированном мире данных. При использовании такого типа доступа к данным необходимо установить соединение с базой данных только для изменения или получения ее содержимого. Конечно, при желании, можно работать и обычным соединенным (connected) способом.
Источники данных ADO.NET позволяют задавать команды непосредственно источнику данных. При этом не используются промежуточные объекты, такие, как объекты OLEDB (OLE для баз данных), которые находятся между ADO и источником данных. Класс DataAdapter эмулирует источник данных (как набор команд базы данных) и соединение с этим источником данных. Класс DataAdapter реализует интерфейс IDa-taAdapter, являющийся связующим звеном между объектом DataSet (Набор данных) и источником данных. Различия между источниками данных скрыты интерфейсом I DataAdapter. Источники данных OLEDB (OLE для баз данных) позволяют использовать вложенные (nested) транзакции; а источники данных SqlServer этого не позволяют.
Источники данных .NET передают данные в набор данных или в устройство считывания данных. Набор данных— резидентная упрощенная реляционная база данных, не соединенная прямо ни с какой другой базой данных. Набор данных можно даже преобразовать в документ XML, и наоборот. Это позволяет работать с данными как с реляционными или как с иерархическими XML-данными. Устройства считывания данных моделируют обычный способ работы с базами данных.
Классы доступа к данным, поставляемые вместе с каркасом, находятся в пространствах имен System: :Data (Система: Данные), System: : Data: :SqlClient, System:: Data::01eDb (Система::Данные::ОЬЕ для баз данных), System: : Data: :Common (Система::Данные::Обшие) и System: : Data: :SqlTypes. Пространства имен OleDb (OLE для баз данных) и Sql содержат классы, используемые при работе с источниками данных OleDb (OLE для баз данных) и SqlServer соответственно. Уже разработан источник данных ODBC, а другие драйверы доступа будут созданы в ближайшем будущем.
В этой главе мы изменим реализацию классов Customer (Клиент) и Hotel (Гостиница) для того, чтобы ближе познакомиться с использованием SQL Server. Для демонстрации использования XML в наш пример туристического агентства Acme Travel Agency добавим возможность бронирования авиабилетов.
В своих примерах мы будем использовать SQL Server 2000 и источник данных SQL Server. Несмотря на это, большинство материала, изложенного в главе, можно отнести и к источнику данных OleDb (OLE для баз данных).
Кроме того, для понимания примеров читателю необходимо понимать принципы работы баз данных
Базы данных, используемые в примерах
В главе предполагается, что SQL Server установлен в конфигурации Local System account, причем в качестве режима аутентификации выбран Mixed Mode (Смешанный режим). Предполагается, что имя пользователя — sa, а поле пароля не заполнялось. В некоторых примерах используется база данных Northwind Trader, инсталлируемая в качестве образца базы данных в составе SQL Server. Кроме того, в некоторых примерах используются базы данных HoteLBroker (Посредник, бронирующий места в гостинице) и AirlineBroker, созданные исключительно как иллюстративный материал к данной книге. Некоторые из иллюстративных программ изменяют используемые базы данных, в то время как в других предполагается, что эти базы имеют первоначальный вид. В результате какие-то программы не будут работать подобающим образом, пока вы не восстановите исходный вид используемых в них баз данных. Это можно сделать с помощью прилагаемых к программам макросов SQL. Более подробную информацию можно найти в файле readme.txt.

Источники данных


Префикс имен классов и методов указывает на источник данных. Например, префикс OleDb (OLE для баз данных) указывает на использование источника данных OleDb (OLE для баз данных). Префикс Sql указывает на использование источника данных SqlServer.
Источник данных SQL Server использует родной протокол SQL Server. Источник данных OleDb (OLE для баз данных) через промежуточный уровень модели компонентных объектов Microsoft (COM) обращается к различным драйверам доступа OleDb (OLE для баз данных). Например, можно взаимодействовать с SqlServer через источник данных OleDb (OLE для баз данных) с целью обращения к драйверу доступа OLEDB (OLE для баз данных) для SQL Server. Но быстродействие при таком способе будет, конечно, меньше, чем при использовании источника данных SqlServer. Преимуществами источников данных OleDb (OLE для баз данных) и ODBC является то, что, используя их при работе в ADO.NET, можно работать с большинством из доступных сегодня источников данных.
Хотя в ADO.NET есть несколько интерфейсов, определяющих общие возможности, и несколько базовых классов, которые можно использовать для обеспечения этих возможностей, к источникам данных не предъявляется требование удовлетворять спецификациям, не соответствующим принятым способам работы с используемыми источниками данных.
Например, классы SqlDataAdapter и OleDbDataAdapter в качестве базовых используют базовые абстрактные классы DbDataAdapter и DataAdapter, находящиеся в пространстве имен System: :Data: : Common (Система::Данные::Общие). С другой стороны, классы SqlTransaction и OleDbTransaction не наследуют реализации какого-либо класса, предназначенного для работы с базами данных. Классы OleDbError и SqlError вообще не похожи друг на друга. Указатель, реализуемый сервером, не поддерживается в ADO.NET из-за того, что некоторые базы данных (например, Oracle и DB2) не имеют встроенной поддержки этой возможности. Поэтому поддержка такой возможности для источника данных SQL Server будет расширением.
В табл. 9.1 приведены классы источников данных О1е и Sql, предназначенные для соединения, задания команд, чтения данных, преобразования данных, хранения параметров данных. Как видно из этой таблицы, приведенные там классы источников данных Ole и Sql имеют общие черты, определенные в интерфейсах IDbConnection, IDbCom-mand, IDataReader, IDbDataAdapter и IDataParameter. Ничто, конечно же, не препятствует реализовать в любом из этих классов методы, не определенные соответствующим интерфейсом.
Таблица 9.1. Сравнение соответствующих друг другу классов источников данных OleDb (OLE для баз данных) и SqlServer
Интерфейс OleDb SQL Server
IDbConnection OleDbConnection SqlConnection
IDbCommand OleDbCommand SqlCommand
IDataReader OleDbDataReader SqlDataReader
IDbDataAdapter OleDbDataAdapter SqlDataAdapter
IDataParameter OleDbDataParameter SqlDataParameter
Классы, не зависящие от какого-либо источника данных, например, DataSet (Набор данных) или DataTable (Таблица данных), не имеют префиксов.
Если важна масштабируемость (расширяемость) базы данных, желательно запретить завершение (fmalization) объектов, не нуждающихся в нем. Тем самым повышается производительность приложения, так как уменьшается время работы потока завершителей (fmalizer).

Проводник Visual Studio.NET по серверу: Server Explorer


Проводник Visual Studio.NET no серверу, Server Explorer— полезная утилита при работе с базами данных. Хотя и не такая мощная, как SQL Server Enterprise Manager, она обеспечивает базовые возможности, необходимые при создании и отладке приложений, работающих с базами данных.
Для того чтобы запустить Server Explorer, выберите пункт меню View=>Server Explorer. Окно Server Explorer можно прикрепить и при необходимости перемещать. На рис. 9.1 представлено окно Server Explorer.
С помощью Server Explorer можно легко получить информацию о любом поле таблицы, просмотреть или изменить данные в ней. Можно также создавать или изменять хранимые процедуры и разрабатывать таблицы. Далее мы рассмотрим Server Explorer в нескольких примерах для того, чтобы ближе познакомить читателя с его использованием.

Установление соединения


Начнем с небольшой программы JustConnect, единственная задача которой — просто устанавливать соединение с базой данных. Пример поможет также проверить, корректно ли установлен SQL Server и существует ли запрашиваемая база данных (в нашем случае — Northwind, входящая в состав SQL Server как ее стандартная часть)
SqlConnection *conn = 0;
String *ConnString =
"server=localhost;
uid=sa;
pwd=;
database=Northwind";
try
{
conn = new SqlConnection(ConnString);
conn->0pen(); // Открыть
Console::WriteLine(
"Connection to {0} opened successfully.", // "Соединение с {0} открыто успешно. ",
conn->Database); // База данных
}
catch(Exception *e) // Исключение
{
Console::WriteLine(e->Message); // Сообщение
}
_finally // наконец
{
if (conn->State == ConnectionState::Open) // если открыто
conn->Close();
}
Если СУБД SQL Server установлена и работает корректно, причем база данных Northwmd существует, результатом работы программы JustConnect будет следующее сообщение:
Connection to Northwmd opened successfully.
(Соединение с Northwmd открылось успешно.)
Если же что-то происходит не так, как должно, при выполнении метода Open (Открыть) возникает исключение и пользователь увидит сообщение, определенное в обработчике исключений. Например, если закрыть SQL Server, программа выведет следующее сообщение:
General network error. Check your network documentation.
(Общая сетевая ошибка. Сверьтесь с вашей сетевой документацией.)
Если изменить имя базы данных, заданное в строке соединения, на имя несуществующей базы, например, Southwind, будет выведено следующее сообщение:
Cannot open database requested in login 'Southwind'. Login fails.
Login failed for user 'sa'.
(He могу открыть базу данных, требуемую в регистрационном имени
'Southwind'. Вход в систему невозможен.
Вход в систему был безуспешным для пользователя 'за'.)

Устройства считывания данных


Следующим примером станет использование классов ADO.NET для получения доступа к данным, хранящимся в базе данных. Соответствующие файлы находятся в подпапке Connected.
Нам необходимы объекты для соединения, хранения команд, передаваемых базе данных, и хранения самих данных, поэтому мы определяем три указателя на объекты классов SqlConnection, SqlCommand и SqlDataReader:
SqlConnectlon *conn = 0;
SqlCommand * command = 0;
SqlDataReader *reader = 0;
Далее инициализируется строка соединения с базой данных Вы можете изменить значение поля, предназначенного для хранения имени сервера, на имя своего компьютера Необходимо также определить имя пользователя и пароль для получения доступа к базе данных Строку соединения можно устанавливать и как свойство объекта SqlConnection В качестве команды, которая будет передаваться базе данных в нашем примере, выбран простой оператор отбора данных:
String *ConnString =
"server=localhost;
uid=sa;
pwd=;
database=Northwind";
String *cmd =
"select Customerld,
CompanyName from Customers";
На рис. 9.2 приведены списки таблиц и хранимых процедур базы данных Northwmd В теле блока try создается объект класса SqlConnection. Затем открывается соединение с базой данных, ведь это должно быть сделано до передачи базе данных какой-либо команды После этого создается объект класса SqlCommand, связанный с созданным ранее соединением.
conn = new SqlConnection(ConnString);
conn->0pen(); // Открыть
command = new SqlCommand(cmd, conn);
Если команда выполняется посредством использования метода ExecuteReader объекта SqlCommand, то при этом возвращается экземпляр класса SqlDataReader Этот объект можно использовать для перемещения по полученному набору данных Для извлечения данных из текущей строки набора можно использовать имя столбца
reader = command->ExecuteReader();
// читатель = команда-> ExecuteReader ();
if (reader != 0)
{
Console::WriteLine(
"CustomerldXtCompanyName");
while (reader->Read()) // Чтение
Console::WriteLine (
"{0}\t\t{l}",
reader->get_Item("Customerld"),
reader->get_Item("CompanyName"));
}
И в заключение, в блоке finally закрываются считывающее устройство и соединение.
if (reader != 0)
reader->Close() ; if (conn->State == ConnectionState::0pen) // если открыто
conn->Close();
Если соединение не закрыть явно, завершитель объекта SqlConnection, рано или поздно запущенный, закроет соединение. Но из-за того, что сборщик мусора не является детерминированным, никто не сможет сказать, когда это произойдет. Поэтому всегда закрывайте соединение явно. Если этого не сделать, будет использоваться больше соединений, чем необходимо (даже если вы организуете связной пул), что может снизить масштабируемость приложения. Кроме того, может исчерпаться запас соединений.
Приведем результат работы программы:
Customerld CompanyName
ALFKI Alfreds Futterkiste
ANATR Ana Trujillo Emparedados у helados
ANTON Antonio Moreno Taqueria
AROUT Around the Horn
BERGS Berglunds snabbkop
BLAUS Blauer See Delikatessen
BLONP Blondesddsl pere et fils
BOLID Bolido Comidas preparadas
BONAP Bon app'
BOTTM Bottom-Dollar Markets
BSBEV B's Beverages
. . .
Для проверки корректности работы программы можно использовать Server Explorer среды разработки Visual Studio.NET. Выберите в базе данных Northwind таблицу Customers (Клиенты) и щелкните на ней правой кнопкой для того, чтобы вызвать всплывающее меню. Выберите в нем пункт Retrieve Data from Table (Получить данные из таблицы) и, просмотрев данные, хранящиеся в таблице, сравните их с результатом работы программы.

 

Работа с базой данных в соединенном режиме


Использовавшийся в предыдущем примере режим называют соединенным. Программа соединяется с базой данных, выполняет все необходимые действия, а затем отсоединяется. При этом перемещаться по данным базы можно только в одном направлении. Это соответствует однонаправленному курсору/набору записей в классической технологии доступа к данным ADO. При использовании соединенного режима следует открывать и закрывать соединение явно.
Держать соединение постоянно открытым — не лучший способ работы, если вы хотите минимизировать потребление ресурсов (соединение само по себе недешево) для обеспечения масштабируемости. Тем не менее, как мы увидим позже, именно использование SqlDa-taReader может, в зависимости от ваших потребностей, оказаться правильным подходом.
Далее будет показано, что SqlConnection используется вместе с DataSet (Набор данных) и SqlDataReader для установления соединения с базой данных так же, как это сделано выше с помощью SqlCommand. Объект SqlConnection, кроме того, управляет свойствами базы данных, такими, как транзакции и уровни изоляции. Основная (root) транзакция начинается вызовом метода BeginTransaction класса SqlConnection". Аналогичная строка соединения с SQL Server с использованием объекта класса OleDbConnection будет такой:
"Provider=SQLOLEDB.1;server=localhost;uid=sa;pwd=;
database=Northwind";
В приведенной строке следует изменить на корректные имя сервера, идентификатор и пароль пользователя.
Как уже было сказано, SqlCommand применяется для выполнения команд при использовании и DataSet (Набор данных) и SqlDataReader, только действует немного по-разному. Это станет более понятным после рассмотрения класса SqlDataAdapter.
Свойство CommandType определяет тип команды, хранимой в SqlCommand. Для источника данных Sql это может быть Text (Текст) (принятое по умолчанию значение) или StoredProcedure (Хранимая процедура). CommandText также можно определить как свойство. Вскоре мы научимся использовать параметры при работе с командами, которые передаются базе данных.
Экземпляр класса SqlDataReader возвращается посредством метода Ехе-cuteReader экземпляра класса SqlCommand. Если программа должна быть независима от используемого источника данных, вместо указанного метода следует использовать интерфейс IDataReader. При этом можно вызывать методы интерфейса, а не самого экземпляра класса.
IDataReader *idr = command->ExecuteReader() ;
Этот же прием можно использовать и для других классов источника данных, где реализованы интерфейсы, которые поддерживаются несколькими источниками данных. Пока экземпляр класса SqlDataReader не будет закрыт, никакие действия над объектом SqlCommand, кроме его закрытия, недоступны.

Выполнение операторов SQL


Метод ExecuteReader класса SqlCommand возвращает экземпляр класса Da-taReader. Данные возвращаются, если в качестве команды задан запрос на выборку. Этот же метод можно использовать для обновления, вставки или удаления данных. Метод SQLCommand: : ExecuteReader использует хранимую процедуру sp_executesql. Некоторые команды, использующие операторы SET (оператор Установить), могут работать неправильно. Другие драйверы могут иметь иные ограничения на использование метода ExecuteReader.
Обычно для команд, при выполнении которых данные не возвращаются, используется метод SqlCommand::ExecuteNonQuery. Пример NonQuery демонстрирует работу этого метода. Кроме того, соединение с SQL Server осуществляется в нем с помощью источника данных OleDb (OLE для баз данных).
String *cmd = "update Customers set ContactName =
// Строка *cmd = "обновить Клиентов, установить ContactName =
Too' where ContactName = 'Maria Anders'";
// Too', где ContactName = 'Мария Андерс";
try {
conn = new OleDbConnection(ConnString);
conn->0pen(); // Открыть
command = new OleDbCommand(cmd, conn);
int NumberRows = command->ExecuteNonQuery(); // команда
Console::WriteLine(
"Number Rows: {0}", NumberRows.ToString());
}
Количество измененных строк, которое должно быть равным 1, показано в окне, предназначенном для консольного вывода. Если запустить программу еще раз, она не сможет найти необходимую ей запись, так как та была изменена при первом запуске программы (нет больше в базе данных Марии Андерс (Maria Anders)!), и выведет значение 0. Для приведения базы данных в исходное состояние необходимо запустить макрос SQL, как это описано в файле readme.txt для этой главы. На рис. 9.4 показан результат изменения первой строки. Значение поля ContactName изменено с Maria Anders на Foo.
При выполнении вставки, обновления и удаления данных возвращается количество строк, которых коснулись изменения. Для всех остальных операторов SQL Server возвращает значение -1 (при использовании родного источника данных или OLEDB (OLE для баз данных)). Другие драйверы доступа могут возвращать 0 или -1.
Для получения одного значения (например, результата вычислений) используйте метод ExecuteScalar. При работе с источниками данных, способными генерировать данные XML, более эффективным будет использовать метод SqlCommand: : ExecuteXmlReader, a не получать данные в объект DataSet (Набор данных), а затем преобразовывать их в ХМL.

DataReader


При его создании SqlDataReader не указывает ни на какую запись возвращенного набора данных. Поэтому для получения доступа к данным следует вызвать метод Read (Читать). Как показано в примере Connected, для получения доступа к отдельным полям или столбцам текущей строки можно использовать свойство Item (Элемент). Получить все поля строки можно также с помощью метода GetValues.
Object * fields [] = new Object *[NumberFields]; // новый Объект
int NumberFields = reader->GetValues(fields); // читать поля
GetValue возвращает значение столбца в его исходном формате Для доступа к данным определенных форматов можно использовать методы GetBoolean (Прочитать Логическое значение), GetDecimal (Прочитать Десятичное число) и GetString (Прочитать Строку). Метод GetName возвращает имя определенного столбца.
Еще раз повторим, что при использовании DataReader в каждый момент времени доступна только одна запись. Убедитесь в том, что по завершении работы с DataReader вы его закрыли.

Множественное результирующее множество


Класс SqlDataReader может хранить несколько результирующих множеств, что продемонстрировано в примере DataReader. Два запроса, разделенные точкой с запятой, являются двумя SQL-запросами, которые приводят к возврату двух результирующих множеств, по одному на каждый запрос.
String *ConnString = // Строка
"server=localhost;uid=sa;pwd=;
database=Northwind";
String *cmd = // Строка
"select Customerld,
CompanyName from Customers where
// выбрать Customerld,
CompanyName из Клиентов где
Customerld like 'T%'/select Customerld, CompanyName ..."
// Customerld подобно "I % '; выбрать Customerld, CompanyName ...
int ResultSetCounter = -1; int NumberFields = 0;
reader = command->ExecuteReader(); // команда
if (reader != 0)
{
NumberFields = reader->FieldCount;
Object *fields[] = new Object*[NumberFields]; // новый
// Объект Console::WriteLine (
"Result Set\tCustomerId\tCompanyName");
// "Результат Set\tCustomerId\tCompanyName");
do
{
ResultSetCounter++;
while(reader->Read() == true) // пока Чтение ()
// == истина {
NumberFields =
reader->GetValues(fields); // поля Console::Write( " { 0 } " ,
ResultSetCounter.ToStringt)); for (int i = 0; i<NumberFields; i++)
{
Console::Write(
"\t\t{0}", fields[i]); // поля
Console::Write("\n"); // Запись
};
}
while(reader->NextResult() == true); // пока NextResult ()
// == истина
}
Метод FieldCount возвращает количество столбцов в результирующем множестве. Поскольку метод GetValues возвращает данные в их исходном формате, в качестве аргументов ему передается массив объектов. Метод NextResult обеспечивает перемещение к следующему результирующему множеству.
Результатом работы программы DataReader будет вывод на экран следующих строк:
Result Set Customerld 'CompanyName
0 THEBI The Big Cheese
0 THECR The Cracker Box
0 TOMSP Toms Spezialitaten
0 TORTU Tortuga Restaurante
0 TRADH Tradigao Hipermercados
0 TRAIH Trail's Head Gourmet
Provisioners
1 WANDK Die Wandernde Kuh
1 WARTH Wartian Herkku
1 WELLI Wellington Importadora
1 WHITC White Clover Markets
1 WILMK Wilman Kala
1 WOLZA Wolski Zajazd

Коллекция параметров


Иногда необходимо параметризировать SQL-запрос. Кроме того, бывает желательно связать входные и выходные аргументы хранимой процедуры с переменными программы.
Для того чтобы сделать это, следует определить свойство Parameters (Параметры) класса SqlCommand, которое является коллекцией экземпляров класса SqlParameter. Процедура инсталляции, имеющаяся на Web-узле данной книги, добавляет в базу данных Northwind хранимую процедуру get_customers. To же самое можно выполнить и вручную с помощью Server Explorer в Visual Studio.NET или SQL Query Analyzer (Анализатор запросов SQL). Еще один способ — запустить макрос SQL, поставляемый вместе с примерами к данной книге. Хранимая процедура get_customers иллюстрирует, как можно использовать простую хранимую процедуру, имеющую один аргумент, а именно — название компании, и возвращающую идентификатор (ID) клиента, т.е. указанной компании.
CREATE PROCEDURE get_customers
(dcompanyname nvarchar ( 40), Scustomerid nchar(5) OUTPUT)
AS
select @customerid = CustomerlD from Customers where
CompanyName = @companyname
RETURN
GO
Пример StoredProcedure (Хранимая процедура) демонстрирует, как это можно сделать.
command = // команда
new SqlCommand("get_customers", conn);
command->CommandType = // команда
CommandType::StoredProcedure;
SqlParameter *p = 0; p = new SqlParameter(
"@companyname",
SqlDbType::NVarChar, 40);
p->Direction = ParameterDirection::Input;
// Направление = Ввод p->set_Value(S"Ernst Handel");
// Эрнст Хандель command->Parameters->Add(p);
// команда-> Параметры-> Добавить
p = new SqlParameter(
"@customerid", SqlDbType::NChar, 5);
p->Direction = ParameterDirection::Output;// Направление = Вывод
command->Parameters->Add(p); // команда-> Параметры-> Добавить command->ExecuteNonQuery(); // команда
Console::WriteLine(
"{0} Customerld = {!}",
command->get_Parameters()-> // команда
get_Item("gcompanyname")->Value, // Значение
command->get_Parameters()-> // команда
get_Item("Scustomerid")->Value) ; // Значение
Каждый отдельный член коллекции SqlParameterCollection, являющийся объектом SqlParameter, соответствует одному параметру SQL-запроса или хранимой процедуры. Как показано в примере, параметру не обязательно иметь какую-либо взаимосвязь с определенной таблицей или столбцом базы данных.
Тем минимумом, который необходимо определить в конструкторе или установкой свойств, являются имя и тип параметра. Если параметр имеет непостоянную длину, необходимо также определить его размер.
В приведенном примере к коллекции параметров добавляются два параметра. Первый соответствует аргументу хранимой процедуры. Второй соответствует возвращаемому хранимой процедурой значению.
Имя параметра соответствует имени аргумента хранимой процедуры get_customers. Другие параметры конструктора SqlParameter определяют тип параметра. В первом случае это строка Unicode переменного размера, длиной до 40 символов. Во втором — строка Unicode постоянного размера (5 символов). Обозначение SqlDbType : :NVarChar означает постоянный подлине поток символов Unicode.
Свойство Value (Значение) используется для установки или получения значения параметра. В нашем примере оно используется для инициализации входного параметра @companyname, соответствующего аргументу хранимой процедуры. Оно используется также для получения значения параметра @customerid, соответствующего возвращаемому хранимой процедурой значению.
Выходной параметр должен быть определен как таковой с помощью свойства Direction (Направление). В нашем примере параметр @companyname устанавливается как входной присвоением этому свойству значения ParameterDirection: : Input (Входной параметр). Аналогично, параметр @customerid устанавливается как выходной присвоением этому свойству значения ParameterDirection: :Output (Выходной параметр). Данная операция для выходного параметра должна быть проведена обязательно, так как по умолчанию свойство Direction (Направление) имеет значение, соответствующее входному параметру. Для того чтобы связать параметр с возвращаемым хранимой процедурой значением, используется значение ParameterDirection: :ReturnValue. Для параметров, используемых в обоих направлениях, берется значение ParameterDirection: : InputOutput (Входной и выходной параметр).
Имена параметров можно использовать для доступа к каждому из параметров коллекции параметров SqlCommand. Параметризованные команды могут работать как с классом SqlDataReader, так и с классом DataSet (Набор данных). Позже, при рассмотрении класса DataSet (Набор данных), мы расскажем, как определить свойство параметра Source (Источник), которое указывает, какому именно столбцу объекта DataSet (Набор данных) соответствует параметр.

Классы SqlDataAdapter и DataSet (Набор данных)
Класс DataSet (Набор данных) представляет собой резидентную упрощенную реляционную базу данных, не соединенную прямо ни с какой другой базой данных. Некоторые из его свойств описывают таблицы (Tables) и отношения (Relations) между ними в наборе данных. Управлять проверкой ограничений можно с помощью свойства Enf ог-ceConstraint. Имя набора данных можно установить с помощью свойства DataSet-Name, а кроме того, его можно определить и в конструкторе DataSet (Набор данных).
И Класс SqlDataAdapter используется для передачи данных от базы данных объекту DataSet (Наборданных). В конструкторе класса HotelBroker (Посредник, бронирующий места в гостинице) продемонстрировано, как использовать SqlDataAdapter для заполнения набора данных. Пример CaseStudy для данной главы содержит приведенный ниже исходный код15. Этот фрагмент находится в файле HotelBroker. h из папки CaseStudy\HotelBrokerAdmin\Hotel.
conn = new SqlConnection(connString) ; citiesAdapter = new SqlDataAdapter(); citiesAdapter->SelectCommand = new SqlCommand( "select distinct City from Hotels", conn); citiesDataset = new DataSet; // новый Набор данных citiesAdapter->Fill(citiesDataset, "Cities"); // Города
Среди свойств класса SqlDataAdapter есть такие, которые связывают его с операциями выборки, вставки, обновления или удаления данных источника данных. В нашем примере экземпляр класса SqlCommand не вызывается непосредственно одним из его методов, а связывается со свойством SelectCommand класса SqlDataAdapter.
Затем для выполнения указанной команды используется метод Fill (Заполнить) класса SqlDataAdapter. При этом объект DataSet (Набор данных) заполняется информацией из таблицы, имя которой указано как аргумент метода Fill (Заполнить). После завершения работы этого метода соединение остается в том же состоянии, в котором оно было при вызове метода.
Теперь соединение с базой данных можно закрыть. Но при этом, вне зависимости от наличия соединения с базой данных, можно продолжить работу с объектом DataSet (Набор данных), содержащим данные.
Класс SqlDataAdapter реализован на основе класса SqlDataReader, поэтому при использовании последнего можно ожидать большей производительности. SqlDataReader может также эффективнее использовать память. Это зависит от структуры.
Примером CaseStudy для этой главы является решение AcmeGui, состоящее из трех проектов AcmeGui, Customer и Hotel. Проекты Customer и Hotel реализованы на управляемом C++, проект Customer — на С». Так сделано потому, что AcmeGui реализует аспекты программы, связанные с графическим интерфейсом пользователя (GUI), а это значительно удобнее делать на С», нежели на управляемом C++. Тем не менее, поскольку в данной главе рассматриваются вопросы, связанные с доступом к базам данных, весь исходный код для работы с базами данных реализован на управляемом C++.
приложения. Так что, если у вас нет необходимости использовать преимущества класса DataSet (Набор данных), нет смысла увеличивать накладные расходы в приложении.

Отсоединенный режим


Режим работы с базами данных при отсутствии постоянного соединения с базой данных называют отсоединенным (disconnected). Соединенный режим представляет собой сильносвязанную среду, которая может содержать состояния и соединения. Среда клиент/сервер является тому подтверждением. Именно для такого подхода и были разработаны ADO и OLEDB (OLE для баз данных). В среде соединенного режима можно использовать устройства считывания данных. При необходимости для этих целей можно использовать, посредством обеспечивающих взаимодействие СОМ-компонентов, ADO. Фактически, специально для применения в .NET, изменения в ADO не вносились, так что здесь есть полная обратная совместимость, включая также ошибки и прочее.
Однако держать соединение постоянно открытым слишком дорого в среде, в которой требуется обеспечить возможность работы нескольким пользователям. Это относится к многоузловым и Internet-ориентированным решениям. В таких средах часто нет необходимости блокировать доступ к таблицам баз данных. А это способствует масштабируемости, так как уменьшает вероятность конфликтов. Объекты DataSet (Набор данных) из коллекции таких объектов Tables (Таблицы), с их ограничениями, могут имитировать таблицы исходной базы данных и взаимосвязи между ними. В приложениях, полностью реализованных в .NET, одна часть приложения может передавать или получать экземпляр DataSet (Набор данных). Конкурентоспособным разработкам это может дать большое преимущество в масштабируемости и производительности, что справедливо также для многих типов Internet-приложений и приложений, ориентированных на внутрисете-вое применение.
При работе в отсоединенном режиме соединение осуществляется таким же образом, как и в соединенном режиме. Данные получают с помощью классов преобразования данных источников данных. Свойство SelectCommand определяет SQL-запрос, используемый для передачи данных в набор данных. В отличие от устройства считывания данных, которое связано соединением с определенной базой данных, набор данных не имеет связей ни с какой базой данных, даже с той, из которой были получены хранящиеся в нем данные.

Коллекции объектов DataSet (Набор данных)


При помещении данных в объект DataSet (Набор данных) считываются также и связанные с этими данными таблицы и столбцы. Каждый набор данных имеет коллекции, представляющие все таблицы, столбцы и строки, связанные с данными, содержащимися в этом наборе.
Класс HotelBroker (Посредник, бронирующий места в гостинице) из используемого нами в качестве примера приложения содержит метод ListHotelsToFile, в котором продемонстрировано, как получить такую информацию и записать ее в файл Hotels. txt. Этот метод вызывается при нажатии кнопки на форме, описанной в файле MainAdmin-Form.cs. Вывод данных осуществляется перенаправлением вывода на консоль. hotelDA-taset — набор данных, содержащий данные из базы данных HotelBroker (Посредник, бронирующий места в гостинице). Приведем фрагмент файла HotelBroker.h.
TextWriter *tw = new StreamWriter("Hotels.txt");
Console::SetOut(tw); // печатающее устройство -
// переадресовать вывод
try
{
Console::WriteLine("Hotels"); // Гостиницы
DataTable *t =
hotelsDataset->Tables->get_Item( // Таблицы
"Hotels"); // Гостиницы
if (t == 0) // если (t == 0)
return;
lEnumerator *pEnum = t->Columns->GetEnumerator(); // Столбцы
while (pEnum->MoveNext())
{
DataColumn *c =
dynamic_cast<DataColumn *>(pEnum->Current);
Console::Write("{0, -20}", c->ColumnName);
}
Console::WriteLine("");
pEnum = t->Rows->GetEnumerator();
while (pEnum->MoveNext())
{
DataRow *r =
dynamic_cast<DataRow *> (pEnum->Current) ;
for (int i=0; i<t->Colunms->Count; i++) // Столбцы-> Счет
{
Type *type = r->get_Item(i)->GetType() ;
if (type->FullName->Equals("System::Int32"))
// если равняется ("Система:: Int32"))
Console::Write("{О, -20}", r->get_Item(i) ) ;
else
{
String *s = r->get_Item(i)->ToString(); // Строка
s = s->Trim(); // Вырезка
Console::Write("{0, -20}", s);
}
}
Console::WriteLine("");
}
Console::WriteLine("");
}
catch(Exception *e) // Исключение
{
throw e;
}
_finally // наконец
{
tw->Close ();
}
Коллекция Tables (Таблицы) — это коллекция всех экземпляров DataTable (Таблица данных), содержащихся в объекте DataSet (Набор данных). В нашем примере такой экземпляр один, так что нет необходимости перемещаться по коллекции. Поэтому программа просто проходит по столбцам таблицы, воспринимая их содержимое как заголовки для данных, которые будут распечатаны далее. После считывания заголовков просматривается содержимое каждой строки таблицы Для каждого из значений в строке программа выясняет тип значения и печатает его в соответствующем формате В нашем случае программа проверяет, какой тип имеет рассматриваемое поле базы данных Hotel (Гостиница) Проверка типа значения поля, а не просто вывод на печать как Object (Объект), позволяет вывести данные в соответствующем формате.
Как мы увидим позже, заполнять набор данных можно с помощью этих коллекций, без получения данных от их источника Для этого просто следует добавить таблицы, столбцы или строки в соответствующие коллекции.

Основные сведения о наборах данных


Можно также выбрать подмножество данных из объекта DataSet (Набор данных). Метод Select (Выбрать) класса DataTable (Таблица данных) имеет синтаксис, совпадающий с синтаксисом фразы "where" в SQL-запросах. Для доступа к полям строки используются имена столбцов Ниже приведен пример из описания класса HotelBroker (Посредник, бронирующий места в гостинице), в котором этот метод используется для получения списка отелей определенного города.
ArrayList *GetHotels(String *city)
{
try
{
DataTable *t = hotelsDataset->
Tables->get_Item("Hotels"); // Гостиницы
DataRow *rows [] = t->Select( // Выбор
String::Format("City = '{0}'", city)); // Строка:: Формат ("Город = ' {0} ' ", город));
ArrayList *hotels = new ArrayList;
for (int i=0; i < rows->Length; i++)
{
String *name = rows[i]->get_Item(
"HotelName") ->ToString {) ->Tnm() ; // Вырезка
hotels->Add(name); // гостиницы-> Добавить (название);
}
return hotels; // гостиницы
}
catch(Exception *e) // Исключение
{
throw e;
}
}
Метод AddHotel класса HotelBroker (Посредник, бронирующий места в гостинице) иллюстрирует, как добавляется новая строка в объект DataSet (Набор данных) При этом создается новый экземпляр класса DataRow и для добавления данных в соответствующие поля используются имена столбцов
Если необходимо сохранить созданную строку в базе данных, используется метод Update (Обновить) класса SqlDataAdapter Он является промежуточным звеном между объектом DataSet (Набор данных) и базой данных Позже мы обсудим, как производить транзакционное редактирование набора данных для того, чтобы принять или отвергнуть изменения до их передачи в базу данных
String *AddHptel( // Строка
String *city, // Строка
String *name, // Строка
int number, // номер
Decimal rate) // Десятичная цена
{
try
{
DataTable *t = hotelsDataset->Tables->get_Item( // Таблицы
"Hotels"); // Гостиницы
DataRow *r = t->NewRow();
r->set_Item("HotelName", name); // название
r->set_Item("City", city); // ("Город", город)
r->set_Item("NumberRooms", _box(number));
r->set_Item("RoomRate", _box(rate));
t->Rows->Add(r); // Строки-> Добавить
hotelsAdapter->Update(hotelsDataset, "Hotels"); // Обновить "Гостиницы"
}
catch(Exception *e) // Исключение
{
throw e;
}
}
Для удаления строки из объекта DataSet (Набор данных) прежде всего необходимо найти эту строку или строки, а затем вызвать метод Delete (Удалить) для каждого из удаляемых экземпляров DataRow. Метод Remove (Удалить) удаляет экземпляр DataRow из коллекции Этот экземпляр не помечается как удаленный, так как он уже не является частью объекта DataSet (Набор данных) При вызове метода Update (Обновить) преобразователя данных соответствующие данные не будут удалены из базы данных Приведем фрагмент метода DeleteHotel класса HotelBroker (Посредник, бронирующий места в гостинице)
String *DeleteHotel(String *city, String *name) // Строка
*DeleteHotel (Строка *city, Строка *name)
{
try
{
t = hotelsDataset->Tables->get_Item("Hotels"); // Таблицы-> get_Item ("Гостиницы")
r = t->Select ( // Выбор String::Format(
// Строка:: Формат (
"City = '{0}' and HotelName = '{!}'",
// "Город = ' {0} ' и HotelName ='{!}' ",
city, name)); // город, название
for (i=0; i<r->Length; i++)
r[i]->Delete (); // Удалить
}
Для изменения строки набора данных достаточно просто найти эту строку и внести необходимые изменения в поля строки. В качестве примера ниже приведен фрагмент реализации метода ChangeRooms класса HotelBroker (Посредник, бронирующий места в гостинице). При вызове метода Update (Обновить) преобразователя данных, все изменения, сделанные в этом фрагменте, будут переданы базе данных.
String *ChangeRooms( // Строка
String *city, // Строка
String *name, // Строка
int numberRooms,
Decimal rate) // Десятичная цена
{
DataTable *t = 0;
try
{
t = hotelsDataset->Tables->get_Item("Hotels"); // Таблицы-> get_Item ("Гостиницы")
DataRow *r [] = t->Select( // Выбор
String::Format(
// Строка:: Формат (
"City = '{0}' and HotelName = '{I}1",
// "Город = ' {0} ' и HotelName = ' {1} ' ",
city, name)); // город, название
for (int i = 0; i < r->Length; i++) {
r[i]->set_Item("NumberRooms", _box(numberRooms));
r[i]->set_Item("RoomRate", _box(rate)) ; }
} }

Обновление источника данных


Каким образом метод SqlDataAdapter: :Update (Обновить) передает источнику данных информацию о произведенных изменениях? Изменения, внесенные в объект Da-taSet (Набор данных), передаются базе данных с помощью свойств InsertCommand, UpdateCommand (Команда обновления) и DeleteCommand класса SqlDataAdapter. Каждому из этих свойств присваивается экземпляр SqlCommand, который может быть параметризован для того, чтобы поставить в соответствие переменные программы частям SQL-запроса. Продемонстрируем это на примере кода, взятого из реализации конструктора класса HotelBroker (Посредник, бронирующий места в гостинице).
Экземпляр SqlCommand создается для представления параметризованного SQL-запроса, который используется при вызове метода SqlDataAdapter:: Update (Обновить) для добавления в базу данных новой строки. В момент вызова метода вместо параметров будут подставлены фактические значения.
SqlCommand *cmd = new SqlCommand(
"insert Hotels(City, HotelName, NumberRooms, RoomRate)
// "вставить Гостиницы (Город, HotelName,
// NumberRooms, RoomRate)
values(@City, @Name, SNumRooms, @RoomRate)", // значения
conn);
Параметры должны быть связаны с соответствующими столбцами в DataRow. Во фрагменте кода метода AddHotel, рассмотренного ранее, столбцы различались по именам: HotelName, City (Город), NumberRooms, RoomRate. В конструкторе SqlParame-ter им соответствуют параметры @Name, @City, @NumRooms, @RoomRate. Последний аргумент инициализирует свойство Source (Источник) объекта SqlParameter. Свойство Source (Источник) определяет столбец объекта DataSet (Набор данных), которому соответствует параметр. Метод Add (Добавить) помещает параметр в коллекцию объектов Parameter (Параметр), связанную с экземпляром SqlCommand.
SqlParameter *param = new SqlParameter(
"@City", SqlDbType::Char, 20, "City");
cmd->Parameters->Add(param); // Параметры-> Добавить
cmd->Parameters->Add(new SqlParameter( // Параметры-> Добавить
"@Name", SqlDbType::Char, 20, "HotelName"));
cmd->Parameters->Add (new SqlParameter( // Параметры-> Добавить
"@NumRooms", SqlDbType::Int, 4, "NumberRooms"));
cmd->Parameters->Add(new SqlParameter( // Параметры-> Добавить
"@RoomRate", SqlDbType::Money, 8, "RoomRate"));
И, наконец, свойству InsertCommand класса SqlDataAdapter присваивается указатель на экземпляр класса SqlCommand. Отныне именно эта команда будет использоваться при вставке строки в базу данных:
hotelsAdapter->InsertCommand = cmd;
Аналогичный исходный код есть в конструкторе класса HotelBroker (Посредник, бронирующий места в гостинице). Различие лишь в том, что там устанавливаются значения свойств UpdateCommand (Команда обновления) и DeleteCommand для определения команд обновления и удаления строк.
hotelsAdapter->UpdateCommand = new SqlCommand(
"update Hotels set NumberRooms = @NumRooms, RoomRate = @RoomRate where City = @City and HotelName = @Name",
// где Город = @City и HotelName = @Name ", conn); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@City", SqlDbType::Char,20, "City")); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@Name", SqlDbType:-.Char, 20, "HotelName")); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@NumRooms", SqlDbType::Int, 4, "NumberRooms")); hotelsAdapter->UpdateCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@RoomRate",SqlDbType::Money, 8, "RoomRate"));
hotelsAdapter->DeleteCoiranand = new SqlCommand(
"delete from Hotels where City = @City and HotelName = // "удалить из Гостиниц где Город = @City и HotelName = @Name", conn);
hotelsAdapter->DeleteCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"SCity", SqlDbType::Char, 20, "City"));
hotelsAdapter->DeleteCommand->Parameters->Add(
// Параметры-> Добавить new SqlParameter(
"@Name", SqlDbType::Char, 20, "HotelName"));
Все изменения, внесенные в объект DataSet (Набор данных), будут переданы базе данных при выполнении метода SqlDataAdapter: : Update (Обновить). Как принять или отменить внесенные изменения до вызова этого метода, будет рассмотрено в следующем разделе.

Автоматически генерируемые свойства команд


Для определения свойств InsertCommand, UpdateCommand (Команда обновления) и DeleteCommand можно использовать класс SqlCommandBuilder. Но из-за того, что для динамического определения этих свойств класс SqlCommandBuilder должен получить нужную информацию, его использование повлечет за собой необходимость произвести несколько дополнительных обращений к базе данных и уменьшение производительности. Поэтому, если структура базы данных, используемой приложением, известна при его разработке, лучше определять свойства InsertCommand, UpdateCommand (Команда обновления) и DeleteCommand явно. Это поможет избежать снижения производительности. Если же структура базы данных точно не известна, но пользователь определил запрос, то SqlCommandBuilder можно использовать для обновления результатов "при последующих запросах.
Этот метод действенен для экземпляров DataSet (Набор данных), соответствующих единичной таблице. Если же данные в наборе данных — результат запроса, использовавшего соединение данных, или между таблицами в наборе данных есть связи, то механизм автоматической генерации не сможет корректно определить команду, обновляющую данные в обеих таблицах. Так как SqlCommandBuilder использует при генерации команд свойство SelectCommand, оно должно быть определено.
Для того чтобы описанный метод работал корректно, в таблице, содержащейся в наборе данных, должен быть главный или единственный столбец. Этот столбец будет результатом выполнения SQL-запроса, установленного в свойстве SelectCommand. Главный столбец используется при обновлении или удалении данных как where-фраза.
Имена столбцов не должны содержать специальных символов, таких, как пробелы, запятые, точки, кавычки или другие символы, отличные от буквенно-цифровых. Это обязательно даже тогда, когда имя взято в скобки. Имя таблицы можно задавать полностью, как, например, SchemaName . OwnerName . TableName.
Простейший способ использования класса SqlCommandBuilder— передача экземпляра класса SqlDataAdapter конструктору SqlCommandBuilder в качестве аргумента. После этого объект SqlCommandBuilder зарегистрирует себя в качестве обработчика события RowUpdating. А дальше он сможет генерировать необходимые команды InsertCommand, UpdateCommand (Команда обновления) и DeleteCoramand до обновления строки.

Транзакции и обновление базы данных


Когда преобразователь данных обновляет содержимое базы данных, это не делается одной транзакцией. Если необходимо, чтобы несколько операций выполнялись за одну транзакцию, в программе следует предусмотреть управление транзакциями.
Объект SqlConnection содержит метод BeginTransaction, возвращающий объект SqlTransaction. При вызове метода BeginTransaction следует определить уровень локализации (выполняемых операций). Когда вы точно знаете, что делаете, и понимаете внутреннюю суть вещей, вы можете повысить производительность и масштабируемость приложения установкой соответствующего уровня локализации (выполняемых операций). Если установить уровень локализации (выполняемых операций) некорректно или даже просто неподходящим образом, это может привести к некорректности или несогласованности полученных данных.
Для выполнения или отмены транзакции в классе имеются методы Commit (Фиксировать) и Rollback (Откат). Вы открываете SqlConnection, вызываете метод BeginTransaction, используете SqlDataAdapter как обычно, а затем вызываете SqlTransaction::Commit (Фиксировать) или SqlTransaction::Rollback (Откат), в зависимости от необходимости. Затем закрываете соединение. Для установки точки сохранения (save point) транзакции используется метод Save (Сохранить).
В целях минимизации используемых ресурсов, а, следовательно, для повышения масштабируемости вашего приложения, может оказаться желательным минимизировать промежуток времени между вызовами методов BeginTransaction и Commit (Фиксировать) или Rollback (Откат).
Приведем фрагмент кода из примера Transaction. В нем используется база данных AirlineBroker, описанная в предыдущей главе. Для иллюстрации здесь используется объект SqlCommandBuilder, рассмотренный выше.
conn = new SqlConnection(ConnString);
conn->0pen(); // Открыть
trans = conn->BeginTransaction();
da = new SqlDataAdapter;
ds = new DataSet; // новый Набор данных
da->SelectCommand =
new SqlCommand(and, conn, trans);
SqlCommandBuilder *sb = new SqlCommandBuilder(da);
da->Fill(ds, "Airlines"); // Авиалинии
DataRow *newRow = ds->Tables->get_Item( "Airlines")->NewRow(); // Авиалинии
newRow->set_Item("Name", S"Midway"); // Название, "На полпути" newRow->set_Item("Abbreviation", S"M"); // Сокращение newRow->set_Item("WebSite", S"www.midway.com"); // Web-узел newRow->set_Item("ReservationNumber", S"555-555-1212"); ds->Tables->get_Item("Airlines")->Rows->Add(newRow); // Авиалинии
Console::WriteLine(
sb->Get!nsertCommand()->CommandText); Console::WriteLine (
sb->GetDeleteCommand()->CommandText); Console::WriteLine(
sb->GetUpdateCommand()->CommandText); pEnum =
sb->GetInsertCommand()->
Parameters->GetEnumerator(); // Параметры while (pEnum->MoveNext()) {
SqlParameter *p =
dynamic_cast<SqlParameter *>(pEnum->Current); Console::WriteLine (
"{0, -10} {I, -10}", p->ParameterName, p->SourceColumn); }
da->Update(ds, "Airlines"); // Авиалинии trans->Commit(); trans = 0; conn->Close () ;
Для полной уверенности в корректности работы источника данных SQL Server следует использовать методы Commit (Фиксировать) и Rollback (Откат) объекта SqlTrans-action для подтверждения или отмены транзакции, выполнение которой начато вызовом метода SqlConnection: : BeginTransaction. При этом не стоит использовать операторы транзакций SQL Server.
Если вы в своей работе с базой данных используете хранимые процедуры, вы можете, конечно, использовать операторы транзакций SQL Server внутри хранимых процедур вместо объекта SqlTransaction. Хранимые процедуры могут инкапсулировать изменения, произведенные в результате транзакций. Это делает, в частности, хранимая процедура MakeReservation базы данных HotelBroker (Посредник, бронирующий места в гостинице).

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика