Классы каркаса, предназначенного для работы с базами данных, собраны в 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 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();
}
<Рис. 9.1. Окно среды разработки Visual Studio NET Server Explorer
Если СУБД SQL Server установлена и работает корректно, причем база данных Northwmd существует, результатом работы программы JustConnect будет следующее сообщение:
Connection to Northwmd opened successfully.
(Соединение с Northwmd открылось успешно.)
Если же что-то происходит не так, как должно, при выполнении метода Open (Открыть) возникает исключение и пользователь увидит сообщение, определенное в обработчике исключений. Например, если закрыть SQL Server, программа выведет следующее сообщение:
Connection to Northwmd opened successfully.
General network error. Check your network documentation.
(Общая сетевая ошибка. Сверьтесь с вашей сетевой документацией.)
Если изменить имя базы данных, заданное в строке соединения, на имя несуществующей базы, например, Southwind, будет выведено следующее сообщение:
Connection to Northwmd opened successfully.
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;
Connection to Northwmd opened successfully.
Далее инициализируется строка соединения с базой данных Вы можете изменить значение поля, предназначенного для хранения имени сервера, на имя своего компьютера Необходимо также определить имя пользователя и пароль для получения доступа к базе данных Строку соединения можно устанавливать и как свойство объекта 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);
<Рис. 9.2. Таблицы и хранимые процедуры, входящие в состав базы данных Northwind, отображаются в окне Server Explorer
Если команда выполняется посредством использования метода 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 (Получить данные из таблицы) и, просмотрев данные, хранящиеся в таблице, сравните их с результатом работы программы. Наверняка вы заметите поразительное сходство (рис. 9.3).
<Рис. 9.3. Просмотр содержимого таблицы Customers (Клиенты) и ее полей с помощью Server Explorer
Использовавшийся в предыдущем примере режим называют соединенным. Программа соединяется с базой данных, выполняет все необходимые действия, а затем отсоединяется. При этом перемещаться по данным базы можно только в одном направлении. Это соответствует однонаправленному курсору/набору записей в классической технологии доступа к данным 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, кроме его закрытия, недоступны.
Метод 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.
<Рис. 9.4. Таблица Customers (Клиенты) после внесения в нее изменений Сравните первую строку таблицы с ее первоначальным видом на рис. 9.3.
При его создании 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; iNextResult() == 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 (Набор данных) соответствует параметр.
Класс 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 (Набор данных) считываются также и связанные с этими данными таблицы и столбцы. Каждый набор данных имеет коллекции, представляющие все таблицы, столбцы и строки, связанные с данными, содержащимися в этом наборе.
Класс 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(pEnum->Current);
Console::Write("{0, -20}", c->ColumnName);
}
Console::WriteLine("");
pEnum = t->Rows->GetEnumerator();
while (pEnum->MoveNext())
{
DataRow *r =
dynamic_cast (pEnum->Current) ;
for (int i=0; iColunms->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; iLength; 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(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 (Посредник, бронирующий места в гостинице).
Транзакции только помогают сохранить непротиворечивость базы данных. Если вы переводите деньги со сберегательного счета на текущий для оплаты счета за телефон, транзакции помогут гарантировать, что деньги будут сняты с одного счета и появятся на другом (или же не произойдет ни того, ни другого). Вы не столкнетесь ни с ситуацией, когда деньги придут на текущий счет, но не будут сняты со сберегательного (это было бы неплохо для вас, но плохо для банка), ни с противоположной ситуацией (неприятной для вас, но хорошей для банка). Ничто не помешает вашей супруге потратить эти деньги на ужин в модном ресторане.
При оптимистическом блокировании предполагается, что ничего подобного не случится, но вам следует быть готовым к такой ситуации, если она все-таки возникнет. Использование пессимистического блокирования требует координации действий всех пользователей таблицы базы данных таким образом, чтобы предотвратить подобную ситуацию. Разумеется, чем меньше блокировок накладывается на столбец базы данных, тем шире возможности использования вашего приложения.
Следует понимать, что такая ситуация влияет и на считывание данных, и на их обновление. Скажем, если ваша супруга видит, что на счету есть деньги, и строит свои планы относительно этих денег, это может привести к не меньшим проблемам, чем просто потеря денег с общего текущего счета.
Хотя обсуждение способов решения таких проблем выходит далеко за пределы рассматриваемого в этой главе материала, важно помнить, что они возникают, если записи, считанные в объект DataSet (Набор данных), не блокированы. Использование SqlDa-taAdapter при работе с DataSet (Набор данных) предполагает применение оптимистической стратегии блокировки.
Почему это так важно? Прежде всего потому, что от этого зависит производительность и масштабируемость вашего приложения. А почему это так сложно? Потому что нельзя дать совета, подходящего для всех приложений в любой ситуации. Когда пользователи не обращаются одновременно к одним и тем же данным, использование оптимистической стратегии блокировок является наилучшим вариантом. Если необходимо заблокировать доступ к записи на долгий период времени, время ожидания получения доступа к этой записи значительно увеличится, понижая тем самым производительность и масштабируемость приложения.
Вы должны понимать, что такое уровни локализации транзакций, администратор блокировок базы данных, и что существует возможность конфликта при доступе к данным, а такой конфликт может привести к зависанию приложения. Вы должны понимать, сколько времени и ресурсов может потратить ваше приложение на разрешение конфликтов, как оно должно поступать с несогласованными или некорректными данными. Все это необходимо для принятия решения, в каких ситуациях допускается попытка избежать зависания любой ценой, и как следует поступать при возникновении последовательности конфликтующих операций.
Иногда может понадобиться использовать объект DataSet (Набор данных) с дополнительно реализованными возможностями для проверки того, были ли изменены записи, содержащиеся в нем, со времени их последней выборки или модификации. А можно просто использовать SqlDataReader и заново произвести выборку. Все это зависит от ситуации.
Так, при бронировании комнат в нашем примере HotelBroker (Посредник, бронирующий места в гостинице) нельзя делать оптимистических предположений о наличии свободных мест. Это равносильно предположению о бесконечном количестве комнат в отеле и приведет к ситуации, когда администратор должен будет распределить ограниченное количество комнат на гораздо большее количество желающих. В нашем примере для проверки того, зарезервирована ли комната, используется хранимая процедура MakeReservation.
Иногда, даже при отсутствии одновременных запросов, объект DataSet (Набор данных) нельзя использовать для добавления новой строки без установления связи с базой данных. В нашем примере HotelBroker (Посредник, бронирующий места в гостинице) нельзя использовать произвольный первичный ключ. Бронирование могут производить одновременно несколько пользователей. Поэтому идентификаторы бронирования не могут быть локальными. Их определение должно производиться самой базой данных. В нашем примере это делает хранимая процедура MakeReservation.
Способ использования отсоединенных операций в вашем приложении следует определить еще прежде, чем вы решите, каким образом будут использоваться объекты SqlDataReader и DataSet (Набор данных).
Зачем вообще в приложении HotelBroker (Посредник, бронирующий места в гостинице) используется DataSet (Набор данных)? Фактически, в реализации объекта Customer (Клиент) DataSet (Набор данных) никак не используется. Но его использует объект HotelBroker (Посредник, бронирующий места в гостинице), и делается это по двум причинам. Первая — педагогическая. Мы хотели показать, как объект DataSet (Набор данных) может быть использован в полноценном приложении, а не только в простой программе. Во-вторых, в Web-ориентированной версии приложения, реализованной в последующих главах книги, удобно производить кэширование некоторых данных. Например, вполне разумно сделать так, чтобы пользователь мог работать с локальной копией системы бронирования. С другой стороны, такую информацию, как электронный адрес пользователя, достаточно запросить один раз — при его регистрации в системе. Поэтому в нашем случае нет необходимости в сложном механизме кэширования информации о пользователе, так что реализованный объект Customer (Клиент) использует методы объекта SqlCommand.
На рис. 9.5 представлена иерархия объектов, содержащихся в DataSet (Набор данных). Прежде, чем приступить к материалу, излагаемому ниже, полезно ознакомиться с этой диаграммой.
Рис. 9.5. Иерархия класса DataSet (Набор данных)
Каждый объект DataSet (Набор данных) содержит коллекцию из одного или более объектов1,DataTable (Таблица данных). Каждый объект DataTable (Таблица данных) соответствует одной таблице. С помощью свойства SelectCommand, в котором содержится операция соединения, можно производить выборку из нескольких таблиц базы данных в один объект DataTable (Таблица данных). При необходимости обновить содержимое множественных таблиц достаточно определить лишь команду обновления, так как информация о связях между таблицами базы данных уже известна. В файле Hotel-Bookings, h нашего примера свойство SelectCommand объекта SqlDataAdapter, содержащегося в объекте HotelBroker (Посредник, бронирующий места в гостинице), определено следующим образом:
String *cmd = // Строка
"select Customerld, HotelName, City, ArrivalDate,
// выбрать
DepartureDate, Reservationld from Reservations, Hotels
where Reservations.Hotelld = Hotels.Hotelld";
// где Резервирование.Hotelld = Гостиницы.Hotelld";
//adapter->SelectCommand = new SqlCommand(cmd, conn);
//dataset = new DataSet;
// новый Набор данных adapter->Fill(dataset, "Reservations");
// Заполнить (набор данных, "Резервирование")
В этом случае DataSet (Набор данных) содержит один объект DataTable (Таблица данных), представляющий таблицу, называющуюся Reservations (Резервирование). Информация о том, что некоторые данные получены из таблицы Hotels, не сохраняется.
В один набор данных можно загрузить данные нескольких таблиц. Это продемонстрировано в примере DataSchema, в котором используется база данных Northwind.
adapter->SelectCommand = new SqlCommand(
"select * from [Order Details] where Productld = 1",
// "выбрать * из [Подробности заказа] где Productld = 1 ",
conn);
adapter->FillSchema (
dataset, SchemaType::Source, "Order Details");
// набор данных, SchemaType:: Источник, " Подробности заказа");
adapter->Fill(dataset, "Order Details");
// Заполнить (набор данных, " Подробности заказа");
adapter->SelectCommand =
new SqlCommand("select * from Shippers", conn);
// выбрать * из Грузоотправителей adapter->FillSchema(
dataset, SchemaType::Source, "Shippers");
// набор данных, SchemaType:: Источник, "Грузоотправители");
// adapter->Fill(dataset, "Shippers");
// Заполнить (набор данных, "Грузоотправители");
В этом случае объект DataSet (Набор данных) содержит две таблицы, OrderDetails и Shippers. Метод SqlDataAdapter: :FillSchema заполняет DataSet (Набор данных) данными из таблиц, а также информацией о первичных ключах, связанных с таблицами. Затем программа просматривает содержимое таблиц и выводит на печать данные и первичные ключи таблиц. Доступ к содержащимся в DataTable (Таблица данных) объектам DataColumn осуществляется с помощью коллекции Columns (Столбцы), также являющейся частью объекта DataTable (Таблица данных).
lEnumerator *pEnum = dataset->Tables->GetEnumerator();
// набор данных-> Таблицы while (pEnum->MoveNext ()) {
DataTable *t =
dynamic_cast(pEnum->Current);
Console::WriteLine(t->TableName);
DataColumn *dc [] = t->PrimaryKey;
for (int i=0; iLength; i++)
{
Console::WriteLine(
"\tPrimary Key Field {0} = {!}",
// " \t Поле первичного ключа {0} = {1} ", _box(i),
dc[i]->ColumnName); }
Console::Write("\t"); // Запись
lEnumerator *pEnum = t->Columns->GetEnumerator();
// Столбцы while (pEnum->MoveNext()) {
DataColumn *c =
dynamic_cast(pEnum->Current);
Console::Write ("{0, -20}", c->ColumnName); // Запись }
Console::WriteLine("");
pEnum = t->Rows->GetEnumerator() ;
// Строки while (pEnum->MoveNext()) {
DataRow *r =
dynamic_cast(pEnum->Current) ;
Console::Write("\t");
// Запись for (int i=0; iItemArray.Length; i++)
Console::Write ( // Запись "{0, -20}",
r->get_Item(i)->ToString()->Trim());
// Вырезка Console::WriteLine(""); } }
Программа выводит на экран название таблицы, первичные ключи, названия столбцов и данные таблиц:
Order Details
Primary Key Field 0 = OrderlD
Primary Key Field 1 = ProductID
OrderlD ProductID UnitPrice Quantity Discount
10285 1 14.4 45 0.2
10294 1 14.4 18 0
Shippers
Primary Key Field 0 = ShipperlD ShipperlD CompanyName Phone
1 Speedy Express (503) 555-9831
2 United Package (503) 555-3199
3 Federal Shipping (503) 555-9931
А вот и перевод:
Подробности заказа
Поле первичного ключа 0 = OrderlD
Поле первичного ключа 1 = ProductID
Идентификатор заказа Идентификатор продукта Цена Количество Скидка
10285 1 14.4 45 0.2
10294 1 14.4 18 О
Грузоотправители
Поле первичного ключа 0 = ShipperlD
Идентификатор грузоотправителя Название компании Телефон
1 Быстрый экспресс (503) 555-9831
2 Объединение пакет (503) 555-3199
3 Федеральная отгрузка (503) 555-9931
DataSet (Набор данных) можно использовать как резидентную реляционную базу данных, не связанную ни с какой другой базой данных. Теперь на примере программы DataEditing, мы рассмотрим несколько возможностей объекта DataSet (Набор данных), связанных с добавлением данных и отношений непосредственно в набор данных, без обращения к какой бы то ни было внешней базе данных.
Прежде всего создадим новый объект DataSet (Набор данных) и включим проверку ограничений. Затем добавим в DataSet (Набор данных) четыре объекта DataTable (Таблица данных): Books (Книги), Categories (Категории), Authors (Авторы) и BookCate-gories (Категории книг).
DataSet *ds = new DataSet;
// новый Набор данных ds->EnforceConstraints = true;
// истина
// Добавить (Add) таблицы (tables) к Набору данных
// (DataSet) DataTable *categories =
ds->Tables->Add("Categories");
// Таблицы-> Добавить ("Категории"); DataTable *bookcategories =
ds->Tables->Add("BookCategories");
// Таблицы-> Добавить ("BookCategories");
//DataTable *authors = ds->Tables->Add("Authors");
// Таблицы-> Добавить ("Авторы");
//DataTable *books = ds->Tables->Add("Books");
// Таблицы-> Добавить ("Книги");
Объект DataTable (Таблица данных) содержит коллекцию объектов DataColumn, каждый из которых представляет собой столбец таблицы. Теперь добавим столбцы в определения таблиц.
// определить типы для определений столбцов
Type *stringType = Туре::GetType("System.String");
// Система.Строка
Type *intType = Туре::GetType("System.Int32");
// Определить столбцы для таблиц
// Добавить столбец (column) в таблицу Category
//(Категория) DataColumn *categoryname =
categories->Columns->Add(
// категории-> Столбцы-> Добавить "Category",stringType);
// Категория
// Добавить (Add) столбцы (columns) для таблицы
//BookCategories DataColumn *cn = bookcategories->Columns->Add(
// Столбцы-> Добавить
"CategoryName", stringType);
DataColumn *loc = bookcategories->Columns->Add(
// Столбцы-> Добавить
"LibraryofCongressNumber", stringType);
// Добавить (Add) столбцы (columns) для таблицы
//Authors (Авторы) DataColumn *auid = authors->Columns->Add(
// авторы-> Столбцы-> Добавить
"AuthorId", intType); authors->Columns->Add(
// авторы-> Столбцы-> Добавить
"AuthorLastName", stringType); authors->Columns->Add(
// авторы-> Столбцы-> Добавить
"AuthorFirstName", stringType);
// Добавить (Add) столбцы (columns) для таблицы Books (Книги)
//DataColumn *ISBN = books->Columns->Add( // книги-> Столбцы-> Добавить
"ISBN", stringType);
DataColumn *booksauid = books->Columns->Add(
// книги-> Столбцы-> Добавить
"AuthorId", intType);
books->Columns->Add("Title", stringType);
// книги-> Столбцы-> Добавить ("Название", stringType);
//DataColumn *bloc = books->Columns->Add(
// книги-> Столбцы-> Добавить
"LibraryofCongressNumber", stringType);
Каждый объект DataTable (Таблица данных) содержит коллекцию объектов Da-taRow. Каждый такой объект представляет строку таблицы. Добавление нового объекта DataRow влияет на ограничения объекта DataColumn (мы предполагаем, что свойство Enf orceConstraints объекта DataSet (Набор данных) имеет значение true (истина)).
Первичные ключи
Существует несколько типов ограничений. Первичный ключ — уникальный ключ для строк таблицы. Другие ограничения единственности определяют единственность каждого значения в столбце (или столбцах). Внешний ключ используется для обозначения того, что значения в столбце являются первичными ключами для другой таблицы объекта DataSet (Набор данных). Первичный ключ объекта DataTable (Таблица данных) является свойством:
// Определить РК для таблицы BookCategories
//DataColumn *bookcategoriesPK [] =
new DataColumn*[2]; bookcategoriesPK[0] = en;
bookcategoriesPK[l] = loc;
bookcategories->PrimaryKey = bookcategoriesPK;
// Определить РК для таблицы Authors (Авторы)
//DataColumn *authorsPK [] =
new DataColumn*[1]; authorsPK[0] = auid;
authors->PrimaryKey = authorsPK; // авторы
// Определить РК для таблицы Books (Книги)
//DataColumn *booksPK [] =
new DataColumn*[1]; booksPK[0] = ISBN;
books->PrimaryKey = booksPK; // книги
Ограничения
Для работы со всеми ограничениями помимо первичных ключей используются абстрактный базовый класс Constraint (Ограничение) и его производные классы: UniqueConstraint и ForeignKeyConstraint. Базовый класс обеспечивает возможность помещения ограничения в коллекцию ограничений таблицы. Первичный ключ также регистрируется в этой коллекции как ограничение единственности с именем, генерируемым системой. Для определения, является ли ограничение первичным ключом, используется свойство UniqueConstraint::IsPrimaryKey.
Определим уникальность значений столбца Category таблицы Categories (Категории). Так как последний аргумент метода Add (Добавить) имеет значение false (ложь), это ограничение не будет первичным ключом таблицы. Для этой таблицы мы не определяем первичного ключа, а задаем только ограничения единственности. Вообще говоря, задавать ограничения для значений таблицы не обязательно. Хотя это и нарушает правила реляционной целостности, никто не заставляет вас использовать объект DataSet (Набор данных) реляционным способом. // Определим ограничение единственности // для таблицы Categories (Категории) categones->Constraints->Add ( // категории-> Ограничениям Добавить
"Unique CategoryName Constraint",
// "Уникальное ограничение CategoryName ",
categoryname,
false); // ложь
При использовании внешнего ключа можно определить действия, которые следует выполнить при изменении первичного ключа, с которым он связан. Выбор здесь стандартен: None (Ничего), Cascade (Каскад), SetNull. Для установки значения, принимаемого по умолчанию для этого параметра (он описывается в свойстве Def aultValue объекта DataColumn), используется метод SetDefault. Эти параметры могут быть определены как для условий обновления, так и для условий удаления данных.
В нашем примере внешний ключ определяется таким образом, чтобы все идентификаторы авторов, содержащиеся в таблице Books (Книги), были описаны также и в таблице Authors (Авторы). Другими словами, у каждой книги, зарегистрированной в базе, есть автор, который также зарегистрирован в этой же базе. Мы назвали это ограничение '"Authors->Books". При изменении идентификатора автора правила обновления данных вынуждают объект DataSet (Набор данных) изменить этот идентификатор и во всех остальных строках таблиц на новое значение. Когда идентификатор удаляется, значение для этого идентификатора в строках, описывающих книги, будет установлено пустым.
Если при этом свойство DeleteRule имеет значение Cascade (Каскад), то каскадное удаление будет выполнено для всех таких строк из таблицы Books (Книги). Свойство Ас-ceptRejectRule используется при транзакционном изменении объекта DataSet (Набор данных) и будет рассмотрено ниже. Значение этого свойства определяет, что произойдет при вызове метода AcceptChanges объектов DataSet (Набор данных), Da-taRow или DataTable (Таблица данных). В нашем примере изменения будут произведены последовательно со всеми данными. Другое возможное значение этого свойства — None (не совершать никаких действий).
// Определить FK для таблицы Books (Книги)
// (Authorld должен быть в таблице Authors
//(Авторы)) DataColumn *bookauthorFK [] =
new DataColumn*[1];
bookauthorFK[0] = booksauid;
ForeignKeyConstraint *fk =new ForeignKeyConstraint(
"Authors->Books", authorsPK, bookauthorFK); // Авторы-> Книги
fk->AcceptRe;jectRule = AcceptRejectRule::Cascade;
// Каскад fk->DeleteRule = Rule::SetNull;
//fk->UpdateRule = Rule:rCascade;
// Каскад books->Constraints->Add(fk);
// книги-> Ограничениям Добавить
Связи между данными
Кроме ограничений для данных, можно задавать связи между ними, для хранения которых используется коллекция DataRelation объекта DataSet (Набор данных). Связи соединяют таблицы таким образом, что вы можете перемещаться от предка к потомку и наоборот. При добавлении связи в коллекцию ограничений автоматически добавляется и соответствующий внешний ключ.
В нашем примере таблица Categories (Категории) сделана предком таблицы Book-Categories (Категории книг) через столбцы Categories (Категории) и CategoryName. Оба столбца, между которыми определяется связь, должны содержать данные одного типа. Эту связь можно использовать для нахождения строк в таблице-потомке или строки в таблице-предке по значению поля, соответствующего связанному столбцу. В нашем примере необходимо также установить связь между столбцами, описывающими номер книги в Библиотеке Конгресса в таблицах Books (Книги) и BookCategory.
// Установим связь между столбцом Categories (Категории)
// в таблице BookCategories (Категории книг) и
// столбцом Categories (Категории) в таблице Categories
//(Категории) ds->Relations->Add(
// Отношения-> Добавить
"Category->BookCategories Relation",
// "Категория-> Отношение BookCategories ",
categoryname,
en) ;
// Установим связь между столбцом Library of Congress Number
// (Номер книги в Библиотеке Конгресса) таблицы Books (Книги) и
// столбцом LOC таблицы BookCategories (Категории книг) ds->Relations->Add(
// Отношения-> Добавить
"Book Category LOC->Book LOC Relation",
loc,
bloc);
Рассмотрим, как можно получить информацию об объекте DataTabie (Таблица данных), точнее, об ограничениях и ключах этого объекта. В предыдущем примере уже было показано, как получить доступ к объектам Data-Column объекта DataTabie (Таблица данных). Обратите внимание на использование свойства IsPrimaryKey объекта UniqueConstraint для определения, является ли ограничение первичным ключом. Следующий фрагмент взят из примера DataEditing.
pEnum = ds->Tables->GetEnumerator(); // Таблицы
while (pEnum->MoveNext() )
{
DataTabie *t =
dynamic_cast<DataTable *>(pEnum->Current);
Console::WriteLine(" {0}", t->TableName);
Console : :WriteLine ( "\tPrimary Key:")/' // Первичный ключ
for (int i = 0; i < t->PrimaryKey.Length; i++)
{
DataColumn *c = t->PrimaryKey[i];
Console::WriteLine("\t\t{0}", c->ColumnName); }
Console::WriteLine("\tConstraints:" ) ;
lEnumerator *pEnum = t->Constraints->GetEnumerator();
// Ограничения while (pEnum->MoveNext()) {
Constraint *c = // Ограничение
dynamic_cast<Constraint *>(pEnum->Current);
// Ограничение
String *constraintName; // Строка
// если (с - ForeignKeyConstraint)
//if (dynamic_cast<ForeignKeyConstraint *>(c) != 0) constraintName =
String::Concat("Foreign Key:",
// Строка:: Concat ("Внешний ключ: ", c->ConstraintName);
else if (dynamic_cast<UniqueConstraint *>(c) != 0) {
UniqueConstraint *u = dynamic_cast<UniqueConstraint *>(c); if (u->IsPrimaryKey)
constraintName = "Primary Key";
// Первичный ключ else
constraintName = u->ConstraintName; } else
constraintName = "Unknown Name";
// Неизвестное имя Console::WriteLine("\t\t{0, -40}", constraintName); } }
Напечатанные программой строки приведены ниже. Обратите внимание на то, что определение связей, осуществленное нами с помощью объектов DataRelation, приводит к появлению объектов ForeignKeyConstraint в коллекции ограничений таблицы.
Первичный ключ также появляется в коллекции ограничений как объект UniqueCon-straint. Ограничения, определенные как ограничения единственности или внешние ключи, появляются в коллекции так, как ожидается.
Categories
Primary Key: Constraints:
Unique CategoryName Constraint BookCategories Primary Key:
CategoryName
LibraryofCongressNumber Constraints:
Primary Key
Foreign Key:Category->BookCategories Relation
Constraint2 Authors
Primary Key:
Authorld Constraints:
Primary Key Books
Primary Key:
ISBN Constraints:
Primary Key
Foreign Key:Authors->Books
Foreign Key:Book Category LOC->Book LOG Relation
Вот перевод этой выдачи:
Категории
Первичный ключ: Ограничения:
Ограничение единственности CategoryName Категории книг
Первичный ключ:
Название категории
Номер в Библиотеке Конгресса Ограничения:
Первичный ключ
Внешний ключ: Отношение Category-> BookCategories
Constraint2 Авторы
Первичный ключ:
Authorld Ограничения:
Первичный ключ Книги
Первичный ключ:
ISBN Ограничения:
Первичный ключ:
Внешний ключ:Авторы-> Книги
Внешний ключ: Отношение Book Category LOC->Book LOG
Обратите внимание на ограничение в таблице BookCategories (Категории книг) с именем, сгенерированным системой. Когда вы внимательно просмотрите исходный код программы, то убедитесь, что эти ограничения в ней не добавляются. Откуда же они берутся? Если бы вы просмотрели содержимое соответствующего объекта, то увидели бы, что ограничение наложено на столбец LibraryofCongressNumber. Система посчитала, что, поскольку столбец CategoryName является внешним ключом для другой таблицы, то значения в столбце LibraryOfCongressNumber должны быть уникальными.
Можно также просмотреть коллекцию связей Relations (Отношения) объекта DataSet (Набор данных). При этом можно узнать, какие таблицы являются предками, и какие именно столбцы в них участвуют в образовании связей. То же самое можно сделать и для таблиц-потомков. Приведем соответствующий фрагмент примера DataEditing.
pEnum = ds->Relations->GetEnumerator(); // Отношения
while (pEnum->MoveNext())
{
DataRelation *dr =
dynamic_cast<DataRelation *>(pEnum->Current);
DataTable ^parentTable = dr->ParentTable;
DataTable *childTable = dr->ChildTable;
Console::WriteLine(
" Relation: {0} ", dr->RelationName); // Отношение Console::WriteLine(
ParentTable: {0, -10}", parentTable); Console::Write(" Columns: ");
// Столбцы forfint j =0; j < dr->ParentColumns.Length; j++) Console::Write(
// Запись
{0, -10}",
dr->ParentColumns[j]->ColumnName); Console::WriteLine(); Console::WriteLine(
ChildTable: (0, -10}", childTable);
Console::Write(" Columns: ");
// Запись forfint j = 0; j < dr->ChildColumns.Length; j++) Console::Write(
// Запись
{0, -10}",
dr->ChildColumns[j]->ColumnName); Console::WriteLine(); }
Программа напечатает:
Output Relations between tables in the DataSet...
Relation: Category->BookCategones Relation
ParentTable: Categories
Columns: Category ChildTable: BookCategories
Columns: CategoryName Relation: Book Category
LOC->Book LOG Relation ParentTable: BookCategories
Columns: LibraryofCongressNumber
ChildTable: Books
Columns: LibraryofCongressNumber
А вот и перевод:
Отношения между таблицами в Наборе данных...
Отношение: Категория-> Отношение Категории книг ParentTable: Категории
Столбцы: Категория ChildTable: Категории книг
Столбцы: CategoryName Отношение: Отношение Категория книги
LOC->Book LOC ParentTable: Категории книг
Столбцы: Номер в Библиотеке Конгресса ChildTable: Книги
Столбцы: Номер в Библиотеке Конгресса
При необходимости внести значительные изменения в объект DataSet (Набор данных) и отложить при этом проверку ограничений и событий, можно использовать режим редактирования набора данных.
BeginEdit. EndEdit, CancelEdit
Переход в режим редактирования осуществляется вызовом метода BeginEdit объекта строки, которую необходимо изменить. Для выхода из этого режима используются методы EndEdit и CancelEdit.
В примере DataEditing мы нарушаем ограничение, заданное внешним ключом, добавляя в таблицу Books (Книги) строку, описывающую книгу автора, идентификатор которого отсутствует в базе данных. Исключение, являющееся результатом этого нарушения, возникнет только после вызова метода EndEdit. Так, при выполнении следующего фрагмента файла DataEditing. h примера DataEditing исключение не возникает.
DataRow *rowToEdit = books->Rows->get_Item(0); // книги-> Строки
rowToEdit->BeginEdit();
try
{
rowToEdit->set_Item("Author!d", _box(21));
}
catch(Exception *e) // Исключение
{
Console::WriteLine(
"\n {0] while editing a row.", e->Message);
// при редактировании строки, Сообщение); Console::WriteLine(); }
Однако исключение возникает, как только в программе вызывается метод EndEdit.
try {
rowToEdit->EndEdit(); }
catch(Exception *e) // Исключение {
Console::WriteLine();
Console::WriteLine(
"\n{0} on EndEdit", e->Message); // Сообщение
Console::WriteLine(); }
В результате будет напечатано следующее сообщение, указывающее на то, что по окончании сеанса изменения содержимого строки обнаружено нарушение ограничения.
ForeignKeyConstraint Authors->Books requires the child key values (21) to exist in the parent table, on EndEdit
Версии объекта DataRow
До того, как будут подтверждены внесенные в строку изменения, доступны и исходные, и измененные значения полей строки. С помощью свойства элемента строки28 Da-taRowVersion можно определить, какое именно значение вы хотите использовать. Это свойство может иметь значения Original (Первоначальное), Default (Заданное по умолчанию), Current (Текущее) и Proposed (Предложенное).
В следующем фрагменте примера DataEditing приведен код, выполняющийся до вызова метода EndEdit:
DataRow *rowToEdit = books->Rows->get_Item(0);
// книги-> Строки rowToEdit->BeginEdit() ; try {
rowToEdit->set_Item("AuthorId", _box(21)); Console::WriteLine(
"Book Author Id Field Current Value {0}",
// Текущее значение поля идентификатора автора книги
rowToEdit->get_Item(
"Authorld", DataRowVersion::Current)); Console::WriteLine(
"Book Author Id Field Proposed Value {0}",
// Предложенное значение поля идентификатора автора книги
rowToEdit->get_Item(
"Authorld", DataRowVersion::Proposed)); // Предложенное Console::WriteLine(
"Book Author Id Field Default Value {0}",
// Значение по умолчанию поля идентификатора автора книги
rowToEdit->get_Item(
"Authorld", DataRowVersion::Default));
// Значение по умолчанию }
В результате программа напечатает:
Book Author Id Field Current Value I Book Author Id Field Proposed Value 21 Book Author Id Field Default Value 21
Вот перевод:
Текущее значение поля идентификатора автора книги 1
Предложенное значение поля идентификатора автора книги 21
Значение по умолчанию поля идентификатора автора книги 21
При выполнении транзакционного редактирования доступны значения Current (Текущее) и Proposed (Предложенное). После вызова метода CancelEdit значение Proposed (Предложенное) становится недоступным. После вызова метода EndEdit значение, имевшее атрибут Proposed (Предложенное) меняет атрибут на Current (Текущее) а значение, имевшее атрибут Proposed (Предложенное), становится недоступным.
Свойство RowState объекта DataRow
Кроме того, что в режиме редактирования доступны значения поля Current (Текущее) и Proposed (Предложенное), сам объект DataRow имеет свойство, описывающее состояние соответствующей строки. Это свойство может принимать значения Added (Добавлено), Deleted (Удалено), Detached (Отсоединено), Modified (Изменено) или Unchanged (He изменено).
Строка находится в состоянии Detached (Отсоединено) в случаях, когда она создана, но либо еще не добавлена ни в одну коллекцию объектов DataRow, либо удалена из какой-нибудь коллекции.
Какое из значений будет возвращено при использовании значения Default (Заданное по умолчанию) свойства DataRowVersion определяется значением свойства RowState.
Принятие или отмена изменений
Вызов метода EndEdit объекта DataRow не приводит к фиксации изменений, сделанных в строке. Вызов методов AcceptChanges или RejectChanges объектов DataSet (Набор данных), DataTable (Таблица данных) или DataRow приводит к выходу из режима транзакционного редактирования для всех строк соответствующего объекта. Если до этого не были вызваны EndEdit или CancelEdit, AcceptChanges или Rejec-tChanges вызывают эти методы для всех строк соответствующего объекта.
После вызова метода AcceptChanges значения, имевшие атрибут Proposed (Предложенное) становятся основными (т.е. имеющими атрибут Original (Первоначальное)). Если при этом свойство RowState имело значение Added (Добавлено), Modified (Изменено) или Deleted (Удалено), ему присваивается значение Unchanged (He изменено), а сами изменения вступают в силу (т.е. строки добавляются, изменяются или удаляются).
После выполнения метода RejectChanges значение, имевшее атрибут Proposed (Предложенное), удаляется. Если при этом свойство RowState имело значение Deleted (Удалено) или Modified (Изменено), значение поля становится прежним и свойству RowState присваивается значение Unchanged (He изменено). Если же RowState имело значение Added (Добавлено), строка удаляется из коллекции Rows (Строки).
Так как после вызова метода AcceptChanges свойство RowState имеет значение Unchanged (He изменено), вызов метода Update (Обновить) объекта DataAdapter не приведет к каким-либо изменениям базы данных. Поэтому, при необходимости внести изменения в базу данных, метод Update (Обновить) следует вызывать до вызова метода AcceptChanges строки, таблицы или объекта DataSet (Набор данных).
Ниже приведен фрагмент реализации метода CancelReservation класса HotelBroker (Посредник, бронирующий места в гостинице) из примера CaseStudy. Фрагмент взят из файла HotelBookings . h, находящегося в папке CaseStudy\HotelBrokerAdmin\Hotel. Обратите внимание, что метод AcceptChanges объекта DataSet (Набор данных) вызывается при успешном завершении работы метода SqlDataAdapter: : Update (Обновить). В случае же возникновения исключения вызывается метод RejectChanges.
void CancelReservation(int id) // идентификатор
{
DataTable *t = 0;
try
{
t = dataset->Tables->get_Item("Reservations");
// набор данных-> Таблицы-> get_Item ("Резервирование");
DataRow *rc [] = t->Select ( // Выбор
String::Format("Reservationld = {0} ", // Строка:: Формат
id.ToString())); // идентификатор for (int i=0; iLength; i++) re[i]->Delete();
// Удалить
int NumberRows = adapter->Update(
// Обновление dataset, "Reservations");
// набор данных, "Резервирование"); if (NumberRows > 0)
// если (NumberRows> 0)
t->AcceptChanges () ; else
t->RejectChanges () ; }
catch(Exception *e) // Исключение {
t->RejectChanges();
throw e; }
return;
}
Если вы не будете отменять внесенные изменения в случае возникновения ошибки, измененные строки останутся в объекте DataSet (Набор данных). Тогда, при попытке произвести следующее обновление, оно также будет отменено из-за того, что строки все еще не обновлены и наличие их приводит к возникновению исключения. Поскольку объект DataSet (Набор данных) независим от других баз данных, тот факт, что данные в базе данных были обновлены, не имеет никакого отношения к принятию или отмене произведенных изменений данных в самом объекте DataSet (Набор данных).
Ошибки объекта DataRow
Если при изменении данных строки произошла ошибка, свойство HasError объекта DataSet (Набор данных), DataTable (Таблица данных) или DataRow примет значение true (истина). Для получения информации об ошибке используются методы GetCol-umnError или GetCoiunmsInError.
К этому моменту мы изложили более чем достаточно материала, необходимого для понимания классов Customer (Клиент) и HotelBroker (Посредник, бронирующий места в гостинице) из версии приложения Acme Travel Agency (Туристическое агентство Acme), ориентированной на работу с базами данных. Как обычно, файлы с исходным кодом для этой версии находятся в папке CaseStudy. Если вы использовали программы, изменяющие содержимое базы данных HotelBroker (Посредник, бронирующий места в гостинице), не забудьте запустить макрос SQL, приводящий эту базу в исходное состояние.
В связи с тем, что у нас не было необходимости хранить какое-либо состояние объекта Customer (Клиент), в нем для доступа к базе данных и получения данных используется объект SqlDataReader Любое состояние, которое может понадобиться программе (например, список клиентов), легко может быть получено у программы-клиента, а не у объекта среднего яруса. Объекты HotelBroker (Посредник, бронирующий места в гостинице) и HotelBookings немного более сложны. Как уже было сказано, из педагогических побуждении эти объекты были реализованы с использованием объекта Data-Set (Набор данных). Так сделано для того, чтобы продемонстрировать использование этой технологии в приложениях. Тем не менее, мы увидим, что при разработке Web-ориентированых приложении есть причины сохранять некоторые состояния в среднем ярусе. В этом случае объект DataSet (Набор данных) служит интеллектуальным кэшем.
А теперь отвлечемся от примера и рассмотрим интеграцию ХМL с базой данных.
Как будет показано в главе 11 "Web-службы", XML имеет много преимуществ при описании данных, которые нужно перемещать между разнородными системами и источниками данных. Поскольку вы можете обеспечить данные XML описанием схемы данных XML, во многих случаях имеет смысл передавать именно такие данные, а не DataSet (Набор данных). Так как данные ХМL являются текстом, они могут проходить через порты брандмауэров, которые обычно открыты, в отличие от протокола распределенной модели компонентных объектов DCOM (Distributed COM — Distributed Component Object Model) или протокола RMI, используемого в JAVA, которые требуют открытия особых портов.
Мы не ставим себе цель обсудить в следующих разделах все детали языка XML. Мы хотим только продемонстрировать, как можно использовать концепции данных, принятые в ХМL и в DataSet (Набор данных).
Язык XML не навязывает принцип организации данных или суть документа XML. Он лишь определяет правила сопоставления документов. С другой стороны, схема XML описывает метаданные, т.е. способ организации данных внутри документа XML. Схемы XML пишутся на XML.
Например, сам по себе XML можно использовать для описания данных реляционной базы данных, а схема XML может использоваться для описания связей между данными, такими, как первичные или внешние ключи. Гораздо проще использовать схему XML и данные в одном документе или текстовом потоке, чем загружать каждую таблицу в набор данных, а затем программно устанавливать связи между таблицами.
Документы могут содержать в себе результат вычислений, полученный от базы данных. Например, отчет о продажах содержит, кроме данных о продажах, полученных от источника данных, и некоторые пояснения. Для представления данных в виде документа ХМL используется класс XmlDataDocument.
Класс XmlDataDocument является производным от класса XmlDocument, который представляет документы XML в библиотеке классов .Net Xml Framework. Особенно удобным делает класс XmlDataDocument то, что экземпляр этого класса можно получить из объекта DataSet (Набор данных) посредством простой передачи объекта Data-Set (Набор данных) конструктору класса XmlDataDocument в качестве аргумента. XmlDataDocument имеет свойство DataSet (Набор данных), так что вы можете работать с документом XML как с реляционными данными, если это имеет смысл.
Объект DataSet (Набор данных) содержит методы WriteXml и WriteXmlSchema, которые выдают данные и схему данных, хранящихся в наборе данных. Схема XML, возвращаемая объектом DataSet (Набор данных), определяется из самих данных. Пока вы не добавите явным образом в объект DataSet (Набор данных) ограничения, такие, как первичные или внешние ключи, они не будут частью схемы.
Объект DataSet (Набор данных) содержит также и методы, предназначенные для чтения XML: ReadXml и ReadXmlSchema. С помощью ReadXml можно считывать данные и схему в объект DataSet (Набор данных). Когда схема отсутствует, метод попытается извлечь ее изданных. Если же это не удастся, возникнет исключение. ReadXmlSchema считывает схему документа.
При отсутствии в документе XML схемы, DataSet (Набор данных) будет извлекать данные, как если бы они были таблицами, руководствуясь при этом набором правил. Оставшиеся элементы будут считаться столбцами таблиц
Для определения того, будет столбец записываться в документ XML как элемент или как атрибут, используется свойство Col-rr.Kapping объекта DataColamn. Запись столбца как элемента предпочтительней. Элемент, содержащий нескалярные данные, считается таблицей. Атрибуты и скалярные значения являются столбцами. Подробнее эти правила описаны в документации к .NET.
Для рассмотрения доступа к данным XML мы будем использовать базу данных Air-linesBrokers. Указанную базу данных можно создать и инициализировать с помощью SqlServer Query Manager и SQL-скрипта, прилагаемого к примерам для этой главы. База данных AirlinesBrokers имеет функциональные возможности, используемые системой бронирования Acme. С ее помощью клиенты Acme могут бронировать авиабилеты. Такая база данных содержит следующие таблицы:
- Airlines (Авиалинии): информация об авиалиниях;
- PlaneType (Тип самолета): типы самолетов, используемые на авиалиниях;
- Flights (Рейсы): информация о рейсах на различных авиалиниях:
- Customers (Клиенты): информация о клиентах;
- Reservations (Резервирование): информация о забронированных клиентами местах.
Хотя в реальной жизни списки клиентов систем AirlinesBroker и HotelBroker (Посредник, бронирующий места в гостинице) вряд ли могут совпасть, в нашем примере мы для простоты будем использовать те же таблицы Customers (Клиенты) и те же компоненты для доступа к данным, ч го и ранее.
Для иллюстрации связи между реляционной моделью объекта DataSet (Набор данных) и моделью ХМ L прежде всего извлечем некоторую информацию из базы данных. В примере DataSetXml для этого используются те же команды и способы, что и ранее в этой главе.
Первым делом создадим соединение, набор данных и преобразователь данных для различных таблиц.
SqlConnect]on *conn =
new SqlConnect K>n ( connectStr ing) ;
DatdSet *d - new Pdtr.Set ( "Ад r±ineBroker") ; // новый Набор данных SqlDjtaAd.-pter "air ] i -n^s Adaptcr =
new Sq]DataAoar te L ; SqiDctaAaapter * f 1 ^qn4- sAdapter -
new SqlLataAdapter; SqlDataAdapter ^plar.etypeAdapter =
new SqlDdtdAdapter; SqlDataAdapter *custon,ersAdaptei =
new SqlDataAaapter; SqlDataAdapter ^reservationsAaapter -
new SqlDataAaapter;
Затем создадим несколько команд select (выбрать) для получения данных, и, используя эти команды, заполним набор данных данными из таблиц.
airlinesAdapter->SelectCommand =
new SqlCommand("select * from Airlines", conn);
// "выбрать * из Авиалиний " airlinesAdapter->Fill(d, "Airlines");
// Заполнить (d, "Авиалинии");
flightsAdapter->SelectCommand =
new SqlCommand("select * from Flights", conn);
// "выбрать * из Рейсов" flightsAdapter->Fill(d, "Flights");
// Заполнить (d, "Рейсы");
planetypeAdapter->SelectCommand =
new SqlCommand("select * from PlaneType", conn);
// выбрать planetypeAdapter->Fill(d, "PlaneType");
// Заполнить
customersAdapter->SelectCommand =
new SqlCommand("select * from Customers", conn);
// "выбрать * из Клиентов " customersAdapter->Fill(d, "Customers");
// Заполнить (d, "Клиенты");
reservationsAdapter->SelectCommand =
new SqlCommand("select * from Reservations", conn);
// "выбрать * из Резервирования " reservationsAdapter->Fill(d, "Reservations");
// Заполнить (d, "Резервирование");
Теперь в объекте DataSet (Набор данных) есть данные таблиц Airlines (Авиалинии), PlaneType (Тип Самолета), Flights (Рейсы), Customers (Клиенты) и Reservations (Резервирование). Далее извлечем из данных объекта DataSet (Набор данных) схему XML. Затем извлечем сами данные и запишем их в формате XML.
d->WriteXmlSchema("Airlines.xsd");
d->WriteXml("Airlines.xml");
Приведенные операторы создают два файла: Airlines . xsd и Airlines . xml. Ниже вы видите некоторые данные, записанные в Airlines. xml. Главным элементом является AirlineBroker; именно так назывался объект DataSet (Набор данных). На один уровень ниже находятся элементы, соответствующие разным таблицам объекта DataSet (Набор данных): Airlines (Авиалинии), PlaneType (Тип самолета), Flights (Рейсы) и Customers (Клиенты). О забронированных местах информации в базе данных не было. В получившемся документе каждой строке исходных таблиц соответствует одна запись. Элементы этих записей соответствуют полям исходных таблиц.
<?xml version="l.О" standalone="yes"?>
<AirlineBroker> <Airlines>
<Name>America West</Name>
<Abbreviation>AW</Abbreviation>
<WebSite>www.americawest.com</WebSite>
<ReservationNumber>555-555-1212</ReservationNumber>
</Airlines> <Airlines>
<Name>Delta</Name>
<Abbreviation>DL</Abbreviation>
<WebSite>www.delta.com</WebSite>
<ReservationNumber>800-456-7890</ReservationNumber>
</Airlines>
<Flights>
<Airline>DIX/Airlj.nЈ>
<FlightNurrber>98^</FlightNumber>
<StartCity>Atlanta</StartCity>
<EndCity>New Orleans</EndCity>
<Departure>2001-10-05T20:15:00.0000000-04:00 </Departure>
<Amval>2001-10-05T22 : 30: 00. 0000000-04 : 00</Amval>
<PlaneType>737</?ianeType>
<FirstCost>1300</FirstCost>
<BusinessCost>0</BusinessCost>
<EconoiryCost>450</EconomyCost> </Flights>
<PlaneType>
<PlaneType> 737 </PlaneType>
<FirstClass>10</FirstClass>
<BusinessClass>0</BusinessClass>
<EconomyClass>200</EconomyClass> </PlaneType->
<Customers>
<LastName>Adams</LastName> <FirstName>John</FirstName>
<EmailAddress>adams@presidents.org</EmailAddress> <CustomerId>1<
/CustomerId> </Custorrers> </AirlineBroker>
Вот более русифицированная версия этого XML-документа:
<? xml версия = "1.0" автономный = "да"?> <AirlineBroker>
<Авиалинии>
<Название> Американский Запад </Название>
<Сокращение> AW </Сокращение>
<УзелИеЬ> www.americawest.com </УзелМеЬ>
<ReservationNumber> 555-5Ь5-1212 </ReservationNumber>
</Авиалинии> <Авиалинии>
<Название> Дельта </Название>
<Сокращение> DL </Сокращение>
<Узeлweb> www.delta.com </УзeлWeb>
<ReservationNumber> 800-456-7890 </ReservationNumber>
</Авиалинии> <Рейсы>
<Авиалиния> DL </Авиалиния> <FlightNumber> 987
</FlightNumber> <StartCity> Атланта </StartCity>
<EndCity> Новый Орлеан </EndCity>
<Вьшет> 2001-10-05Т20:15:00.0000000-04:00
</Вылет> <Прибытие> 2001-10-05Т22:30:00.0000000-04:00
</Прибытие>
<Тип самолета> 737 </Тип самолета> <F.L-(Cust> 1300
</FirstCost> <Bus _ncssCost> 0 </BusinessCost>
<EconcmyCost> 450 </EconomyCost> </Рейсь:>
<Тип самолета>
<Тип самолета>737</Тип самолета>
<FirstClass> 10 </FirstClass>
<BusinessClass> 0 </BusinessClass>
<EconomyClass> 200 </EconomyClass> </Тип самолета>
<Клиенты>
<LastName> Адаме </LastName> <FirstName> Джон </FirstName>
<EmailAddress> adams@presidents.org </EmailAddress>
<CustomerId> 1 </CustomerId> </Клиенты> </AirlineBroker>
Исходя из этих данных, объект DataSet (Набор данных) создал схему и сохранил ее в файле Airlines.xsd. Дальше мы обсудим некоторые отрывки из этого файла. В нем нет информации о связях или первичных ключах какой бы то ни было таблицы, такой, как Airlines (Авиалинии) или Flights (Рейсы), по той простой причине, что они не были определены в исходной базе данных. Если вы просмотрите созданный файл, вы увидите, что в нем записана и информация о схеме данных таблицы Reservations (Резервирование), несмотря на то, что в этой таблице нет никаких данных.
В первой строке заголовка схемы определено название схемы (AirlineBroker). Кроме того, в нем определены два пространства имен, используемых в этой схеме документа. Одно пространство имен, названное xsd, содержит описание стандарта схемы XML. Второе, названное msdata, содержит описание от Microsoft.
<xsd:schema id="AirlineBroker" targetNamespace="" xmlns="" xmlns:xsd=http://www.w3.org/2001/XMLSchema xmlns:rnsdata="urn:schemas-microsoft-com:xml-msdata">
В следующей строке описывается элемент под названием AirlineBroker, имеющий атрибут, указывающий, что эта схема получена из объекта DataSet (Набор данных). Это атрибут в определениях Microsoft, а не в пространстве имен W3C Schema. Элемент AirlineBroker относится к составному (не скалярному) типу, т.е. является структурой, состоящей из элементов других типов. Такая структура может содержать произвольное количество элементов (или не содержать ни одного) любого типа, определенного в остальной части схемы.
<xsd:element name="AirlineBroker" msdata:IsDataSet="true"> <xsd:complexType> <xsd:choice maxOccurs="unbounded">
Далее описывается элемент, определяющий тип данных. Этот тип — тоже структура и потому относится к составному (не скалярному) типу, очередность элементов в котором совпадает с очередностью их определения в объекте DataSet (Набор данных). Так уж получилось, что все элементы, соответствующие столбцам таблицы базы данных, определены здесь как имеющие строковый тип string, причем их напичие не считается обязательным. В исходной таблице первичные ключи не определялись, а так как все эти строки в записях базы данных обязательны, объект DataSet (Набор данных) при преобразовании данных вывел и\ из набора таблиц, ограничений и связей, определенных в объекте DataSet (Набор данных) в момент преобразования.
<xsd : element r.ame = "Airlines"> <xsd:conplexType>
<xsd:sequence>
<xsd: element narre = "Kame" type="xsd:string"
minOccurs="0" /> <xsd: element narr:e="Abbreviation"
type="xsd:string" minOccurs="0" /> <xsd:element name="KebSite" rype="xsd:string"
minOccurs="0" /> <xsd:element name="ReservationNumber"
type="xsd:string" minOccurs="0" /> </xsd:sequence>
</xsd:complexType> </xsd:element>
Таблица Flights (Рейсы) определена аналогичным образом. Кроме того, что в ней не определен первичный ключ, в ней нет и внешних ключей для столбцов Airline (Авиалиния) и Plane Type (Тип самолета).
<xsd: element na.me = "Fliqhts"> <xsd:complexType> <xsd:sequence>
<xsd:element name="Airline" type="xsd:string"
minOccurs="0" /> <xsd:element name="FlightNumber" type="xsd:int"
rainOccurs="0" /> <xsd:element name="StartCity" type="xsd:string"
minOccurs="0" /> <xsd:element name="EndCity" type="xsd:string"
minOccurs="C" /> <xsd:element name="Derarture" type="xsd:dateTime"
minOccurs="0" />
<xsd:element name="Arrival" type="xsd:dateTime"
minOccurs="0" />
<xsd:element name="PlaneType" type="xsd:string"
minOccurs="0" />
<xsd:element name="FirstCost" type="xsd:decimal"
minOccurs="0" /> <xsd:element name="BusinessCost"
type="xsd:decimal" minOccurs="0" /> <xsd:element name="EconomyCost"
type="xsa:decimal" minOccurs="0" /> </xsd:sequence>
</xsd: ccm,plexType> </xsd:element>
</xsd:choice>
</xsd:complexType> </xsd:element> </xsd:schema>
К этому определению схемы данных мы еще вернемся позже, а сейчас продолжим рассмотрение примера.
Используя объект DataSet (Набор данных) можно создать новый документ XML. Используя запрос XPath, можно перейти в начало документа, а затем, с помощью объекта XmlNodeReader прочитать весь документ. Мы выведем содержимое документа на экран. Класс XmlNodeReader обеспечивает перемещение по документу. Приведем фрагмент кода из примера DataSetXML:
XmlDataDocument *xmlDataDoc = new XmlDataDocument(d);
XmlNodeReader *xmlNodeReader = 0;
try
{
XmlNode *node = xmlDataDoc->SelectSingleNode("/");
XmlNodeReader = new XmlNodeReader (node);
FormatXml (XmlNodeReader); }
catch (Exception *e) // Исключение {
Console::WriteLine (
"Exception: {0}", e->ToString()); // Исключение
}
finally // наконец
r
if (XmlNodeReader != 0) // если (XmlNodeReader! = 0)
xmlNodeReader->Close(); }
static void FormatXml (XmlReader *reader) {
while (reader->Read()) // читатель-> Чтение () {
switch (reader->NodeType) // переключатель
//(читатель-> NodeType) {
case XmlNodeType::Element: // случай
// XmlNodeType::Элемент Format (reader, "Element");
// Формат (читатель, "Элемент"); while(reader->MoveToNextAttribute() )
//Format (reader, "Attribute"); // Формат (читатель, "Атрибут"); break;
case XmlNodeType::Text: // случай XmlNodeType:: Текст Format (reader, "Text");
// Формат (читатель, "Текст"); break;
static String *lastNodeType = ""; // статическая Строка
static void Format(XmlReader *reader, String *nodeType) // Формат
{
if (nodeType->Equals("Element"))
// если (nodeType-> Равняется ("Элемент"))
{
if (lastNodeType->Equals("Element"))
// если (lastNodeType-> Равняется ("Элемент"))
{
Console::WriteLine();
}
for (int i=0; i < reader->Depth; i++)
{
Console::Write(" "); // Запись
}
Console::Write(reader->Name) ;
// Запись:: (читатель-> Название); }
else if (nodeType->Equals("Text"))
// если (nodeType-> Равняется ("Текст"))
//Console::WriteLine("={0}", reader->Value);
// Значение else
{
Console::Write(String::Format(
// Запись:: (Строка:: Формат (
//"{0}<{1}>{2}", nodeType, reader->Name,
// Название reader->Value));
// читатель-> Значение Console::WriteLine (); }
lastNodeType = nodeType; }
Вот какой документ XML будет записан объектом DataSet (Набор данных) в файл:
AirlineBroker
Airlines
Name=America West
Abbreviation=AW
WebSite=www.americawest.com
ReservationNumber=555-555-1212
Airlines
Name=Northwest
Abbreviation=NW
WebSite=www.northwest.com
ReservationNumber=888-111-2222
Airlines
Name=Piedmont
Abbreviation=P
WebSite=www.piedmont.com
ReservationNumber=888-222-333
Airlines
Name=Southwest
Abbreviation's
WebSite=www.southwest.com
ReservationNumber=l-800-111-222
Airlines
Name=Unitea
Abbreviation=UAL
WebSite=www.ual.com
ReservationNumber=800-123-4568
Flights <'-- Рейсъ. -->
Airline=DL
FlightNumber=987
StartCity=Atlanta
EndCity=New Orleans
Departure=2001-10-05T2G:15:СС.ООСГПСО-04:00
Arnval=2001-10-05T22:30:ОС.ОЭООССО-04:СО
PlaneType=737
FirstCost=1300
BusinessCost=0
EconomyCost=450
Flights
Airline=UAL
FlightNumber=54
StartCity=Boston
EndCity=Los Angeles
Departure=2001-10-01T10:00:OO.OOCOOOO-r4:00
Arriva1=2001-10-01T13:00:00.0000000-04:00
PlaneType=767
FirstCost=1500
BusinessCost=1000
EconomyCost=300
PlaneType
PlaneType=737
FirstClass=10
BusinessCldss=0
EconomyСlass=200
PlaneType
PlaneType=767
FirstClass=10
BusinessClass=30
EconomyCiass=300
Customers !'-- 1лкеггы -->
LastName=Adams
FirstName=John
EmailAddress=adans@presidents.erg
Customerld
ADO.NET содержит классы, позвопяющие создавать и использовать распределенные данные Вы можете работать с базами данных в соединенном или отсоединенном режимах, в зависимости от потребностей Объект Dt,L_Set (Набор данных) дает возможность работать с данными реляционным способом даже при отсутствии соединения с каким-либо источником данных Для модетирования реляционных данных можно использовать документы XML, содержащие информацию в нереляционном виде Типизированный DataSet (Набор данных) облегчает работу, обеспечивая определение схемы XML для данных