WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 || 3 | 4 |   ...   | 6 |

«том1 а л ь м а н а х программиста Тематический сборник материалов MSDN» Library и MSDN» Magazine Microsoft ADO.NET ...»

-- [ Страница 2 ] --

Руководство по архитектуре доступа к данным на платформе.NET try { conn.OpenO;

SqlDataReader reader = cmd.ExecuteReaderf);

reader. ReadO;

// читаем единственную запись // Возвращаем выходные параметры из полученного потока данных ProductName = reader. GetString(O);

UnitPrice = reader. GetDecimal(l);

reader. Closef);

} catch { throw;

} finally { conn.CloseO;

Для чтения одной записи с помощью объекта SqlDataReader выполните следующие действия.

1. Создайте объект SqlCommand.

2. Откройте соединение.

3. Вызовите метод ExecuteReader объекта SqlCommand, чтобы получить объект SqlDataReader.

4. Считайте выходные параметры типизированными методами-аксессора ми объекта SqlDataReader (в данном случае — GetString и GetDecimal).

В предыдущем фрагменте кода вызывается следующая хранимая процедура, CREATE PROCEDURE DATGetProductDetailsReader &ProductID int AS SELECT ProductName, UnitPrice FROM Products WHERE ProductlD = @ProductID GO Использование ExecuteScalar для чтения одного поля Метод ExecuteScalar предназначен для запросов, возвращающих только одно значение. Если запрос возвращает несколько полей и/или записей, ExecuteScalar вернет для этого запроса лишь первое поле первой записи.

В следующем фрагменте кода показывается, как найти наименование про дукта по идентификатору.

72 Microsoft ADG.NET void GetProductNameExecuteScalar{ int ProductID, out string ProductName ) { SqlConnection conn = new SqlConnection( "server=(local);

Integrated Security=SSPI;

database=northwind");

SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn );

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add("@ProductID", ProductID );

try { conn.0pen();

ProductName = (string)cind. ExecuteScalar();

} catch { throw;

} finally { conn.Close();

} ;

Чтобы считать одно поле через ExecuteScalar, выполните следующие действия.

1. Создайте объект SqlCommand для вызова хранимой процедуры.

2. Откройте соединение.

3. Вызовите метод ExecuteScalar. Обратите внимание, что он возвращает значение первого считанного поля как объектный тип, и вы должны привести его к нужному типу.

4. Закройте соединение.

В приведенном выше фрагменте кода используется эта хранимая процедура.

CREATE PROCEDURE LookupProductNameScalar ©ProductID int AS SELECT TOP 1 ProductName FROM Products WHERE ProductID = tProductID GO Руководство по архитектуре доступа к данным на платформе.NET Использование возвращаемого значения или выходного параметра хранимой процедуры для чтения одного поля Вы можете получить единственное значение через возвращаемое значение или выходной параметр хранимой процедуры. В следующем коде демон стрируется вариант с выходным параметром.

void GetProductNaraeUsingSPOutpuU int ProductID, out string ProductName ) { SqlConnection conn = new SqlConnection( "server=(local);

Integrated Security=SSPI;

database=northwind");

SqlCommand cmd = new SqlCommandC'LookupProductNameSPOutput", conn );

cmd.CommandType = CommandType.StoredProcedure;

SqlParameter paramProdID = cmd.Parameters.Add("§ProductID", ProductID );

ParamProdlD.Direction = ParameterDirection.Input;

SqlParameter paramPN = cind.Parameters.Add("@>ProductName", SqlObType.VarChar, 40 );

paramPN.Direction = ParameterDirectlon.Output;

try I conn.OpenO;

cmd.ExecuteNonQue ry();

ProductName = paramPN. Value. ToStringO;

} catch { throw;

} finally { conn.Close();

} I Для чтения единственного значения из выходного параметра хранимой процедуры выполните следующее.

1. Создайте объект SqlCommand для вызова хранимой процедуры.

2. Задайте все входные параметры и единственный выходной параметр, до бавляя объекты SqlParameter в набор Parameters объекта SqlCommand.

3. Откройте соединение.

4. Вызовите метод ExecuteNonQuery объекта SqlCommand.

5. Закройте соединение.

6. Извлеките выходное значение из свойства Value объекта SqlParameter.

74 Microsoft ADO.NET В приведенном выше фрагменте кода используется следующая хранимая процедура.

CREATE PROCEDURE LookupProductNameSPOutput SProductlD int, @ProductName nvarchar(40) OUTPUT AS SELECT @ProductName = ProductName FROM Products WHERE ProductID = @ProductID GO Следующий код иллюстрирует, как воспользоваться возвращаемым значе нием хранимой процедуры, чтобы проверить наличие определенной запи си. С точки зрения программирования, это аналогично применению вы ходных параметров — с тем исключением, что вы должны явно присвоить объекту SqlParameter значение ParameterDirection. Return Value.

bool CheckProduct( int ProductID ) ( SqlConnectIon conn = new SqlConnection{ "server={local);

Integrated Security=SSPI;

database=northwind");

SqlCommand cmd = new SqlCommandC'CheckProductSP", conn );

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add("@ProductID", ProductID );

SqlParameter paramRet = cmd.Parameters.Add("@ProductExists", SqlDbType.Int );

paramRet.Direction = ParameterDirection.ReturnValue;

try { conn.0pen();

cmd.ExecuteNonQuery();

\ catch { throw;

} finally conn.CloseO;

} return (int)paramRet.Value == 1;

J Чтобы проверить наличие определенной записи, выполните следующие действия.

1. Создайте объект SqlCommand для вызова хранимой процедуры.

Руководство по архитектуре доступа к данным на платформе,NET 2. Задайте входной параметр, содержащий значение первичного ключа той записи, наличие которой вы хотите проверить.

3. Задайте единственный параметр для возвращаемого значения. Добавь те объект SqlParameter в набор Parameters объекта SQLCommand и ус тановите его направление как ParameterDirection.RetumValue.

4. Откройте соединение.

5. Вызовите метод ExecuteNonQuery объекта SqlCommand.

6. Закройте соединение.

7. Получите возвращаемое значение из свойства Value объекта SqlParameter.

В показанном выше фрагменте кода используется следующая хранимая процедура.

CREATE PROCEDURE CheckProductSP isProductlD int AS IF EXISTSC SELECT ProductID FROM Products WHERE ProductID = @ProductID ) return ELSE return GO Использование SqIDataReader для чтения одного поля Объект SqIDataReader позволяет получать единственное выходное значе ние методом ExecuteReader объекта команды. Это требует чуть больше кодирования, чем в предыдущих случаях, поскольку вы должны вызвать метод Read объекта SqIDataReader, а затем считать требуемое значение с помощью одного из методов-аксессоров класса чтения. Использование объекта SqIDataReader демонстрирует следующий код, bool CheckProductWithReaderC int ProductID ) { SqlConnection conn = new SqlConnection( "server=(local);

Integrated Security=SSPI;

database=northwind");

SqlCominand cmd = new SqlCommand("CheckProductExistsWithCount", conn );

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add("©ProductID", ProductID };

cmd. Parameters["©ProductID"]. Direction = ParameterDirection.Input;

try ( conn.OpenO;

SqIDataReader reader = cmd.ExecuteReader( CommandBehavior.Singleflesult );

Microsoft ADO.NET reader.Read();

bool bRecordExists = reader.Getlnt32{0) > 0;

reader.Closet);

return bRecordExists;

catch throw;

finally conn.Close();

В приведенном выше фрагменте кода используется следующая хранимая процедура.

CREATE PROCEDURE CheckProductExistsWitnCount @ProductID int AS SELECT COUNT(*} FROM Products WHERE ProductID = eProductID GO Программирование ручных транзакций ADO.NET Ниже показано, как использовать поддержку транзакций в SQL Server.NET Data Provider, чтобы с помощью транзакции защитить перевод де нежных средств. Перевод средств выполняется между двумя счетами, на ходящимися в одной базе данных.

public void TransferMoney( string toAccount, string fromAccount, decimal amount ) { using ( SqlConnection conn = new SqlConnection( "server=(local);

Integrated Security=SSPI;

database=SimpleBank" ) ) { SqlCommand cmdCredit = new SqlCommand("Credit", conn );

cmdCredit.CommandType = CommandType.StoredProcedure;

cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) );

cmdCredit.Parameters.Add( new SqlParameter("©Amount", amount ));

SqlCommand cmdDebit = new SqlCommand("Debit", conn );

cmdOebit.CommandType = CommandType.StoredProcedure;

cmdDebit.Parameters.Add{ new SqlParameter("@AccountNo", fromAccount) );

cindDebit.Parameters.Add( new SqlParameter("@Amount", amount ));

conn.0pen();

// Начинаем новую транзакцию Руководство по архитектуре доступа к данным на платформе.NET using ( SqlTransaction trans = conn.BeginTransaction() ) { // Связываем с этой транзакцией два объекта команды omdCredit. Transaction = trans;

cmdDebit. Transaction = trans;

try { cmdCredit. ExecuteNonQueryt ) ;

cmdDebit. ExecuteNonQueryt ) ;

// Обе команды (для дебета и кредита) выполнены успешно trans. CommitO;

} catch( Exception ex ) { // Транзакция потерпела неудачу trans. RollbackQ;

// Протоколируем параметры исключении...

throw ex;

I Выполнение транзакций с помощью Transact-SQL Вот как выполнить транзакционную операцию по переводу денег с ис пользованием хранимой процедуры, написанной на Transact-SQL.

CREATE PROCEDURE MoneyTransfer @FromAccount char(20), taToAccount char(20), ^Amount money AS BEGIN TRANSACTION - ВЫПОЛНЯЕМ ОПЕРАЦИЮ ПО ДЕБЕТУ UPDATE Accounts SET Balance = Balance - ^Amount WHERE AccountNurober = ©FromAccount IF №RowCount = BEGIN RAISERRORC 'Invalid From Account Number', 11, 1) GOTO ABORT END DECLARE ^Balance money SELECT ^Balance = Balance FROM ACCOUNTS WHERE AccountNumber = @FromAccount IF ^BALANCE < BEGIN RAISERRORC Insufficient funds', 11, 1) GOTO ABORT Microsoft ADO.MET END - ВЫПОЛНЯЕМ ОПЕРАЦИЮ ПО КРЕДИТУ UPDATE Accounts SET Balance = Balance + ©Amount WHERE AccountNumber = @ToAccount IF @@RowCount = BEGIN RAISERROHCInvalid To Account Number', 11, 1) GOTO ABORT END COMMIT TRANSACTION RETURN ABORT:

ROLLBACK TRANSACTION GO В этой хранимой процедуре для управления транзакцией вручную исполь зуются операторы BEGIN TRANSACTION, COMMIT TRANSACTION и ROLLBACK TRANSACTION.

Программирование транзакционного.NET-класса В следующем примере показано три управляемых обслуживаемых.NET класса, сконфигурированных для автоматических транзакций. Каждый класс помечен атрибутом Transaction, значение которого определяет, сле дует ли создавать новый поток транзакции или объект должен участвовать в потоке транзакции вызывающего объекта. Операция по переводу денег выполняется при взаимодействии этих компонентов. Класс Transfer поме чен атрибутом транзакции RequiresNew, а классы Debit и Credit — атрибу том Required, В итоге при выполнении все три объекта участвуют в одной и той же транзакции.

using System;

using System.EnterpriseServices;

[Transaction(TransactionOption.RequiresNew}] public class Transfer : ServicedComponent < [AutoComplete] public void Transfer^ string toAccount, string fromAccount, decimal amount } { try // Выполняем операцию по дебету Debit debit = new Debit();

debit.DebitAccount( fromAccount, amount );

// Выполняем операцию по кредиту Руководство по архитектуре доступа к данным на платформе.NET Credit credit = new CreditO;

credit. CreditAccount( toAccount, amount );

} catch( SqlException sqlex ) { // Обрабатываем исключение и протоколируем его параметры.

// Помещаем исключение в оболочку и передаем дальше.

throw new TransferException( "Transfer Failure", sqlex );

[Transact ion(TransactionOption. Required)] public class Credit : ServicedComponent { [AutoComplete] public void CreditAccount( string account, decimal amount ) { SqlConnection conn = new SqlConnection( "Serve r=( local);

Integrated Security=SSPI";

database="SimpleBank");

SqlComtnand cmd = new SqlCommand("Credit", conn );

cmd.CommandType = CommandType.StoredProcedure;

cmd. Parameters. Add{ new SqlParameter("@AccountNo", account) );

cmd. Parameters. Add( new SqlParameter("@Amount", amount ));

try ( conn.0pen();

cmd. ExecuteNonQuery( ) ;

} catch (SqlException sqlex) { // Протоколируем параметры исключения throw;

// передави исключение дальше I [Т ransaction(TransactionOption. Required)] public class Debit : ServicedComponent { public void DebitAccount( string account, decimal amount ) { SqlConnectton conn = new SqlConnection( "Server=(local);

Integrated Security=SSPI";

database="SimpleBank");

SqlCommand cmd = new SqlCommandC'Debit", conn );

cmd.CommandType = CommandType.StoredProcedure;

cmd. Parameters, Add( new SqlParameter("@AccountNo", account) );

cmd,Parameters.Add( new SqlParameter("$Amount", amount );

) try { conn.0pen();

80 Microsoft ADO.NET cmd. ExecuteNonQueryO;

catch (SqlException sqlex) // Протоколируем параметры исключения throw;

// передаем исключение обратно вызывающему Благодарности Выражаем признательность всем, кто участвовал в подготовке этой статьи:

Bill Vaughn, Mike Pizzo, Doug Rothaus, Kevin White, Blaine Dokter, David Schleifer, Graeme Malcolm (Content Master), Bernard Chen (Sapient), Matt Drucker (Turner Broadcasting) и Steve Kirk.

Вопросы? Замечания? Предложения? С авторами статьи можно связаться (на английском языке) по адресу devfdbck@microsoft.com.

Хотите освоить платформу.NET и задействовать всю ее мощь? Для обме на опытом лучше всего поработать бок о бок с экспертами из технологи ческих центров Microsoft. Дополнительную информацию можно получить по ссылке http://www.microsoft.com/business/services/mtc.asp.

Джонни Папа ADO.NET: концепции и реализация В этой статье подробно рассматривается концепция отделения доступа к данным через ADO.NET от бизнес-логики и показывается, как добиться этого на практике. Автор реализует компонент, абстрагирующий доступ к данным, на двух.NET-языках: С# и Visual Basic.NET.

С появлением Microsoft.NET разработчики готовятся воспользоваться такими достоинствами этой платформы, как улучшенные возможности доступа к данным. Хотя и СОМ, и Visual Basic 6.0 будут широко приме няться еще долгие годы, уже сейчас заметен большой интерес к переходу на.NET-компоненты. Чаше всего меня спрашивают, чем отличается раз работка с использованием ADO.NET от ADO 2л и как выделить логику доступа к данным через ADO в отдельный компонент. Эти вопросы мы и рассмотрим.

Архитектура ADO.NET предоставляет богатые возможности манипулиро вания данными в программах на С#, Visual Basic.NET и других.NET-co вместимых языках. В предыдущей статье из этой рубрики я продемонст рировал методику инкапсуляции компонента доступа к данным с исполь зованием ADO 1.x и Visual Basic 6.0. Разумеется, концепция отделения доступа к данным от бизнес-логики относится не только к Visual Basic 6.0.

Аналогичные подходы применяются при разработке для.NET, и в этой статье мы начнем разбираться с ADO.NET и ее влиянием на программи Публиковалогь в MSDN Magazine/Русская Редакция. 2002. Спецвыпуск №1 (янпарь март). — Прим. изд.

Microsoft ADO.NET рование под.NET. Я покажу, как создать сервис для доступа к данным че рез ADO.NET, отделенный от бизнес-логики, реализуемой посредством ASP.NET.

В примерах, которые мы рассмотрим, три основных составляющих:

• Web-форма, содержащая серверные элементы управления, такие как asp:DataGrid, для отображения данных;

• CodeBehind-класс, получающий данные от сервиса доступа к данным и заполняющий ими элементы на Web-форме;

• исходный код, в котором определяется собственное пространство имен и классы, реализующие сервис доступа к данным.

Сейчас много спорят, какой.NET-язык использовать. Хотя.NET спроек тирована так, что ни один конкретный язык не имеет существенных пре имуществ перед другими, в каждом отдельном случае неплохо определить ся с этим вопросом. Между тем, пока дискуссии насчет языков продолжа ются, я приведу примеры как на Visual Basic, так и на С#. При этом не забывайте, что сборки, созданные с использованием этих языков, могут взаимодействовать друг с другом. Иначе говоря, можно создать сборку на С# и обращаться к ней из класса, написанного на Visual Basic.NET.

Web-форма Начнем с простой Web-формы, представляющей пользовательский интер фейс (UI) приложения. Код формы WebForm.aspx содержит два серверных элемента управления DataGrid, которые будут заполнены данными из двух разных ADO.NET-объектов DataSet (рис. 1). DataGrid позволяет представлять набор данных как HTML-таблицу. (О DataGrid подробно рассказывает Дино Эспозито в рубрике «Cutting Edge» в апрельском, май ском и июньском номерах «MSDN Magazine? за 2001 г.) На моей Web форме два элемента DataGrid: grdSql (заполняется SQL-оператором с ис пользованием моего сервиса доступа к данным) и grdProc (заполняется хранимой процедурой с применением того же сервиса).

Заметьте: Web-форма на рис. 1 содержит только директивы, HTML и сер верные элементы управления. Собственно кода здесь нет. В.NET Frame work код, реализующий Ш, можно отделить от самого интерфейса. Иначе говоря, весь HTML и серверные элементы управления могут быть внутри Web-формы, а весь код, взаимодействующий с UI, может храниться в от дельном файле класса. Это альтернатива стандартному подходу в ASP, где код и интерфейс смешаны, так что управлять проектом теперь гораздо проще.

ADQ.NET: концепции и реализация Рис. 1. Web-форма из Visual Basic a <Х@ Page Language "vtr Codebehind="WebForra.aspx.vb" a Innerlts "HyV80ataLayer.WebForerx> Example of Executing a SQL Statement and Filling a Grid

xampla of Executing a Stored Procedure and Filling a Grid Привязка Web-формы к коду сводится к тому, что вы указываете, где на ходится файл кода (его также называют CodeBehind-файлом). (Подробнее о концепции CodeBehind см. http://msdn.microsoft.com/msdnmag/issues/ 01/08/cuttmg/cutting0108.asp.) Директива @Page имеет атрибут CodcBe-.

hind (рис, 1). С ее помощью Web-форма связывается с исходным кодом.

Директива @Page также имеет атрибут Inherits, указывающий простран ство имен и класс, которые наследует Web-форма.

Ctf-версия этой страницы незначительно отличается от эквивалента на Visual Basic — фактически только первыми строками. Например, в атри буте Language указан С#, а не Visual Basic. Атрибут CodeBehind указыва ет на файл WebForm.aspx.cs, a Inherits — на класс My С SDataLayer. Web Form. Столь незначительные отличия — следствие того, что весь реальный код для этих страниц находится в файлах кода.

Рис. 2. Файл кода Web-формы «a Visuaf Basic Imports System Imports System. Data Imports System. Wet. Imports System. Web. UI.WebGontrols Public Class WebForm Inherits System. Web. UI. Page '// ОБЪЯВЛЕНИЯ ПЕРЕЛЕННЫХ '//- DataGrid для вывода данных, полученных от SQL-запроса Protected grdSql As DataGrid '//- DataQrid для вывода данных, полученных от хранимой процедуры Protected grdProc As DataGrid 'ft Конструктор ' / / _„_.„, Public WebForntO Private Sub Page_Load(8yVal sender As System. Object, ByVal e As System. EventArgs) Handles MyBase.Load ' Здесь должен быть ваш код, инициализирующий страницу Dim oDs As DataSet = Nothing Difli oSql As SqIService = Nothing. Dim sSql As String = "" Dim sProcName As String = "" V/- Создание экземпляра SqIService со строкой подключения •• oSql = New MyVBDataLayer. SqlService("localhost", "northwind "sa", "") '//- Определение SQL-оператора sSql * "SELECT CategorylD, CategoryName, Description F80H " "Categories ORDER 8Y CategoryName" '//- Выполнение SQL-оператора через объект SQLService '// и получение DataSet см. след. стр.

&DO.NET: концепции и реализация РИС. 2. Файл кода Web-формы на Visual Basic (окончание) oDs ~ QSql.Run5ql{sSql, "Category") '//- Заполнение DataGrid данными из DataSet {jrdSql.DataSoorce = New DataVlew(oBs.Tables("Category")> grdSql.DataBiRdO '//- Уничтожение DataSet oDs = Nothing *,//- Определение хранимой процедуры и ее параметров sProcName = "SalesByCategory" eSqlrAddParaffleterC"®CategoryName",ssenumSqlDataTypes.

ssSOT_String, 15, "Produce") QE^l.AddParameter("QrdYear",sseruitfiSqlDataTyp&s. _ ssSDT_Strifi&, 4, "1998") '//- Выполнение хранимой процедуры через объект SQLService V/ и получение OataSet oDs * oSql.RunProc

Затем объявляются два DataGrid из того же пространства имен, что и эле 86 Microsoft ADO.NET менты управления в файле с UI, соответствующем данному файлу кода.

Это дает возможность взаимодействовать с элементами управления Data Grid из файла кода. Переменные объявлены как Protected, а значит, дос тупны только из самого класса или его потомков. Затем вы должны объя вить конструктор для WebForm, в который можно поместить код, выпол няемый при создании экземпляра формы.

Определив основные части CodeBehind, рассмотрим код обработчика ос новного события Page_Load (рис. 2). Здесь я объявляю объект DataSet — он будет содержать набор результатов (result set), получаемый после каж дого обращения к моему компоненту. Также заметьте, что эта процедура обрабатывает событие My Base. Load. Иначе говоря, событие Page_Load замещает событие Load для этой страницы.

Я объявляю свой компонент доступа к данным (MyVBDataLayer.Sq]Ser vice) как переменную oSQL. Затем создаю экземпляр класса SqlService, используя конструктор с четырьмя аргументами, которым присваиваются значения, нужные для соединения. Конструктор определяет свойство строки подключения объекта SqlClient.Connection, содержащегося в клас се SqlService.

Далее я задаю SQL-оператор и передаю его методу RunSql, выполняющий этот SQL-оператор и генерирующий DataSet, который присваивается пе ременной oDs. Свойству DataSource данного DataGrid (grdSql) присваи вается Data View по умолчанию (таблицы Category в DataSet). Указав таким образом элементу DataGrid, откуда брать данные, я вызываю его ме тод DataBind. В итоге данные напрямую привязываются к первому эле менту DataGrid на странице WebForm.aspx.

Одна из основных причин создания компонента (сервиса) доступа к дан ным — упрощение выборки данных. Как видите, этого можно добиться, написав лишь несколько строк кода. Отказавшись от использования на странице объектов из пространства имен SqlClient, я могу инкапсулиро вать специфические параметры в собственных методах. Так, вам нужен единственный метод, выполняющий любой SQL-оператор независимо от того, возвращает ли он набор результатов. Перегружая метод, вы можете добиться этого в своем классе SqlService. Я использую лишь два объекта из System.Data: DataSet и Data View, Следовательно, мой компонент дос тупа к данным абстрагирует все объекты пространства имен SqlClient.

Можно нойти дальше: получать XML из набора результатов и на его осно ве заполнять сетку (DataGrid). При этом полностью отпадает надобность в ссылке на пространство имен System.Data в CodeBehind, ADO.NET: концепции и реализация Первый DataGrid заполняется результатами выполнения SQL-запроса, а второй — хранимой процедуры. Я передаю параметры процедуры своему компоненту через его метод Add Parameter (рис. 2). Имя параметра, его тип данных, длина и значение передаются конструктору метода AddParameter.

Тип данных параметра я идентифицирую с помощью своего перечислимо го, определенного в пространстве имен SqlService. Затем вызываю метод RunProc, запускающий хранимую процедуру и возвращающий DataSet.

Посмотрите на аналогичный код на С#. На пространства имен я ссылаюсь через ключевое слово using. Далее определяю используемое в проекте про странство имен. Еще одно отличие — в способе объявления класса Web Form. В С# для наследования одного класса от другого вы указываете ба зовый класс после определения нового класса, разделяя их двоеточием (рис. 3).

Рис. 3. Файл кода Web-Формы на С# // Пространства имен using System;

using System. Data;

using System. Web. Ul;

using System. Web. UI. WebContt ol&;

namespace MyCSDataLayer i /—«„„»„„ _ _ // имя файла;

WebForm.aSpx.C // Автор;

Lancelot Web Solutions, LLC // Дата: 07/04/ / / _ // Назначение: данный класс содержит код для ASP.NET-етраницы // WebForts.aspx. При загрузке страницы заполняются два DataGrid // с использованием собственной сборки MyCSDataLayer. Первый // DataGrld заполняется из набора результатов, полученного от // SQL-оператора. Второй - из набора результатов, полученного // от хранимой процедуры.

//_—.«.-.—-._. _„_ public class WebForm : System. Web. UI. Page / „..._ _ _. ».. _ _ „„„_— //»._„-. _ — // Объявлений защищенных переменных с.ч. след. стр.

Microsoft ADO.NET Рис. 3. Файл кода Web-d>opMw;

«a С# „ _„„„ /{ //- DataGrld для вывода данных от SQL-запроса protected DataSrid grdSql;

//- DataGrid для вывода данных от хранимой процедуры protected DataGrid grdProc;

it //_ _„„„ // Конструктор //_ „_-.„„„„.-._ protected WebFoneO i Page,Init += new System.EventHandler(Paffe_lnit);

// Этот ков выполняется при загрузке страницы //—...., _ — «.„.

private void Page_Load(object sender, System, EventArgs e) II DataSet для хранения данных, DataSet oDs = null;

// отображаемых DataGrid'ами SqlService oSql = null;

// экземпляр класса SqlService string sSql = "";

// SQL-оператор для заполнения PataSet string sProcName - "";

// хранимая процедура для заполнения DataSet //- Создание экземпляра SqlService со строкой подключения oSql = new SqlService<"localhost", "northwinct", "sa", " ) ";

//- Определение SQL-оператора sSql = "SELECT CategorylD, CategoryName, Description " sSql += " FROM Categories ORDER BY CategoryName";

//- Выполнение SQL-оператора через объект SQLService // и получение DataSet oDs = oSql.8unSql(sSql, "Category"};

//- Заполнение DataGrid данными из DataSet grdSql.DataSource * new SataView(oOs.Tables["Category"]};

grdSql.DataBindC);

//- Уничтожение DataSet oDs=null;

//- Определение хранимой процедуры и ее параметров sProcName = "SalesByCategory";

oSql.AddParaiieter<"@CategoryNaffle", см. след. стр.

ADO.NET: концепций и реализация Рис. 3- Файл кода Web-формы на С# (окончание) MyCSOataUyer.ssenumSqlDataTypes.ssSDT.String, 15, /'Produce");

oSql.AddPara[neter("@QrdYear", MyGSDataLayer.ssenumSqlDataTypes.ssSDT_String, 4, "1998");

//- Выполнение хранимой процедуры через объект SQLService // и получение DataSet oDs * QSql.flunProe(sProcNaffle, "Sales");

//- Заполнение OataGrid данными из DataSet grdProc.DataSouree = new DataView(oDs.Tables["Sales"});

grdProc.DataBindO:

//- Уничтожение DataSet oDs=nyll;

//- Уничтожение объекта SQiServlce oSql=null;

Прочих отличий ни так много, и все они следуют из различий в синтакси се: точки с запятой в конце операторов, присвоение null (вместо Nothing) при уничтожении объекта и указание типа перед именем в объявлениях переменных (рис. 3). Не забывайте, что С# чувствителен к регистру букв — здесь проще всего сделать ошибку, особенно если вы привыкли к Visual Basic.

Класс SqIService Класс SqIService является компонентом доступа к данным, определяя все его методы, свойства и перечислимые. В начале класса я указываю все используемые пространства имен:

Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Data.SqlTypes Imports System.Collections Затем определяю перечислимое, нужное вызывающему коду (в данном случае CodeBehind). Перечислимое указывает различные типы данных, которые можно передавать методу AddParameter. Так я избавляюсь от не обходимости использовать в вызывающем коде ADO.NET-специфичные пространства имен для SqlDataTypes.

90 Microsoft ADO.NET Public Enum ssenumSqLDataTypes ssSDT_Bit ssSDT_DateTime ssSDT_Decimal ssSDT_Integer ssSDT_Honey ssSDT_String End Enum Следующий шаг — собственно определение класса SqlService (рис. 4).

Здесь объявлены переменные уровня класса для хранения значений, нуж ных для соединения с источником данных. Я также определяю объект ArrayList для хранения параметров, передаваемых методу AddParameter при использовании хранимой процедуры.

Создав разные конструкторы, я предоставляю вызывающей программе разные варианты создания экземпляра класса SqlService. Чтобы опреде лить несколько конструкторов, я просто указываю для каждого из них раз личное число аргументов и/или типов аргументов.

Класс SqlService Рис. 4.

Public Class SqlService • //_ _„-„_ _. _ '// Объявления переменных уровни класса '/ / Private m_sUsername As String * " '// имя пользователи Private m_sP3Ssword As String - // пароль Private ra_sServer As String = "" '// экземпляр SQL Server Private ift_sDatabase As String = "" '// имя базы данных Private m^sConnectionStfing As String - "" "// строка подключения '//- Массив для хранений параметров хранимой процедуры Private ffl_oParmList As ArrayList ~ New ArrayListO Всего для класса SqlService определено три конструктора. Первый не име ет аргументов и поэтому лишь создает экземпляр класса, не настраивая его свойства. Второй использует как единственный аргумент строку подклю чения к базе данных. Если вы передадите конструктору SqlService один аргумент, его код присвоит значение аргумента строке подключения. Тре тий конструктор принимает четыре аргумента: имя сервера базы данных, имя базы данных, идентификатор пользователя (имя) и его пароль (рис. 5).

У класса SqlService есть также открытые свойства. Я определил их, чтобы иметь альтернативный способ для задания значений строки подключения из вызывающей программы — без использования конструкторов.

ADO.NET: концепции и реализация Рис. 5. Конструкторы класса SqIService "U Конструктор класса (без аргументов) '// Перегружаемый;

да 7/ Sub NewO End Sub '// Конструктор класса (аргумент - полная строка подключении) '// Перегружаемый;

да 7/ Sub Hew(ByV8l sConnectionString As String) ffl_sCoRnectionString - sConneetionString nd Sub V/ Конструктор класса (аргументы - элементы строки подключения) V/ Перегружаемый;

да '// Sub New(8yVal aServer As String, ByVal sOatabase AS Stritvg, _ ByVal sysername As String, _ ByVal sPassword As String) Server = sServer Database = sDatabase Userrame = sttsemaee Password * sPassword End Sub RunSql (рис. 6) — перегружаемый метод, который принимает SQL-опера тор и имя нужной вам таблицы (объект DataTable), а возвращает DataSet.

Вторая версия RunSql принимает как аргумент только SQL-оператор и ничего не возвращает (рис. 7). Хотя оба метода выполняют SQL-операто ры, первый можно использовать для выборки данных, а второй — для до бавления, обновления и удаления данных. Перегрузка RunSql позволяет создать набор методов с одним именем, и конкретная версия выбирается в зависимости от переданных параметров. В данном случае, если вы пере дадите этому методу только SQL-оператор, будет вызван второй вариант RunSql.

В методе, который возвращает DataSet (рис. 6), я объявляю и создаю свои ADO.NET-объекты. Затем настраиваю объект SqlConnection вызовом ме тода Connect. (SqlConnection можно считать аналогом объекта ADODB.Con nection в ADO 2.x.) После того как свойства SqlConnection настроены, но соединение еще не открыто, я могу задать свойства объекта SqlComniand.

(Этот объект можно рассматривать как аналог ADODB.Command в ADO Microsoft ADO.NET 2.x.) Затем я присваиваю SqlCommand, созданный в соответствии с пере данным SQL-оператором, свойству SelectCommand объекта SqlDataAdap ter. Это указывает SqlDataAdapcer выполнить SqlCommand при выборке данных. Существуют и другие типы команд (InsertCommand, UpdateCom inand и DeleteComniand), но на них я остановлюсь в следующих статьях.

В завершение я вызываю метод Fill объекта SqlDataAdapter, чтобы запол нить DataSet данными из набора результатов, полученного при выполне нии команды.

Рис. 6. RunSql, возвращающий Dataset V/ Открытый иетод V/ Перегружаемый: да '// Возвращаемое значение: DataSet '// Назначение: выполняет SQL-оператор Public Overloads Function RunSqKByVal sSql As String, _.

ByVal sTableName As String) As DataSet Dim oCrad As SqlCommand = New SqlCoismandO Dim oCn As SqlConnection = Nothing '// объявить SqlCormection Dim oOa As SqlDataAdapter = New SqlDataAdapterO Dim oOs As DataSet = New DataSetO '//- Подготовить соединение с базой данных oCn = ConnectO With oCffid '//- Установить CommamjText, Connection и СошпапоТуре '//- для объекта SqlCommand.Connection - oCn. ConiBiandText = sSql.ComrnandType = CommandType.Text End With With oDa '//- Назначить объект SqJGomruand для SqlDataAdapter.SeleotCommand = oCmd '//- Выполнить SQL-оператор и заполнить DataSet.Fill(oDs, sTableName) End With *//- Отсоединиться от базы данных Disconnect(oCn).

'//- вернуть DataSet Return oDs End Function ADO,NET: концепций я реализация Рис. 7, RunSql для запросов, не возвращающих данные V/ Открытый метод "// Перегружаемый: да '// Возвращаемое значение: «ет V/ Назначение: выполняет SQL-оператор и ничего не возвращает Public Overloads Sub RunSqKByVal sSql As String) Dim oCind As SqlCoMand = New SqlCommandO Dim oCn As SqlCorvnection = Nothing '// объявить SqlConnection ' / Подготовить соединение с базой данных / oCn * Connect{) With oGuid ' / Выполнить SOt-one ратор /,CommandText = sSql.Connection = oCn.CommandType = ComrnandType.Text. ExecuteNonQue ry() End With ' / Отсоединиться от базы данных / Disconnect(oCn) End Sub Объект SqlDataAdapter (в ADO.NET Beta 1 он назывался SqlDataCom mand) не имеет прямого аналога в архитектуре ADO 1.x. On играет роль механизма управления четырьмя командами (SELECT, INSERT, UPDATE и DELETE), которые можно выполнить над DataSet. SqlDataAdapter мож но считать мостиком между подключеиными объектами (SqlCommand и SqlConnection) и отсоединенным объектом (DataSet).

DataSet примерно соответствует объекту Recordset в ADO 2.x, так как со держит данные, получаемые в результате выполнения запроса. Между тем DataSet может содержать несколько наборов данных благодаря объектам DataTable. Например, DataSet может содержать три связанных друг с дру гом объекта DataTable (вроде таблиц покупателей, заказов и позиций за казов). Кроме того, DataSet не знает, из какого источника в него поступи ли данные, что совсем не похоже на объект Recordset в ADO 2.x. По сути, различий у DataSet и Recordset больше, чем общего. К другим особеннос тям DataSet относятся Extended-свойства, позволяющие задавать нестан дартные свойства объекта;

свойства DataRelation для установления связей между разными объектами DataTable;

и средства работы с XML для экс порта в DataSet и импорта из него данных в формате XML.

94 Microsoft ADO.NET Если вам не нужен результирующий DataSet и вы просто изменяете дан ные, используйте тот метод RunSql, который показан на рис. 7. Он прини мает SQL-оператор и устанавливает SqlConnection, Затем настраиваются свойства объекта SqlCommand IT вызывается метод ExecuteNonQuery. Пос ледний исполняет SQL-запрос, но при этом информирует ADO.NET, что результирующих данных он не ожидает. (Это аналогично вызову метода Connection.Execute в ADO 2.x с параметром adExecuteNoRecords.) Если вы не производите выборку данных, гораздо эффективнее обращаться к методу ExecuteNon Query.

У меня также есть две перегружаемые версии метода RunProc. Первая возвращает DataSet (рис. 8), вторая не возвращает ничего (рис. 9). Пер вый RunProc работает почти так же, как метод RunSql за исключением того, что устанавливает параметры из Array List-списка m_oParmList. Что бы пройти по списку параметров, я создаю объект oEnumerator как экзем пляр класса lEnumerator. Это позволяет организовать цикл, добавляя па раметры в набор Sql Parameters.

Рис. 3. RunProc, возвращающий Dataset '// Открытый метод V/ Перегружаемый: да V/ Возвращаемое значение: DataSet V/ Назначение: выполняет хранимую процедуру Public Overloads Function RunProc(ByVal sProcName As String, ByVal sTableName As String) As DataSet Din oCrad As SqlCoiBiand = New SqlCommandO Dim oCn As SqlGonnectiorj = Nothing Dim oOA As SqlDataAdapter = New SqlDataAdapter() Dim oDs As DataSet = New DataSetO : Dim oSqlParameter As SqlParaiaeter = Nothing Dim oP As Parameter = Nothing '//- Получить перечислитель елиска параметров Dim oEnumerator As lEnumerator = m_oPamList.i3etEnuieeratorC) '//- Подготовить соединение с базой данных oCn = ConnectO With oCmd '//- Настройка CommandText, ActiveConneetlon и CommandType '//- йля объекта SqlConmtand.Connection = oCn.CommandText = sProeName.CommandType = GommandType.StoredProcedure End With см. след. стр.

ADO.NET: концепции и реализация Рис. 8. RunProc, возвращающий Dataset ' / Перебор Parameters в ArrayLtst /~ Do While (oEnumeratQr.MoveNextO) oP = Hotting "//- Получить текущий объект Parameter oP = Q&Himerator. Current *//- Создать экземпляр SqlPa remoter oSqlParameter = ConvertPararaeterToSqlParametef(oP)^ '//- Добавить объект SqlParameter к объекту SqlConmiano;

oCmd. Parameters. Add(oSqlPara«ieter) Loop With OOA '//- Назначить объект SqlCoirmiand для ^IDataAdapter.SelectCoflimand = oCmti '//- Выполнить храниму!» процедуру и заполнить BataSet,Fiil(oDs, nd '//- Отсоединиться от базы данных DiscoMect{oCR> V/- Вернуть DataSet Return oDs 'End fraction Рис. 9. RunProc для запросов, не возвращающих данные V/ Открытый метод V/ Оерегружаешй: да V/ Возвращаемое значение: нет '// Назначение;

выполняет хранимую процедуру Public Overloads Sub RunProc(ByVal sProcMame As String) Dim oCmti As SqlCommand = New SqlCommand() Dim оСл As SqlCoimeetion = Nothing Din oSqlParameter As SqlParameter = Nothing Diffl oP As Paraneter = Nothing '//- Получить перечислитель для списка параметров Di* «Enumerator As leniwierator <= Bi_oPariel.ist.eetEnuBerator{) т //» Подготовить соединение с -базой данных oCn = Connect О With '//- Настрбйка CoaniandText, ActiveCotinection :и CommandType см. след. стр.

Microsoft ADO.NET Рис. 9. RunProc для запросов^не возвращающих данные (окончание) '//- Для обьв1сга SqlGommand,Connection = oCn.CommandText = sProcName. ConwandType = ComBiandType.StoredPrQcedure. End ltttii '//- Перебор Parameters a ArrayLlst Do While (oEnumerator.MoveNextO) oP = Nothing '//- Подучить текущий объект Parameter of = oEnumerator.Current V/- Создать экземпляр SqlParameter oSqlParameter ~ ConvertParameterToSqlPafameter(oP) '//- Добавить объект SqlParameter к объекту SqlCoramand oCffld.Parameters.Add(oSqlParameter) Loop '//- Выполнить хранимую процедуру oCmd.ExecuteNonQueryQ '//- Отсоединиться от базы данных Disconnect(oCn) End Sub Данный цикл выполняется, пока метод MoveNext класса lEnumerator не возвращает false, т. е. когда параметров в ArrayList больше нет. Индекс текущего параметра из списка присваивается переменной оР. Затем я преобразую этот параметр в формат, понятный ADO.NET, т. е. в объект SqlParameter. В завершение я добавляю объект SqlParameter к набору SqlParameters объекта SqlCommand и продолжаю цикл. После того как все параметры добавлены в набор, я выдаю SQL-запрос и заполняю DataSet данными, вызывая методы Select Command и Fill объекта SqlDataAdapter так же, как и в RimSql.

Второй метод RunProc (рис. 9) служит для выполнения запросов, не воз вращающих DataSet. Здесь я опять указываю ключевое слово Overloads, сообщая Visual Basic, что этот метод следует вызывать при передаче одно го параметра. При передаче двух параметров вызывается другой метод RunProc (рис. 8).

Метод AddParameter (рис. 10) применяется, когда нужно передать пара метры классу SqlService при вызове хранимой процедуры. Этот метод при нимает имя параметра, его тип данных, длину и значение. Эти значения помещаются в локальный объект Parameter, который добавляется к Array ADO.NETt концепции и реализация List — тому самому списку, который я просматриваю в методе RunProc, Заметьте: я преобразую полученный тип данных (он соответствует одно му из значений перечислителя ssenuinSqlDataTypes) в соответствующий тип SqlDBType.

Рис. 10. Метод AddParameter V/ вткрьгтый метод V/ Пе.регружае«ый: да '// Возвращаемое значение: нет V/ Назначение: добавляет параметр хранимой процедуры Public Sub AddParameterCByVal sParameterNante As String, „, ByVal ISqlType As ssemiBiSqlDataTypes» _ ByVal iSize As Integer, ByVal sValue As String) г OiiB eQatalype As SqlDbType Dim оРатат As Parameter = Nothing Select Case ISqlType Case ssenuffl$qlDataTypes.ssSDT_String eDataType = SqlQbType.VarChar Case ssenu№SqlOataTypes,ssSOTInteger eDataType = SqlDDType.Int Case ssenumSqlDataTypes.ssSDT_OateTime eDataType = SqlDbType.BateTine Case ssenwiSqlDataTypes.ssSDTjJit eOataType = SqlDbType.Bit Case ssenuinSqlBataTypes. ssSDT_Decifial eDataType = SqlDbType.Decimal Сазе ssenuraSqlOataTypes.ssSDT_Honey eDataType = SqlDbType.Money ind Select оРагав ~ New Parameter(sParameterNaffls, eDataType, iSize, sValue) и_оРа rmUst. Add< oFa ram) End Sub SqIService на С# Версия SqIService на С# аналогична описанной выше — различия только в синтаксисе. Полностью код обоих примеров (на Visual Basic и С#) мож но скачать с сайта MSDN Magazine (http://msdn.microsoft.com/msdnmag/ issues/01/ll/code/DataOlll.exe). Метод RunSql, реализованный на С#, выполняет SQL-запрос и возвращает DataSet (рис. 11). Обратите внима ние на ключевую особенность: возвращаемое значение здесь указывается 4- Microsoft ADO.NET типом данных этого открытого метода. Кроме того, метод является пере гружаемым (есть две версии RunSql), но явно указывать это (как делает ся в Visual Basic ключевым словом Overloads) не нужно. В С# для пере грузки метода достаточно определить два метода с разными списками ар гументов.

Рие. 11. Метод RunSqJ на С#, возвращающий Dataset //- Открытый метед //- Перегружаемый: да //- Возвращаемое значение: DataSet //- Назначение: выполняет SQL-оператор public DataSet RunSql(string sSql, string sTableName) SqlCommand oCmd = new SqlCommandO;

SqlConnection oCn •= null;

SqlDataAdapter oda = new SqlDataAdapterO;

DataSet oDs = new DataSetf);

//- Подсоединиться к базе данных oCn = CorvnectO;

//- Настройка CommandText, Connection и CommandType //- для обьекта SqlComnand оСшй.Connection = oCn;

oCiBd.CofflinandText ~ sSql;

I.CoifflBandType = CommandType.Text;

//- Назначить объект SqlCommand для SqlDataAdapter oDa.SelectConimand = oCmd;

//- Выполнить SQL-оператор и заполнить DataSet oDa.FilKoDs, sTableName};

' / Отсоединиться от базы данных / DiscorineGt(oCn);

/ - Вернуть OataSet / return oDs;

Второй метод RunSql, не возвращающий DataSet, предназначен для запро сов, изменяющих данные. Для этого он объявляется как void:

public void RunSql(string sSql) ADO.NET: концепции и реализация Метод RunProc на С# выполняет хранимую процедуру и возвращает DataSet (рис. 12). От версии на Visual Basic он опять же отличается толь ко синтаксисом.

Рис. 12. Метод RunProc на С#, возвращающий Dataset //- Открытый метод //- Перегружаемый: ва //- Возвращаемое значение: DataSet //- Назначение: выполняет хранимую процедуру public DataSet RunProc{string sProcName, string зТаЬДеКаве) { :

SqlCommand oCmd = new SqlCoramandO i SqlConnection oCn = null;

SqlPararoeter oSqlParameter = null;

SqlDataAdapter oDa - new SqlDataAdapter О ;

DataSet oDs - new DataSetO;

Parameter oP = null;

//- Подучить перечислитель для списка параметров lEnumerator oEnutnerator * m_oPariBList.uetEnuroerator();

//- Подготовить соединение с базой данных оСп = Connect();

//- Настройка CofftmandText, ActiveGonnectlon и CommandType //- для объекта Sql Command oCnd.CoinmandText = sProcName;

. Connection = oCn;

, CommandType = CommandType. StoredProcedure;

.

//- Перебор Parameters a ArrayList while ( oEnuraerator.MoveNextO ) { oP = null;

//- Получить текущий объект Paraaeter oP = (ParaiReter)oEmjmeratQr. Current;

//- Создать экземпляр SqlParameter oSqlParanteter = ConvertParameterToSqlParameter(oP);

//- Добавить объект SqlParaieeter к объекту SqlCommand oCmd,Parameters.Add(oSqlParaaeter};

//- Назначить объект SqlCoiranarnJ для SqlBataAdapter oDa.SelectComfliand - oCid;

'//- выполнить храниму» процедуру и заполнить DataSet oDa.Fill(oOs, sTableName);

см. след. стр.

100 Microsoft ADO.NET Рис. 12. Метод RunProc на C$, возвращающий Dataset (окончание} //- бтеоединитьей от базы данных DlsconnGct(oCn);

//- Вернуть DataSet retarn oDs;

Второй метод RunProc, не возвращающий DataSet, предназначен для зап росов, изменяющих данные, Поскольку он не возвращает значений, то объявляется как void:

public void RunProc(string sProcName) Выполнение кода Ознакомившись с кодом, остается загрузить страницу WebForm.aspx (рис. 13). (Ее внешний вид одинаков для обеих версий — на Visual Basic и С#.) Поскольку данные привязаны к элементу управления DataGrid, вам не нужно заботиться о переборе всех записей и их отображении в HTML.

F-gyortes Vcots He p J ] 3, ccTFeeSj tea;

, bea =, aid aies 'Sweet and s^v-jry чаькм, r*iESt;

fs, swssttr, srii se^scnings Cnndirfienrs 3 Confections Desserts, candies, and «wet b'eads 4 Dairy Products Cheeses 5 Grehs^Cereals Breads, crackers, posts, snd cc'tal 6 MeaLJPuJtri/ Prepared riieuU 7 РггАям Dried fruit and bean (urd ;

S Seafood ''зевиеей and Hih Exanple of fisecutmg a Stored Procedure and Pi]Jing a Grid Longlf* Tofu -lie Dried uppies 1J Sob's Organic Dried Рел s;

I Рис. 13. WebForm.aspx ADO.NET;

концепции и реализация 3. Заключение Компонент доступа к данным, описанный в этой статье, выносит код, от носящийся к ADO.NET, в отдельные пространство имен и класс. Это по зволяет разделить ADO.NET-код и бизнес-логику, а также унифицировать доступ к данным через ADO.NET.

Разумеется, это пространство имен можно расширить, включив сюда и пространство имен System.Data.OleDb. Б приведенных примерах исполь зуется пространство имен SqIClient. которое при работе с SQL Server обес печивает большее быстродействие в сравнении с пространством имен OleDb. Но если вашим приложениям нужен доступ к базам данных под управлением СУБД, отличной от SQL Server, можно запросто создать SqlService, использующий пространство имен System.Data.OleDb (воз можно, совместно с SqIClient). Выбор за вами, но я рекомендую простран ство имен SqIClient, если вы работаете только с SQL Server.

В следующих публикациях я подробнее расскажу о методиках работы с ADO.NET, в том числе о применении четырех разных команд SqlDataAd pater, об использовании объектов DataSet для обмена данными между уровнем представления (presentation layer) и уровнем бизнес-логики (business layer), а также об установлении связей (relations) между несколь кими таблицами, представленными в DataSet объектами DataTable.

Джонни Папа (Johnny Papa) — вице-президент компании MJM Investigations по разработке программного обеспечения (Роли, штат Северная Каролина), автор нескольких книг по ADO, XML и SQL Server. Часто выступает на конфе ренциях, в том числе на VSLive. С ним можно связаться по адресу datapoints@lancelotweb.com.

Боб Бьючмин Разработка собственных провайдеров данных для.NET Data Access Framework* В сборке.NET Framework System.Data.dll содержатся пространства имен, где определены базовые классы, используемые при создании нестандартных провайдеров данных (custom data providers). Кроме того, в этих простран ствах имен определено несколько интерфейсов доступа к данным и базовых классов, позволяющих создавать провайдеры данных, способных взаимодей ствовать с другими нестандартными провайдерами.

С помощью ADO.NET-классов Connection, Command, DataReader и DataAdapter написать провайдер ADO.NET легче, чем провайдер OLE DB. В статье расска зывается об этих классах и их реализации, а также о том, как на их основе создавать провайдеры данных разных видов.

Windows-платформы уже давно включают API доступа к данным, осно ванные на универсальной парадигме «провайдер-потребитель» (provider consumer). Первым появился ODBC (Open Database Connectivity) API, ориентированный на источники реляционных данных. ODBC почти одно значно соответствовал интерфейсу SQL CLI (Call-Level Interface), опреде ленному комитетом стандартизации ANSI SQL. ODBC-драйверы либо инкапсулируют протоколы, специфичные для конкретных баз данных, вроде TDS (tabular data stream), используемого SQL Server, либо образу ют уровень, размещаемый поверх API, специфичных для конкретных по * Публиковалось в MSDN Magazine. 2001. №12 (декабрь). — Прим. изд.

Разработка собственных провайдеров данных для.NET Data Access Framework ЮЗ ставщиков, например поверх OCI (Oracle Call Interface). Затем появился OLE DB — универсальный набор СОМ-интерфейсов и типов, распростра нивший возможности ODBC на источники нереляционных данных. Так как OLE DB позволял работать с источниками любых нереляционных данных, создатели любых видов информации — от плоских (flat) файлов до иерархических и многомерных данных — были заинтересованы в общей объектной модели OLE DB.

Microsoft.NET поставляется со сборкой System.Data.d 11, содержащей про странства имен, которые напоминают OLE DB и ODBC. В ее пространстве имен Systeni.Data определен общий набор интерфейсов доступа к данным, а в System.Data.Common присутствует несколько абстрактных базовых классов, реализующих функциональность, общую для всех провайдеров.

Кроме того, в System.Data.dll входят два пространства имен (System.Da ta.OleDb и System.Data.SqlClient), которые предоставляют параллельные наборы классов и интерфейсов, называемые в документации провайдера ми данных. Провайдеры данных инкапсулируют в наборах управляемых классов и интерфейсов средства взаимодействия с базами данных и дру гие API-функции доступа к данным. Типы и интерфейсы, предоставляе мые провайдерами данных, наследуются от общего подмножества и обес печивают почти эквивалентную (но не идентичную) функциональность.

Так как не требуется, чтобы у всех провайдеров были одинаковые типы и интерфейсы, разработчики провайдеров могут реализовать функциональ ность, уникальную для источника данных. Кроме того, System.Data под держивает модель отсоединенных объектов данных (disconnected object model), основанную на типе DataSet, который является более совершенной версией библиотеки клиентских курсоров ODBC и отсоединенного объек та Recordset (OLE DB).

В документации на.NET SDK Beta 2 поясняется, как разрабатывать про вайдеры данных, какие типы и интерфейсы содержит стандартный про вайдер и как использовать методы интерфейсов, чтобы обеспечить взаимо действие с нестандартными провайдерами данных. Там же дан пример провайдера для доступа к нестандартному хранилищу данных. Информа ции, приведенной в документации, вполне достаточно, чтобы понять, как ре ализуются стандартные типы и интерфейсы, определенные в System.Data, и используются вспомогательные базовые классы из System.Data.Common.

Я покажу простейший пример провайдера данных.NET, предоставляюще го стандартный набор типов и интерфейсов. На основе этого примера мы исследуем объектную модель провайдера данных, отделим базовую функ циональность от расширенной и рассмотрим некоторые расширения, спе цифичные для конкретных провайдеров. Я также расскажу о причинах, по которым доступ к данным предоставляется через провайдеры, и об альтер 104 Microsoft ADO.NET нативах провайдерам данных. Учтите, что код для этой статьи был напи сан в расчете на.NET Beta 2.

Зачем создавать провайдеры данных?

Во времена OLE DB и ODBC драйверы и провайдеры разрабатывались по разным причинам. Реализовав провайдер, вы получали возможность рабо тать с наборами данных через стандартные GUI-элементы. Программы генерации отчетов, обмена данными и другие аналогичные приложения использовали один и тот же API. Стандартом обмена данными де-факто стали ADO Recordset и RDS (Remote Data Services), поэтому многие груп пы разработчиков создавали провайдеры, предоставляющие данные толь ко в виде ADO-объектов Recordset. Появилась даже перспектива создания клиентов, работающих по принципу «написан однажды, используется с чем угодно». Провайдеры и драйверы создавались, даже если способ дос тупа к данным мало соответствовал или вообще не соответствовал объек тной модели. Так, был разработан ODBC-драйвер для доступа к текстовым файлам через SQL и архитектуру OLE DB Simple Provider.

Различные среды визуальной разработки для Windows, например Visual Basic, предоставляли доступ к данным через элементы управления, связы ваемые с данными (data-bound controls). Чтобы провайдеры и драйверы могли работать с такими элементами управления, вы должны были писать их в соответствии с дополнительным набором правил и реализовать в них поддержку стандартных объектов и интерфейсов, Последней из таких спе цификаций для разработчиков провайдеров OLE DB и элементов управ ления, связываемых с данными, была ActiveX Control Writer's Specifica tion for OLE DB.

Провайдеры ODBC и OLE DB удобны тем, что их можно напрямую ис пользовать в приложениях Microsoft и сторонних фирм. Например, в Microsoft Access можно задействовать любой ODBC-драйвер для прямого доступа к данным — тогда Access-приложения работают с ними так, будто получают их из базы данных Access. SQL Server и DB/2 (в Windows NT) позволяют применять источники данных OLE DB в распределенных зап росах. Кроме того, начиная с SQL Server 7.0, любой провайдер OLE DB можно использовать для импорта, экспорта и преобразования данных че рез DTS (Data Transformation Services). В Crystal Reports ODBC-драйве ры или ADO-объекты Recordset служат для получения входных данных, из которых формируется отчет. В других приложениях с помощью ADO или ODBC генерируются OLAP-данные или XML-схемы. Эти и другие примеры показывают, что разработка провайдеров OLE DB или ODBC драйверов весьма перспективна.

Разработка собственных провайдеров данных для.NET Data Access Framework Создание провайдера данных В Visual C++ 6.0 Microsoft ввела набор классов на основе ATL (ActiveX Template Library), позволяющий разрабатывать провайдеры OLE DB. С ATL-шаблонами провайдеров OLE DB поставлялся мастер (wizard), кото рый использовал инфраструктуру ATL и генерировал пример провайдера, предоставляющего доступ к структуре WIN32_FIND_DATA (с информа цией о файлах и каталогах) через соклассы (cotypes) и интерфейсы OLE DB. Я настолько часто применял этот провайдер в демонстрационных це лях, что даже присвоил ему стандартное имя: DirProv — провайдер OLE DB для доступа к информации о каталогах. Поскольку в.NET есть клас сы со схожей функциональностью, я переделаю тот пример в провайдер данных и назову его «управляемым dirprov» (managed dirprov).

Написать провайдер данных для ADO.NET гораздо проще, чем для OLE DB. Интерфейсы в ADO.NET четко определены;

создавая класс с опреде ленной функциональностью, вы должны реализовать соответствующий интерфейс. При этом нужно реализовать минимум четыре основных клас са: Connection, Command, DataReader и DataAdapter. Вкратце рассмотрим эти классы.

Connection Этот класс обязателен, даже если вы не собираетесь на деле подключаться к источнику данных. Он нужен другим классам, так как предоставляет им базовую функциональность. DataAdapter, например, вызывает методы Open и Close класса Connection при заполнении Data Table, входящего в DataSet.

Command У класса Command как минимум два предназначения. В язы ках, используемых при операциях с данными, команды позволяют напря мую обновлять хранилища данных. Скажем, в SQL такими командами яв ляются INSERT, UPDATE и DELETE. Кроме того, можно передавать коман ды, возвращающие результаты. Пример такой команды в SQL — SELECT.

DataReader Класс DataReader используется для обработки результатов, возвращаемых Command. Его методы позволяют перебирать записи одно го или нескольких наборов результатов с перемещением только вперед (forward-only). Кроме того, он предоставляет методы для считывания дан ных из полей этих записей в переменные.NET-типов.

DataAdapter Класс DataAdapter заполняет DataSet результатами, воз вращаемыми классом Command, а также позволяет обновлять данные с по мощью команд и предоставляет событийную модель для операций обнов ления, требующих тонкой настройки.

Эти обязательные классы показаны на рис. 1;

там же отражены некоторые взаимосвязи между ними. Некоторые, но не все — например, класс Com 106 Microsoft ADO.NET mand может быть создан не только методом Connection. Create Command, но и указанием Connection в одной из перегруженных версий конструкто ра класса Command. Поскольку ряд классов зависит друг от друга, на ил люстрации представлен минимальный набор классов, реализуемых на практике. Для начала я реализовал провайдер, содержащий эти классы и поддерживающий только чтение. Создав базовую функциональность (рис. 1), дополнительные сервисы можно разместить поверх существующих обяза тельных классов или определить через вспомогательные классы. Обо всем этом я и расскажу в статье.

IDbConneclion CHH IDbCommand Connection Created о mm and ctor(Command) ExecuteReader IDbDalaAdapter О-Ц IDbOataReader IDbDataRecord тжввввтвтгявчячч Рис. 1. Некоторые взаимосвязи между классами провайдера данных Класс Connection До реализации класса Connection надо реализовать интерфейс IDbConnec tion. В IDbConnection (рис. 2) шесть открытых методов, среди которых наиболее очевидны Open и Close. Так как в исполняющей среде.NET не поддерживается концепция детерминированной деструкции (deterministic destruction), вы должны явно вызывать метод Close, а не просто освобож дать все указатели на интерфейс, как это можно было бы сделать в OLE DB. Метод ChangeDatabase используется, если ваш источник данных дол жен поддерживать подключение к другой базе данных.

Метод BeginTransaction запускает локальную транзакцию. BeginTransac tion возвращает интерфейс IDbTransaction, через который вы вызываете методы Commit или Rollback. Имеется перегруженная версия BeginTran saction, принимающая в качестве параметра уровень изоляции транзакции (transaction isolation level). Если ваш источник данных не поддерживает локальные транзакции, реализовать метод BeginTransaction не нужно.

Наконец, IDbCreateCommand создает объект Command вашего провайде ра и возращает ссылку на интерфейс IDbCommand, У IDbConnection. четыре открытых свойства. ConnectionString, Connec tionTimeout и Database — это наиболее часто используемые свойства, от носящиеся к соединениям. Четвертое открытое свойство, State, крайне важно. Оно доступно только для чтения и возвращает значение перечис лимого ConnectionState (табл. 1). Смысл значений Open и Closed очеви Разработка собственных провайдеров данных для.NET Data Access Framework ден. Они используются, если ваш источник данных поддерживает асинх ронную инициализацию, a Fetching применяется, если источник данных поддерживает асинхронную выборку. Broken соответствует состоянию, когда соединение открыто, но не работоспособно, например, если исполь зуемая вами база данных завершает работу по указанию системного адми нистратора.

Рис. 2. Интерфейс iDbConnectipn public interface IDbConnection { IQbTransaction BeginTrartsaction(IsolationLevel iso) IDbTransaction 8egin.Transaetion() bool ChaflgeDatabase(striRg newdb) ;

void CloseO IPbCoiamand CreateComroandO void Qpert() // Свойства // get, set string ConnectionString // get, set int ConnectlonTimeout // get, set string Database Connect!onState State // set Табл. 1. Перечислимое ConnectionState Описание Член Broken Связь разорвана. Это возможно только после открытия соеди нения. Если соединение находится в таком состоянии, его можно закрыть, а потом открыть снова.

Closed Объект закрыт.

Connecting Выполняется подключение к объекту.

Executing Объект выполняет команду.

Fetching Выполняется выборка данных.

Open Объект открыт.

Реализация класса Connection Чтобы реализовать интерфейс IDbConnection, определитесь, к какому ис точнику данных вы будете подключаться и какая информация вам нужна.

Если требуемая информация соответствует используемой базе данных, тем лучше. Если нет, придется включать дополнительные свойства и поля.

Для получения примитивной информации о каталогах я реализовал про стые методы Open и Close (они лишь присваивают значения свойству 108 Microsoft ADO.NET ConnectionState), а формированием строки подключения, выбором теку щей базы данных и созданием транзакций я, естественно, не занимался.

Класс Connection должен разрешать создание своих экземпляров другими классами. Следует написать минимум два конструктора: не принимающий аргументов и принимающий аргумент типа String (строку подключения).

Так как мой провайдер данных не использует строку подключения, я реа лизую только первый конструктор. Состояние создаваемого объекта Con nection должно инициализироваться значением ConnectionState.Closed;

для этого закрытой переменной _ConnectionState присваивается соответ ствующее значение.

При реализации я придерживался нескольких простых соглашений. Име на закрытых полей, к которым обращаются свойства, начинаются со зна ка подчеркивания. Когда функция не поддерживается (NotSupported) или не реализована (Notlmplernented), генерируется исключение вызовом со ответствующего закрытого метода. Реализация MDirConnection включена в набор исходного кода, который можно скачать с сайта MSDN Magazine по ссылке http://msdn.microsoft.com/msdnmag/code01.asp в разделе за де кабрь.

Специфичная функциональность в Connection Спецификация OLE DB позволяла разработчикам провайдеров реализо вать нестандартные интерфейсы, специфичные для конкретных провайде ров. То же самое можно делать и при создании.NET-провайдеров данных.

Кроме того, вы можете реализовать методы экземпляра (или их перегру женные версии), специфичные для провайдера. Как и в случае OLE DB, универсальный клиент может выбирать, использовать ли ему методы, спе цифичные для провайдера. Самый надежный способ распознавания под держиваемых возможностей в период выполнения — приведение типов интерфейсов/классов. Для этого можно использовать интерфейсы-марке ры (marker interfaces).

Специализированные клиенты могут находить специфичные для провай дера методы, перегруженные версии и свойства через отражение (reflec tion), не зная заранее об их наличии, и получать доступ к ним в соответ ствии с разрешениями. Так как все эти действия производятся в период выполнения, ни один метод не может заблаговременно сообщить потреби телю сведения, касающиеся семантики использования специфичной фун кциональности.

Примером специфичной функциональности класса Connection является набор параметров, передаваемых в строке подключения в случае SqlClient.

Кроме стандартных параметров вроде Data Source, User ID и Password (за Разработка собственных провайдеров данных для.NET Data Access Framework имствованных из спецификации OLE DB), SqlClient поддерживает пара метры, специфичные для SQL Server (например, Network Library и размер буфера TDS).

Более сложный пример специфичной функциональности — реализация пула соединений, повышающего эффективность совместного использова ния соединений в трехуровневой среде, например в случае Web-сервера. И OleDb, и SqlClient поддерживают создание пула соединений, но семанти ка этой поддержки различна.

Класс Command Класс Command должен реализовать интерфейс IDbCommand (рис. 3), Основное назначение IDbCommand — отправлять команды или запросы хранилищу данных. Команды, изменяющие данные, но не генерирующие набор результатов, передаются методом IDbCommand.ExecuteNonQuery, возвращающим общее число записей, на которые воздействовала команда.

Команды-запросы возвращают наборы записей через класс DataReader.

Метод IDbCommand.ExecuteReader вовзращает интерфейс IDataReader класса DataReader. Метод ExecuteScalar используется, чтобы получить первое поле первой записи, но применим и для получения скалярного ре зультата. Возможное применение этого метода — чтение набора записей, который на самом деле является целым экземпляром объекта или доку ментом.

Для IDbCommand.ExecuteReader имеется перегруженная версия, прини мающая параметр CommandBehavior. Этот параметр указывает, что долж но происходить при выполнении команды. Значения перечислимого Com mandBehavior показаны в табл. 2.

Табл. 2. Перечислимое CommandBehavior Член Описание CloseConnection При выполнении команды связанный с ней объект Connection закрывается, когда закрывается сопоставленный с той же командой объект DataReader Keylnfo Запрос возвращает информацию о полях и первичном ключе SchernaOnly Запрос возвращает информацию только о полях и не влияет на состояние базы данных SequentialAccess Результаты считываются последовательно на уровне полей SingleResult Запрос возвращает один набор результатов;

выполнение запроса может повлиять на состояние базы данных SinglcRow Ожидается, что запрос вернет одну запись;

выполнение запроса может повлиять на состояние базы данных 110 Microsoft ADO.NET Рис. 3. Интерфейс IDbCommand public interface IDbCommand public void CancelO public I&ataParameter CreatePararaeterO public int ExeeuteNonQueryO public IDataReader ExecuteReaeferQ public IDataReader ExecutefieaderfCommandBehavior b) public object ExecuteScalar() public string CoairoandText // get, set public int CofflfflandTimeotit // get, set public ComaancfType CoioiandType // get, set public XDbCdnnection Connection // get, set public IDataParameterColleetion Parameters // get public IDbTransaction Transaction // get, set public UpdateftowSource UpdatedRowSouree // get, set Для команд необходимо задавать свойства CommandText (саму команду) и CommandType. Эти свойства являются открытыми. Кроме того, как от мечается в документации, строку CommandText и экземпляр класса Con nection можно указать в перегруженной версии конструктора. Единствен ный CommandType, который обязательно нужно поддерживать, — это CommandType.CommandText. Другие типы служат для работы с хранимы ми процедурами и использования имен таблиц вместо команд (последнее только в OLE DB). Команды можно отменять напрямую (методом Cancel) или по истечении срока ожидания (задаваемого свойством Command Timeout).

Типы команд содержат ссылки на другие типы объектной модели. С объ ектом Command нужно связать объект Connection (как ссылку на IDbCon nection), указав на соединение в конструкторе или напрямую задав соот ветствующее свойство. При необходимости с командой можно сопоставить объект Transaction (как ссылку на IDbTransaction). Transaction также за дается в конструкторе или наследуется от нижележащего объекта Con nection.

Хранимые процедуры или параметризованные запросы используют набо ры параметров. Для работы с параметрами предназначен.NET-тип, явля ющийся набором (collection). Набор Parameters — это свойство класса Command, который включает и метод Create Parameter. Наконец, Updated RowSouree указывает, какая версия DataRow используется при обновле ниях, выполняемых через класс DataAdapter.

Разработка собственных провайдеров данных для,NET Data Access Framework Реализация класса Command В моем примере провайдера данных в качестве входных данных использу ется имя каталога. Для этого достаточно типа команды CommandText.

Поскольку мне не нужны транзакции, параметризованные запросы и воз можность отмены команды, основную часть реализации составляют мето ды выполнения команды. ExecuteNonQuery и ExecuteReader создают эк земпляр DataReader, вызывают его внутренний метод GetDirectory и воз вращают либо число элементов в каталоге (количество обработанных записей), либо DataReader, обрабатывающий эти записи.

Рис. 4. ExecuteReader public IDataReader ExecuteReader(CominandBehaviQr t>) { Debug.WriteLine("MBirCoimancl.ExecuteReader(b)", "MdirCommand"};

// Соединение должно быть установлено и открыто if ^Connection == sull II.Connection.State != ConnectionState.Open) throw new InvalidOperationExceptionC" Connection must be valid and open"};

if ((b & CoKiBand8etiavior.KeyInfo) > 0) Debug.WriteLine("Behavior includes Keylnfo"};

if ((b & CoieiandieftavlQr.SchemaQnly) > 0} Debug.WriteLinet"Behavior in-cludes Schema&nly");

// Обрабатываем только CloseConrtection или "все остальное" if <(Ь & CoiemaniiBehavior.CloseConnection) > 0) { De&ug.WriteUneC'Behavior Inelttctes CloseCotiRection");

HDirDataReader reader = new HDirDataReader(_CominandText, „Connection);

reade r. GetOi recto ry („CoiwnandText);

return reader;

I else { HDirCataReader reader = new MDirDataReader(_ComfflandText);

, reade r. Get Di recto ry(_ConnnandText);

return reader;

Самый сложный в программировании — перегруженный мегод Execute Reader, принимающий параметр типа Command Behavior. Поскольку неко Microsoft ADO.NET j. торые значения CommandBehavior можно объединять логической опера цией «или», при реализации метода приходится выполнять побитовую проверку параметра. Хотя можно указать Keylnfo или SchemaOnly, инфор мация о схеме все равно возвращается вместе с данными (через метод GetSchemaTable класса DataReader). Особый случай — CloseConnection.

При этом значении вызывается конструктор DataReader, который прини мает объект Connection, используемый командой. А когда вызывается ме тод Close класса DataReader, закрывается и связанный с командой объект Connection. Реализация метода ExecuteReader показана на рис. 4.

Специфичная функциональность в Command Провайдеры данных SqlCIient и OleDb реализуют почти всю функцио нальность IDbCommand. Так как базы данных во многих случаях быстрее генерируют отдельные результаты, а не наборы, оба провайдера поддержи вают для CommandBehavior значение SingleRow. В обоих провайдерах ре ализован метод Prepare, позволяющий отправить базе данных отдельную команду для анализа запроса и подготовки плана его выполнения. Кроме того, SqlCIient уникальным образом расширяет возможности класса Com mand. Поскольку SQL Server 2000 может возвращать результаты запроса в виде потока XML-данных, SqlCIient содержит открытый метод Execute XmlReader, возвращающий XmlReader вместо IDataReader.

Класс DataReader У класса DataReader нет открытого конструктора. Этот класс должен реа лизовать интерфейсы IDataReader и IDataRecord. Класс DataReader по зволяет читать данные по одной записи единовременно в направлении только вперед и получать значения полей извлеченных записей как типи зированные или обобщенные типы данных. Определения интерфейсов IDataReader и IDataRecord показаны на рис. 5.

IDataReader дает возможность проходить набор результатов методом Read, который возвращает false, если достигнут конец набора записей. При выполнении одной команды может возвращаться несколько наборов ре зультатов. Для перехода к следующему набору результатов используется метод NextResult.

Иногда нужно получить описание информации, содержащейся в наборе результатов, — так называемые метаданные. Метод GetSchemaTable интер фейса IDataReader может возвращать DataTable с метаданными для каж дого набора результатов. Таблица, возвращаемая GetSchemaTable, содер жит как стандартную информацию, так и специфичную для провайдера, Разработка собственных провайдеров данных для.NET Data Access Framework Рис. 5. Интерфейсы IDataReader и IDataRecord public interface tuataReader \ Ц Иетода IDataReader public BataTable GetScheiftaTable< } public void Close() public bool Nextflesult(> public bool ReadO // Свойства IDatafieader public int Depth public bool IsGlosed public int Records Affected public interface IDataRecord { •// Методы IDataRecord public bool Set&Qoleafl{iRt i) public byte QetSyteCint i) public int Get8ytes(int i, int fieldoffset, byteH buffer, int length, int bufferoffset) putilic char SetChar(int i) public int GetCharsfirvt i, int fielaoffset, chart] buffer, Int length, int bufferoffset) public IDataReader GetCata(int i) public string 6etOataTypeNarne(int i) public DateTime 6etDateTiroe(int i) public decimal QetDecimaKint i) public double QetDauble(int i) public Type GetFleldType(int i) public float SetFloatCint i) public Quid 6etGuid(int i) public short Setlntl6(int i) public int Getlnt32(int i) public long 6etlnte4{int i) public string 6etNafne(int i) public int GetOrdinal(string name) public string QetString(int i) public object GetValue(int i) public int GetValuesCobjectC] values) public bool IsDBHulldnt i) // Свойства IDataRecord public int FleldCount public object this[string nafae];

public object this[int 1] 12,4 Microsoft ADO.NET IDataRecord предоставляет методы, позволяющие провайдерам возвра щать строго или слабо типизированные данные из каждого поля. Суще ствует ряд строго типизированных методов-получателей (getters), прини мающих порядковый номер поля с отсчетом от нуля, а есть и универсаль ные методы GetValue и GetValues, возвращающие значения типа object.

IDataRecord всегда возвращает управляемые.NET-типы;

таким образом, этот интерфейс инкапсулирует проекцию системы типов источника дан ных на систему.NET-типов. Кроме того, IDataRecord содержит перечис лители (iterators) полей, доступные как перегруженные версии свойства Item. Эти перечислители возвращают поле по имени или порядковому номеру с отсчетом от нуля.

Реализация DataReader Класс DataReader содержит специфичный для провайдера метод Get Directory, который вызывает управляемый метод System.IO.Directory Info. GetFileSysternlnfos. Провайдер данных предоставляет доступ к под множеству возвращаемой этим методом информации, позволяя получить поля Name, Size, Type (файл или подкаталог) и CreationDate. Name и Туре имеют тип String, Size — Int64, a CreateDate — DateTime. Поскольку в каж дом наборе результатов возвращаются одни и те же метаданные, результа ты помещаются в четыре массива: типов (управляемый тип каждого поля), размеров (размеры этих типов), имен (имена полей) и полей (значения типа object). Хотя элементами массива полей являются объекты, каждое поле — это экземпляр соответствующего типа, т. е. в отличие от ADO поля не представляются типом Variant. Поскольку метаданные всегда одни и те же, получение информации о схеме «зашито» в методе GetSchemaTable. За щищенная реализация класса DataReader и метода Read показана на рис. 6.

При реализации строго типизированных методов-получателей использу ется простое приведение типов. Неправильное приведение типа (напри мер, применение Getlnt32 для поля типа String) вызывает генерацию InvalidCastException, как и в провайдерах данных SqlClient и OleDb. По скольку все значения данных уже относятся к управляемым типам, преоб разование типов хранилища данных в управляемые типы не требуется.

Свойства Item реализованы с помощью индексаторов (indexers) языка С#.

Провайдер не поддерживает формирование нескольких наборов результа тов одной командой.

Чтобы элементы управления, связываемые с данными, могли использо вать класс DataReader, этот класс должен реализовать интерфейс lEnume rable. Я предпочел реализовать lEnumerable с помощью класса DbEnume rator пространства имен System.Data.Common. Провайдеры данных Sql Client и OleDb тоже используют этот класс. Чтобы реализовать метод Get Enumerator интерфейса lEnumerable, я возвращаю экземпляр DbEnu Разработка собственных провайдеров данных для.NET Data Access Framework merator, создаваемый конструктором, в который передается экземпляр моего класса DataReader:

lEnumerator System.Collections.Innumerable.GetEnumerator() { return ((lEnumerator) new DbEnumerator(this));

Рис. 6. Реализация MDirDataReader // IDataReader.ReadC) public bool ReadO i Debug. WrlteLinerK&lrDataReader. Read", "HdirBataReader");

if (_le i» null) { bool notEQF = „ie.MoveNextO;

if (notEOF == true) { „CurrentRow++;

if (_fsi[_CurrentRo«] is Filelnfo) { FUelnfo f = (Filelnfo)_fsiE_GurrentRow3;

_cols[Q] a f.Narae;

_cols[l] « f.tangttv.ToStringC);

;

- -. - • _cols[2] * "File";

„ео1з3] = f.CreatiDnTiffle.ToStrlngO;

else Oireetoryinfo d * (DirectoryInfo)_fsit_GurrentBow];

_cols[0] * d.Hame;

_cols{1] = "0";

_cols[23 * "Directory";

_eols[3] * d. Great ionTiae.ToStringO;

return notEOF;

}, return false;

} // Реализация MOir DataReader используется методами // HDirCoronand, Execute и HOirCoiffiand.ExecuteReader /* « Поддерживаем автоматическое закрытие соединения, обрабатывая * флаг CoBinandfleliavior.GloseConnection. Null задает * обычное поведение (без автоматического закрытия), */ см. след. стр.

116 Microsoft AD0.NET Рис, 6. Реализация MDirDataReader (окончание) private IDbConnectiOR „Connection = null;

internal Directorylnfo _dir;

internal meSystemlnfop _fsi;

internal int _GurrentRow;

internal lEnumerator _ie;

internal StringCJ „names * {"Name", "Size", "Type", "CreationDate" };

internal Type[] „types = (typeofCString), typeof(long), typeof(St ring}, typeof(Dateline)I;

internal object[] _cols = new object[43;

// Максимальный размер в байтах internal Int32{] „sizes » { 1024, 8, 9, 8 >;

internal void GetDirectory(String command) _dir = new Plrectorylnfo(coffimand);

_fsi = _dir.GetFileSystemInfos();

„flecordsAffected = „fsi.Length;

_GurrentRow = -t;

_ie = _fsi.GetEnumerator();

isClosed = false;

Специфичная функциональность в DataReader Провайдеры OleDb и SqlClient no-разному работают с управляемыми ти пами. У SqlClient есть перечислимое SqlTypes, а пространство имен Sys tem. Data.SqlTypes включает типы, специфичные для SQL Server. Помимо строго типизированных методов-получателей (например, Getlnt32), пре образующих типы данных SQL Server в.NET-типы, в SqlClient имеется ряд методов-получателей (скажем, GetSqlInt32), которые предоставляют доступ к «родным» типам SQL Server из пространства имен System.Da ta.SqlTypes без всякий преобразований. В документации.NET SDK утвер ждается, что при использовании «родных» типов достигается большее быстродействие, чем при преобразовании в управляемые типы.

Провайдер данных OleDb конвертирует OLE DB DBTYPE в управляемые типы, поскольку типы, используемые хранилищем данных, отображаются провайдером на DBTYPE-типы OLE DB. Для нас представляет интерес только интерфейс IDataReader, возвращаемый методом IDataRecord.Get Data. В OLE DB поддерживается концепция разделов (chapter concept), в соответствии с которой иерархические данные предоставляются через спе циальное поле типа chapter, связывающее родительский и дочерний набо ры записей (rowsets). IDataRecord.Get Data возвращает DataReader, уста навливаемый на дочернюю запись, которая относится к заданной роди тельской. Ту же концепцию можно было бы применить и в провайдере данных MDirProv для рекурсивного обхода подкаталогов файловой системы, Разработка собственных провайдеров данных для.NET Data Access Framework Ц Класс DataAdapter DataAdapter — один из немногих классов провайдеров данных, который реализуется на общем базисе. Ваши классы DataAdapter, специфичные для конкретных провайдеров, наследуются от класса DbDataAdapter, который в свою очередь является производным от DataAdapter. Эти классы реали зуют интерфейсы IDataAdapter (в котором определяются методы Fill и Update, используемые при взаимодействии с DataSet) и IDbDataAdapter.

Последний предоставляет доступ к четырем объектам Command (Select Command, UpdateCommand, InsertCommand и DeleteCommand), определя ющим взаимодействие между провайдером и DataSet. Кроме того, класс DataAdapter содержит стандартный набор событий и делегатов, позволя ющих принимать уведомления и влиять на поведение класса до и после обновлений (под обновлениями подразумеваются операции Insert, Delete или Update, выполняемые над хранилищем данных). Иерархия классов и интерфейсов показана на рис. 7.

IDbDataAdapter t IDbDataAdapter t Наследование интерфейса Наследование класса I IDataAdapter DataAdapter Рис. 7. Иерархия наследования Реализация DataAdapter Реализация DataAdapter провайдера данных DirProv будет «облегчен ной», поскольку этот провайдер предоставляет данные только для чтения.

Поддерживается лишь метод SelectCommand, поскольку никакой поддер жки обновлений или передачи команд не требуется — как и событий и делегатов, относящихся к обслуживанию обновлений (они все равно не будут использоваться).

Базовый класс, DbDataAdapter, содержит почти все необходимые для класса MDirDataAdapter методы (Fill, FillSchema и др.). MDirDataAdapter работает так, как и должен, позволяя потребителю данных заполнять DataSet методом Fill и предоставляя доступ к SelectCommand. Изучая ис Microsoft ADO.NET ходный код провайдера, вы заметите, что метод Fill класса DbDataAdapter вызывает Command.ExecuteReader(commandBehavior), передавая флаг поведения SequentialAccess, а метод FillSchema этого класса использует флаг поведения Keylnfo SchemaOnly.

Специфичная функциональность в DataAdapter Провайдеры SqlClient и OleDb реализуют подклассы Updating/Updated EventArgs и делегаты. В провайдере OleDb реализована перегруженная версия класса Fill, позволяющая заполнить DataSet или DataTable данны ми из классического ADO-объекта Recordset.

Добавление дополнительной функциональности Итак, я рассказал обо всех обязательных объектах в объектной модели провайдера данных. В качестве заключительного аккорда хотел бы отме тить, что провайдер данных DirProv полностью готов к работе и может быть расширен для обслуживания трассировки более высокоуровневых вызовов, поступающих в стек данных, управляемых ADO.NET. Весь ис ходный код можно скачать по уже упоминавшейся ссылке с сайта MSDN Magazine. В моем провайдере реализован только базовый набор типов и интерфейсов, поэтому, как и в случае провайдеров OLE DB, вы могли бы добавить в него дополнительную функциональность. Для полноты карти ны я кратко опишу доступные расширенные типы, а также определения и реализации базовых классов: типы транзакций, классы, реализующие па раметры и наборы параметров, класс CommandBuilder, типы, применяемые при обработке ошибок, и типы разрешений. Обратите внимание, что в про вайдерах данных SqlClient и OleDb реализованы специфичные вариации этих типов и перечислимых.

Типы транзакций Типы транзакций инкапсулируют семантику локаль ных транзакций, которые могут запускаться внешними по отношеник» к провайдеру объектами. В состав как OleDb, так и SqlClient входят типы Transaction, реализующие интерфейс IDbTransaction.

Параметры и наборы параметров Эти классы реализуют наборы (collec tions) параметров, используемые хранимыми процедурами и параметризо ванными запросами. Параметры должны преобразовывать.NET-типы в типы базы данных методами, аналогичными методам IDataReader. SqlPara meter и OleDb Parameter реализуют один и тот же интерфейс IDataPara meter, а наборы параметров — интерфейс IDataParameterColIection, а также интерфейсы для операций над наборами (lEnumerable, ICollection и IList).

CommandBuilder Этот класс предназначен для того, чтобы упростить формирование используемых по умолчанию команд InsertCommand, Up dateCommand и DeleteCommand класса DataAdapter на основе метадан Разработка собственных провайдеров данных для.NET Data Access Framework Ц ных, получаемых из базы данных. И в SqlClient, и в OleDb есть своя вер сия Command Builder, но общего интерфейса или базового класса у этих версий нет.

Типы, применяемые при обработке ошибок При разработке любого API доступа к данным весьма сложно увязать последовательность выполняе мых операций с обработкой ошибок (особенно с учетом их разнообразия).

Как в SqlClient, так и в OleDb реализован набор Errors, содержащий объекты, которые описывают ошибки. Эти типы не являются расширени ями общего базового типа. Однако они реализуют интерфейсы наборов.

Оба провайдера генерируют исключения, специфичные для провайдера, — GleDbException/SqlException. Кроме того, в обоих провайдерах реализо вано событие/делегат для предупреждений и информационных сообще ний — Sql/OleDblnfoMessageEventArgs/Delegate. Поэтому предупрежде ния не прерывают логику выполнения вызывающей программы.

Типы разрешений Провайдеры данных должны соответствовать требо ваниям архитектуры защиты в.NET, а клиентам нельзя разрешать вызовы защищенных функций лишь потому, что они используют высокоуровне вый API. Поэтому в провайдере данных следует реализовать типы Permis sions и PermissionsAttribute. Базовые классы DBDataPermission и DBData PermissionAtribute, на основе которых создаются классы, отвечающие тре бованиям защиты, содержатся в System.Data.Common.

Альтернативные варианты реализации Я создал провайдер данных MdirProv, чтобы исследовать модель провай деров данных в.NET. Огромная часть доступной функциональности в этом провайдере не реализована. А каковы альтернативные варианты?

Является ли провайдер данных лучшим выбором? Причины, по которым к данным обращаются через провайдер, — упрощение обмена данными, интеграция с GUI-компонентами и возможность доступа к данным из сто ронних приложений вроде Crystal Reports.

На момент написания статьи разработчикам было доступно минимум четы ре способа создания провайдеров данных, совместимых с платформой.NET.

.NET-провайдер данных Неплохой вариант, если источник данных (обычно реляционная или какая-то другая база данных) использует соб ственный протокол, с которым можно работать с помощью набора управ ляемых классов. За примерами далеко ходить не надо. Вспомните Sql Client, в котором реализован анализатор TDS (TDS parser), или OleDb, который инкапсулирует API, основанный на СОМ. Такой вариант наибо лее эффективен, когда источник данных допускает обновление и поддер живает локальные транзакции, параметризованные запросы, настраивае ±20 Microsoft ADO,NET мые команды и нестандартные наборы ошибок. В идеале источник данных должен поддерживать множественные табличные наборы результатов (multiple rectangular resultsets). пакеты команд и обновления, управляе мые на основе команд. Заметьте, что сейчас в модели нет поддержки об новлений «по месту» (управляемых не на основе команд) и серверных курсоров.

Нестандартный XmlReader или XPathNavigator Этот вариант особенно хорош, если ваши данные имеют иерархическую структуру (точнее, не таб личную) и требуют доступа на основе навигации (курсоров), а не наборов данных. Для источников данных, оптимизированных под выборку подмно жеств (subsetting), т. е. чтение только части иерархии, отлично подходит нестандартная реализация XPathNavigator. Кроме навигации, можно ис пользовать XPath-запросы и XSLT-преобразования, напрямую работаю щие с данными. Аарон Сконнард (Aaron Skonnard) написал серию классов XPathNavigator (для файловой системы,.NET-сборок и др.), демонстриру ющих такой подход. Они доступны на сайте http://staff.develop.com/ aarons.

«Унаследованный» («legacy») провайдер OLE DB или ODBC-драй вер Хотя в конечном счете такой подход выйдет из употребления, это по-прежнему лучший вариант, когда вам нужно выдавать распределенные запросы к SQL Server, использовать SQL Server DTS, напрямую включать объекты в MSDTC-транзакции и т. д. Оба API реализуют модель курсоров с поддержкой обновления «по месту». Если вы разработаете провайдер OLE DB, то почти со стопроцентной гарантией сумеете интегрировать его с.NET через провайдер данных OleDb.

Программное заполнение DataSet или XmlDocument Используйте с этой целью API, специфичный для ваших данных. Если вы хотите лишь получить набор данных, оба API можно программировать напрямую. Вы не обязаны реализовать навигацию, поддержку обновлений, соединения и команды, если все это вам не нужно.

.NET-класс DataSet можно использовать для обмена данными, предостав ляя функциональность, аналогичную той, которая есть у ADO-объекта Recordset. DataSet можно напрямую задействовать в Web-сервисах, и он поддерживает маршалинг по значению. Но самое интересное — как Data Set осуществляет маршалинг по значению. В отличие от ADO-объекта Recordset, использующего собственный двоичный формат (advanced data tablegram, ADTG), DataSet выполняет маршалинг в формате XML, как и принято в.NET Framework. XML — универсальный формат маршалинга и обмена данными в.NET. Поэтому нет необходимости реализовать провай дер данных только для того, чтобы программно заполнять DataSet.

Разработка собственных провайдеров данных для.NET Data Access Framework Ту же функциональность можно реализовать на основе нестандартного XmlReader или XPathNavigator, который обращается к XML infoset, а не к DataSet. XML infoset — непрозрачное (opaque) представление данных XML-документа в памяти. Зачастую infoset является наиболее эффектив ным представлением XML-документа, Сериализация в формат XML не требуется, поскольку доступ к данным обеспечивается с помощью модели XML infoset. Кроме того, XML infoset лучше подходит для таких нереля ционных данных, как гомогенные и гетерогенные иерархии или полу структурированные данные (данные в структурированном документе).

Учтите, что вы можете не только предоставлять прямой доступ к данным как к XML, но и программно заполнять DataSet. Как и в случае ADO объекта Recordset, нет необходимости использовать модель Connection Command- DataReader.

Visual Studio упрощает интеграцию элементов управления не только с провайдерами данных, но и с любыми источниками данных..NET-элемен ты управления, связываемые с данными, способны работать с любыми типами, реализующими lEnumerable или ICollection. Дизайнеры Visual Studio тоже поддерживают такую интеграцию. Поэтому для поддержки элементов управления создавать провайдер данных не нужно.

В некоторые сторонние программные продукты уже вводится прямая под держка.NET. Так, Crystal Reports.NET, который будет поставляться с Visual Studio.NET, позволяет использовать при построении отчетов клас сы DataSet наряду с ODBC-драйверами и ADO-объектами Recordset.

Заключение Хотя мой провайдер данных не имеет прямого отношения к объектам Connection, Transaction и Parameters или к преобразованию типов источ ника данных в управляемые типы, он позволяет заполнять DataSet. Этого можно было бы добиться и по-другому, но в любом случае этот пример весьма полезен, так как благодаря ему вы изучили архитектуру провайде ров данных.

Боб Бьючмин {Bob Beauchemin) — старший преподаватель в DevelopMentor, занимающийся программированием более 20 лет. Соавтор учебного курса DevelopMentor.NET. Работает над книгой «Essential ADO.NET» для Addison Westey/DevelopMentor. С ним можно связаться по адресу bobb@develop.com.

Джонни Папа Доступ к данным Выражения в ADO.NET Автор рассматривает основы применения в ADO.NET вычисляемых полей и функцию Compute, подсчитывающую агрегатные значения. Также рассказы вается о функциях агрегации в объектах DataColumn, получении итоговых значений, выполнении других вычислений по всему объекту DataSet и связывании объектов DataColumn, принадлежащих разным DataTable. Кроме того, дано несколько практических примеров.

С появлением ADO.NET управлять отношениями в базе данных стало го раздо проще. Вместо получения из хранилища данных одного набора за писей теперь можно считывать группы наборов записей, а затем связывать их в объекте DataSet. Связанные ADO.NET-объекты DataTable имеют ряд преимуществ и, в частности, позволяют хранить данные в виде иерархи ческой структуры, облегчают обновление данных и дают возможность ис пользовать выражения в полях.

Сегодня я расскажу об основах применения в ADO.NET вычисляемых полей (column-based expressions) и функции Compute, подсчитывающей агрегатные значения. Также будут рассмотрены функции агрегации в объектах DataColumn, получение итоговых значений (totals), выполнение других вычислений по всему объекту DataSet и связывание объектов DataColumn, принадлежащих разным DataTable. Кроме того, я приведу несколько практических примеров.

Суммирование и вычисление средних значений по группам связанных за писей в SQL-запросах, вероятно, вам отлично знакомо, поскольку такие функции агрегации, как SUM и AVG, являются частью стандарта ANSI Публиковалось в MSDN Magazine/Русская Редакция. 2003. №1 (январь). — Прим. изд.

Выражений в ADO.NET SQL. SQL также позволяет выполнять вычисления по полям, например умножение цены единицы товара на количество товаров. Благодаря ADO.NET эти возможности доступны не только в источнике данных, но и на промежуточном и более высоких уровнях я-уровневых приложений.

Используя вычисляемые поля, можно создавать свои поля, агрегирующие значения по входящим в DataSet наборам данных, вычислять значения по другим значениям той же записи и даже обращаться через DataRelation к полям родительских или дочерних объектов DataTable. С появлением в ADO.NET вычисляемых полей и функции Compute стали доступны новые способы управления объединением данных.

Конечно, у вычисляемых полей, функций агрегации и функции Compute в ADO.NET есть свои плюсы и минусы. Выражения, указываемые для вычисляемых полей, могут обращаться к полям как одного DataTable, так и двух объектов DataTable одного и того же DataSet, связанных через DataRelation. Я объясню, какие преимущества дают вычисляемые поля в ADO.NET и SQL и в чем между ними разница. Также рассмотрю опера ции, связанные с применением объекта DataRelation. (Об объектах Data Relation см. мою рубрику за ноябрь 2002 г. на http://rasdn.microsoft.com/ msdnmag/issues/02/ll/datapoints/default.aspx.) Здесь же я покажу, как создавать объекты DataColumn, содержащие выражения, применять фун кции агрегации в объектах DataSet и операторах SQL. Кроме того, вы уз наете, как группировать данные и обращаться в дочернем DataTable к по лям родительского DataTable, а также выполнять вычисления по полям наборов данных DataSet с помощью функции Compute.

SQL-выражен ия SQL-выражения бывают разных видов и служат разным целям, в частно сти для форматирования строк, вычисления пользовательских функций и математических расчетов. Если в SQL-операторе объединяются имя и фа милия, вычисляется сумма позиции заказа или содержится пользователь ская функция SQL Server 2000, извлекающая дату заказа, значит, в этом операторе есть выражение.

Выражения очень гибки, позволяя разработчикам считывать в наборы за писей значения, определяемые по другим полям базы данных. В реляци онной и даже в какой-то мере нормализованной базе данных обычно сле дует хранить по каждой позиции заказа не общую сумму, а цену за едини цу и количество соответствующего товара. Сумму нужно вычислять по цене и количеству — это исключает вероятность рассогласования данных.

Так, если хранить в записи количество, цену и сумму, может получиться, что количество равно 10, цена за единицу — $7.00, а сумма почему-то $100.00. Такого быть не должно, но при хранении избыточных данных Microsoft ADO,NET вполне вероятно. При работе с транзакционными базами данных, как пра вило, не хранят информацию, которую можно получить на основе имею щейся (в нашем примере это сумма позиции заказа).

В таких случаях применяются SQL-выражения. Сумму вы получаете, со здав в SQL-операторе вычисляемое поле, где цена умножается на количе ство (рис. 1). Если есть скидка, ее тоже можно учесть при вычислении суммы.

Рис. 1. Математические выражения в SQL USE rjorthwind GO SEUCI QrderlO, :

ProductID, UnitPrice, Quantity, (UnitPrice * Quantity) AS ExtendedPrice, Discount, ((UnitPrioe * Quantity) * (1 - Discount)) AS Extended?riceWithDiscauflt РЙ0Н [Order Details] QROER BY OrderlD, Product!» Следующий SQL-код иллюстрирует конкатенацию строк в SQL-выраже ниях. Здесь из имени и фамилии формируется строка, содержащая имя и фамилию (с буквами верхнего регистра), причем сначала идет фамилия:

USE pubs GO SELECT au_fname AS FirstName, au_lname AS LastName, au_lname + ', ' + au_fname AS FullNamel, (UPPER(au_fnanie) + ' ' + UPPER(au_lname)) AS FullName FROM authors ORDER BY au^lname, au_fname SQL-выражения позволяют форматировать строки и выполнять математи ческие операции, возвращая результаты в наборе записей. Но, используя их в этих целях, остерегайтесь ошибок. Если DataSet заполнен SQL-опе ратором с рис. 1 и, например, в первой записи изменится поле с количе Выражения в ADD,NET ством товара, то вычисляемые поля останутся прежними. Так, если коли чество равно 10, а цена — $7.00 и мы поменяем количество на 5, то в поле ExtendedPrice сохранится считанное из базы данных значение $70.00 ( * $7.00), т. е. нарушится синхронность данных. Таким образом, основная проблема в том, что выражения не «переносятся» из SQL-оператора в ADO.NET-объект DataSet.

Выражения в объектах DataColumn Выражения можно определять и в ADO.NET-объектах DataColumn, созда вая вычисляемые поля. Вместо вычисления суммы в SQL-операторе вы создаете DataColumn, содержащий сумму. Одно из отличий между выра жением в SQL-операторе и выражением в DataColumn заключается в том, что если одно из полей, участвующих в выражении, изменится, Data Column с вычисляемым полем тоже автоматически изменится, тогда как DataColumn с результатом вычисления SQL-выражения (например, пока занного на рис. 1) — нет.

Рис. 2 демонстрирует, как заполнить DataTable в DataSet SQL-оператором, а затем создать DataColumn, содержащий выражение, вычисляемое по другим полям объектов DataTable этого DataSet. Затем представление по умолчанию (default view) этого DataTable связывается с объектом Data Grid — grdOrderDetail.

Рис. 2. Заполнение DataSet и добавление выражений private void LoadGrderDetailDataQ //- Создаем подключение ifSqlConnection oCn * new SqlConnection("Data.. Souree=papa;

" + '-".

"Initial Catalog=rn3rthwind;

User ID=sa;

" + : • "Password-secretpassword;

" ;

) //- Создав*! команду SELECT string sSQL * "SELECT od.OrderlD, od.ProductIO, p.ProductName, od.UnitPrioe, od.Quantity, od.Discount " + " FROM [order details] od ШЕЙ JOIN Products p ON " + "od.ProductlO = p.ProductID " + " ORDEfi BY od.Orderlo, " + "p.ProductNaae ";

SqlCommand oSelCrud = new SqlGoraBand

см. след. стр.

Microsoft ADG.NET Рис. 2. Заполнение DataSet и добавление выражений (окончание) //- Связываем SqlCommand с DataAdapter и заполняем DataSet // SqlDataAdapter oDA = new SqlDataAdapter(oSelCffld);

DataSet oDs * new DataSetO;

aDA,F±ll(oDs, "Orderoetail");

//- Добавляем вычисляемые поля (выражения) // oOs.Tables["OrderPetail").ColuiBns.AtM("ExtendedPrice", typeof(tieeimel), "UnitPrice * Quantity"};

//- Связываем данные с АЗР.Ш"-объектон DataGrid // grdGrderQetail.DataSouree = oDs,Tablest"OrderDetail"J.DefaultView;

QrdOrderDetail.DataBindO;

oDs.DisposeO;

В этом коде создается DataSet, заполняемый информацией о позициях заказа. Затем в DataTable этого DataSet добавляется вычисляемое поле — Extended Price с типом данных decimal. В выражении вычисляется произ ведение полей с количеством и ценой. В таких выражениях можно обра щаться к любому DataColumn объекта DataTable, для которого определе но выражение. Значения полей берутся для текущего DataRow. Например, если в первой записи указаны количество 10 и цена $7.00, то значением поля суммы будет $70,00.

В выражениях могут применяться любые другие поля DataTable, а также литеральные значения. Например, наше выражение, считающее сумму, можно изменить, чтобы учитывалась скидка. Для этого код добавления поля ExtendedPrice надо модифицировать так;

oDs.Tablesf"ОrderDetail"].Columns.Add("ExtendedPrice", typeof(decimal), "(UnitPrice * Quantity) * (1 - Discount)");

Попробуйте изменить значение любого из объектов DataColumn — Unit Price, Discount или Quantity. В отличие от поля с SQL-выражением зна чение DataColumn ExtendedPrice сразу изменится. Эта возможность очень удобна для приложений, работающих с корзиной покупателя: пользова тель вносит в нее изменения, сохраняет их и смотрит, какая сумма заказа получилась.

Выражения в ADO.NET Выражения применяются и для вычисления значений других типов: логи ческих или строковых. Так, в следующем фрагменте показано добавление поля, указывающего, получает ли автор скидку.

oDs.Tables["ОrderDetail"].Columns.Add("GetsDiscount", typeof(bool), "Discount > 0");

Это выражение можно усложнить, создав составное выражение;

в нем про веряется несколько условий, к которым применяются операции AND, OR или NOT В выражениях используются и другие операции, такие как LIKE или IN.

Выражения также позволяют вычислять строковые значения, например, объединять в одну строку содержимое полей DataTable с именем и фами лией. Взгляните, как выполняется конкатенация ProductName и Рго ductlD:

oDs.Tablest"ОrderDetail"].Columns.AddC'stringfield", typeof(string), "ProductID + '-' + ProductName");

.r • Функции Чтобы поле содержало выражение с более сложной логикой, можно задей ствовать функции. В выражениях применяются такие функции, как Len, lif, IsNull, Convert, Trim и Substring. Каждая из них позволяет более гибко создавать выражения. Функция Len вычисляет длину строки:

oDs.Tables["OrderDetail"].Columns.AddC'LengthOfProductName", typeof(int), "Len(ProductName)");

Функция lif, аналогичная Ilf в Visual Basic.NET, — это указываемый в выражении оператор If. Она принимает три аргумента и вычисляет пер вый из них, чтобы выяснить равен он true или false. Если при вычислении первого аргумента получилось true, функция lif возвращает второй аргу мент, в ином случае — третий. Вот как легко вставить в выражение конст рукцию If...Then...Else, упакованную в функцию Ilf;

oDs.Tables["OrderDetail"].Columns.Add{"Inventory", typeof(string), "Iif(Quantity < 10,'A few left', 'Plenty is stock')");

Функция IsNull вычисляет первый аргумент и сравнивает его значение с System.DbNull. Если результат сравнения — false, возвращается значение первого аргумента, в ином случае (т. е. когда первый аргумент равен Sys tem.DbNull) — значение второго аргумента. Это полезно, когда требуется, чтобы вместо NULL-значений были пустые строки или подставляемое значение (placeholder value), как, например, здесь:

Microsoft ADO.NET oDs.Tables["OrderDetail"].Columns,Add("DiscountString", typeof(string), "IsNull(Discount, '[null value]')");

Функция Trim удаляет из строки ведущие и концевые символы пробелов.

Функция Convert изменяет тип данных для значения в выражении, явля ющегося первым аргументом, на тип данных, передаваемый во втором ар гументе. Функция Substring, как и ее аналог в T-SQL, возвращает часть строки (подстроку) передаваемого в нее строкового значения. Substring позволяет урезать слишком длинные строки, чтобы ограничить показыва емое количество символов (см. фрагмент кода ниже). Кроме того, эта фун кция применяется для получения любых других частей строк и при необ ходимости используется в сочетании с другими функциями.

oDs.Tables["OrderDetail"].Columns.Add("ShortProduct", typeof(string), "Substring(ProductName, 1, 10)");

Агрегация и отношения Внедрение функций в выражения позволяет создавать вычисляемые поля с достаточно сложной логикой. А как быть, если требуется создать поле, вычисляющее значения по нескольким записям? Используйте функции агрегации.

Допустим, у вас есть DataSet, в котором связываются таблицы Order и Order Details базы данных Northwind из SQL Server. Создание вычисляе мых полей с выражениями, содержащими функции агрегации, оказывает ся довольно простым делом. В коде на рис. 3 показано создание DataSet, в котором в родительский DataTable помещаются сведения о заказах, а в дочерний DataTable — о позициях заказов. Эти объекты DataTable связы ваются друг с другом через объект DataRelation (Orders2 Order Details). Об объектах DataRelation и о том, как с их помощью связывать иерархические и реляционные структуры данных ADO.NET, см. мою рубрику в «MSDN Magazine» за ноябрь 2002 г.

Рис. 3, Отношение и выражения private void LoadDataO С // --.' //- Создаем подключение // SqlGonnaotioR oCn « new SqlConnection("Data Source^papa;

"Initial Gatalog*northwlnd;

User ID^sa;

" * "PassworcNsecretpassword;

");

см. след. стр.

Выражения в ADQ.NET Рис. 3. Отношение и выражения //- Создаем команду SELECT для заказов // string sSQL = "SELECT GrderlD, CustomerlD, QrdsrDate, ShipCity, ShipCountry " + " FROM Orders " + " ORDER BY CustomerlD, OrderDate DESC ";

.

SqlCofflroand oSelCmd = new SqlContmand{sSQL, oCn);

. oSelCmd.CommaridType = ComfnandType.Text;

//- Связываем SqlCommand e DataAdapter и заполняем DataSet // SqlDataAdapter oOA « new SqlOataAdapter(oSelCmd);

DataSet oBs = new DataSet();

oDA.FilKoDs, "Order");

//- Создаем команду SELECT для позиций заказов // string sSQL « "SELECT od.OrderlD, od.ProductID, p.Producttiame, od.UftitPrice, od. Quantity, od. Discount " + " FROH [order details] od INNER JOIN Products p 0« od.ProductID - p.ProductlS " + " ORDER BY od.OrderlD, p.ProductName ";

SqlCommand oSelCffid = new SqlComniand(sSQL, oCn);

oSelCmd.CoiTiBiandType = CommandType.Text;

I/ //- Связываем SqlCommand с OataAdapter и заполняем DataSet // SqlOataAdapter oDA = new SqlDataAdapter(oSelCmd);

oDA.FillCoDs, "OrderDetail");

//- Связываем объекты DataTable // V \ '_ oDs. Relations. Add("Order20rderDetail", oDs. Tables[ "Order" ]. Columns {"OrderlO"), oDs.Tables["OrderOetail"].ColuiinsC"OrderIO''I);

//- Добавляем выражения для таблицы OrderDetail oDs.Tables["OrderDetail"].Columns.Add("OrderDate", typeof(string), "Parent.OrderDate");

oDs.Tables["OrderDetail"].Columns,AddC'ExtendedPrlce", typeof(decifflal), "(UnltPrice * Quantity) * (1 - Discount)");

oDs.Tablesf'OrderDetail"]. Columns. AddC'GetsDiscount", см. след. стр.

5- 30 Microsoft ADO,NET Рис, З. Отношение и выражения (окончание) typeaf(bool), "Discount > О");

oDs,Taules["OrderDetail"].Columns.Add("stringtest", typeof{string), "ProductID + '-" * ProductName");

oDs.Tablest"OrderDetail"]. Columns. Addt"Len9thOfPrm3«ctH«rfe"f typeof(int), "Len(ProductNaine)");

.

oDs.Tables["OrderOetail"].Columns.AddC"Inventory", typeof(strinff), "lifCQuantity < 10, 'Only a few left', 'Plenty 1л stock')");

oOs, Та bles[ "Order-Detail"], Columns. Add("DiscountString", typeof(string), "IsNull(Discount, '[null value]')");

oDs.Tat>les[''QrderDetair']. Columns. Add("ShortProduct", typeof (string), "Substring(Pro6uctName, 1, 10)+ '...'") //- Добавляем выражения для таблицы Order o0s.Tables["Order"l.Columns.Add("OrderTotal", tyjssof (decimal)* "Suin

oDs.Taules["Orcfer"].Columns.Add("AvgOuantity", typeof(decimal), "Avg(Child(Order2QrderQetail).Quantity)");

Обратите внимание, как на рис. 3 создаются вычисляемые поля, добавля емые в объект DataTable Order. В первом вычисляемом поле — объекте DataColumn OrderTotal — подсчитывается итоговая сумма по всем пози циям текущего заказа. Поле содержит выражение, суммирующее значения другого вычисляемого поля — DataColumn ExtendedPrice объекта Data Table OrderDetail. Так что, как видите, функции агрегации позволяют ра ботать с полями таблиц, с которыми устанавливаются отношения через DataRelation, и даже использовать другие вычисляемые поля.

''* В ADO.NET есть и другие функции агрегации, в том числе Sum, Avg, Max, Min, StDev, Var и Count. На рис. З демонстрируется, как применять фун кцию Avg для вычисления среднего количества по всем позициям заказа.

В таких случаях приходится использовать ключевые слова Parent и Child, чтобы обращаться к данным, доступным через DataRelation:

oDs.Tables["Order"].Columns.AddC'AvgQuantity", typeof(decimal), Avg(Child(Order20rderDetail). Quantity)");

Функция Child принимает в качестве аргумента имя DataRelation, по ко торому определяется дочерний набор записей. Этот необязательный аргу мент необходим, только когда есть несколько DataRelation, связывающих исходный DataTable с дочерними наборами записей. Если у DataTable Выражения в ADO.NET только один дочерний DataTable, можно упростить синтаксис, убрав имя DataRelation, так как имеется только один объект DacaRelation:

oDs.Tables["Order"].Columns.Add("AvgQuantity", typeof{decimal), Avg(Child.Quantity)");

Обращение к полям родительского набора записей и функция Compute Функция Parent работает так же, как Child, но, разумеется, обращается к родительскому DataTable, поднимаясь по цепочке отношений. Эти две функции позволяют реализовать в ADO.NET-коде функциональность, аналогичную GROUP BY в SQL.

Эти ключевые слова позволяют выполнять группирование и передавать между наборами записей значения, не изменяющиеся при переходе от ро дительского набора записей к дочернему. Меня часто спрашивают, как за ново объединить родительский и дочерний DataTable в один DataTable, чтобы показать оба набора записей в одном DataGrid (без иерархии). Ис пользуя ключевое слово Parent, можно из дочернего DataTable обращать ся к полям родительского DataTable и отображать в DataGrid только до черний DataTable. Так, если требуется показать дату заказа для каждой записи DataTable OrderDetail из примера на рис. 3, можно добавить Data Column с ключевым словом Parent:

oDs.Tables["OrderDetail"].Columns.Add("QrderDate", typeof(string), "Parent.OrderDate");

Это позволяет передавать значения из полей родительских наборов запи сей в поля дочерних (roll up and down) без вычислений. Используя клю чевые слова Parent и Child, можно обращаться к полям родительской таб лицы и показывать их в DataGrid. При этом вы получаете единый двух мерный набор записей, аналогичный получаемому из SQL-оператора.

Если же нужно задействовать данные в едином наборе записей, то вполне приемлемо использовать один набор записей, получаемый путем считыва ния всех данных в один DataTable. Но если вы хотите применить реляци онную структуру DataSet, ключевое слово Parent позволит более гибко показывать данные.

Еще одно средство, которое стоит рассмотреть, — функция Compute объек та DataTable, выполняющая вычисления с использованием функций агре гации применительно к текущему DataTable и заданного фильтра. Допус тим, вам нужно вычислить общее количество заказов с итоговой суммой не менее $1000. Ниже показан пример подходящего кода. Первый аргу мент функции Compute — функция агрегации, подсчитывающая число значений OrderTotal, удовлетворяющих условию фильтра:

132 Microsoft ADO.NET //- Показ общего числа заказов с суммой, большей или равной $ int iCnt = (int)oDs.Tables["Order 1 '].Compute("Count(OrderTotal)", "OrderTotal >= 1000");

IblTest.Text = iCnt.ToStrirtgO + " orders are at least $1000";

Второй аргумент Compute — фильтр, ограничивающий круг записей, для которых вычисляется функция агрегации, только записями, отвечающими условию фильтра. Так что подсчитываются записи, для которых Order Total равно $1000.00 и более. Compute, благодаря поддержке фильтрации, отлично подходит для быстрых вычислений по DataTable. Например, вы могли бы легко определить число клиентов, заказавших товар х, и число клиентов, заказавших товар у, — не надо ни писать циклы, ни выполнять запросы к базе данных.

Важно помнить, что объекты DataColumn с вычисляемыми полями нельзя изменять вручную. Эти поля связаны с выражениями, поэтому их нельзя переопределить, не удалив выражение. Кроме того, вычисляемым полям не соответствуют никакие поля в источнике данных — базе данных или XML-файле. Так что, если вам нужно сохранить измененную информацию в базе данных, имейте в виду, что выражения в ней не сохраняются.

В этой статье я рассмотрел выражения SQL и ADO.NET. Вы познакоми лись с обширной функциональностью, обеспечиваемой выражениями, и теперь сможете использовать их в своих приложениях, работающих с дан ными.

Джонни Папа (Johnny Papa) — вице-президент компании MJM Investigations по разработке программного обеспечения (Роли, штат Северная Каролина), автор нескольких книг по ADO, XML и SQL Server. Часто выступает на конфе ренциях, в том числе на VSLive. С ним можно связаться по адресу datapoints@lancelotweb.com.

Дино Эспозито Двоичная сериализация ADO.NET-объектов Автор поясняет, какими способами можно сериализовать объекты ADO.NET.

Однако по-настоящему сериализуемыми являются только DataSet и DataTable, в которых реализован интерфейс ISerializable. В связи с этим в статье показывается, как расширить объекты ADO.NET, чтобы они поддерживали XML-сериалнзацию. Автор рассматривает сериализацию этих объектов как особый случай.NET-сериалиэации объектов в период выполнения и демонст рирует, что за счет переопределения, а не использования суррогатных типов можно добиться значительного повышения скорости обработки ADO.NET объектов форматирующими объектами.

Одно из важнейших преимуществ ADO.NET по сравнению с ADO заклю чается в уровне интеграции с XML. XML в ADO — не более чем формат ввода-вывода. Более того, формат ADO XML совершенно негибок и не поддается никакой адаптации. Поскольку XML в ADO.NET, к счастью, интегрирован гораздо теснее, им можно пользоваться при сериализации ADO.NET-объектов. Такой уровень интеграции делает возможным ис пользование программного интерфейса двойного назначения, который позволяет рассматривать один и тот же набор данных и как иерархичес кий, и как реляционный. В этой статье я сосредоточусь на первом аспек те, т. е. на сериализации ADO.NET-объектов. Если вас интересует второй аспект, начните с характеристик класса XmlDaiaDocument, описанного в документации MSDN.

Сериализацию ADO.NET-объектов можно выполнять двумя функцио нально разными способами. Во-первых, через стандартный механизм * Публиковалось в MSDK Magazine/Русская Редакция. 2002. №6 (декабрь). — Прим. изд.

34 Microsoft ADO.NET Microsoft.NET Framework, основанный на форматирующих объектах (formatters), например с применением форматирующего объекта SOAP (SOAP formatter) или двоичного (binary formatter). Во-вторых, через встроенные методы ADO.NET-объектов, записывающие содержимое объ екта в XML-документ в соответствии с заданной схемой.

Важно отметить, что по-настоящему сериализуемыми являются только ADO.KET-объекты DataSet и DataTable, в которых реализован интерфейс ISerializable. Именно поэтому они доступны любым форматирующим объектам.NET Framework. Однако лишь DataSet предоставляет дополни тельные методы (вроде WriteXral), позволяющие явно сохранять его. со держимое в XML. К счастью, добавить такие методы к другим ADO.NET объектам наподобие DataTable, Data View и DataRow не слишком трудно.

Для начала я покажу, как расширить ADO.NET-объекты, чтобы они под держивали встроенную XML-сериализацию. Затем я рассмотрю сериали зацию этих объектов как особый случай.NET-сериализации объектов в период выполнения, Основы такой сериализации (и многое другое) изло жены Джеффри Рихтером (Jeffrey Richter) в серии статей «Сериализация в период выполнения» из рубрики «.NET»*. Там он показал, как переоп ределить способ, которым тип может сериализовать сам себя. Именно пе реопределение, а не использование суррогатных типов — ключ к значи тельному повышению скорости обработки ADO.NET-объектов формати рующими объектами.

Сериализация объектов DataTable Единственный в пространстве имен System.Data объект DataSet предос тавляет набор методов для своей сериализации и десериализации в XML формат, определяемый классом (class-defined XML format). Лично я назы ваю этот формат нормальной формой ADO.NET XML. Метод WriteXml класса DataSet сохраняет содержимое всех дочерних таблиц и отношений (relations) в различные выходные потоки.

В классе DataTable такого метода для сохранения содержимого в XML нет.

Значит, чтобы сохранить в XML отдельный объект DataTable, не включен ный как дочерний в какой-либо родительский объект DataSet, без трюка не обойтись.

При сохранении объекта DataSet в XML все включенные в него объекты DataTable преобразуются в XML, но соответствующие методы не доступ ны извне. Чтобы обойти это препятствие, просто создайте временный пу См. MSDN Magazine/Русская Редакция. 2002. Спецвыпуск №2, №1(7), №3(9). - Прим. изд.

Двоичная сериализация ADO.NET-объектов стой объект DataSet, добавьте к нему таблицу, а затем сериализуйте его в XML. На рис. 1 приведен статический метод, который принимает объект DataTable и сериализует его в дисковый файл, используя режимы записи, поддерживаемые объектами DataSet. У метода Write DataTable несколько перегруженных версий, по мере возможности имитирующих метод Write Xml объекта DataSet. Входной объект DataTable в коде па рис. 1 включа ется во временный объект DataSet, которому присвоено имя DataTable.

Конечно, в своей реализации вы можете изменить это имя или (что гораз до лучше) разрешить пользователю изменять его. Заметьте, что имя слег ка влияет на конечный XML-вывод. Дело в том, что имя DataSet факти чески представляет корневой узел конечного XML-документа:

DataSet ds = new DataSet("DataTable");

if (dt.DataSet == null) ds.Tables.Add(dt);

else ds.Tables.Add(dt.CopyO);

Рис. 1. Класс MsdnMagActoNetSerializer public class MsdRMagAdoNetSerializer I public static void WriteDataTable( DataTable eft, XmlWriter writer) i WriteDataTable(dt, writer, XmlWriteMode. IgnoreScnema);

public static void WriteDataTableCDataTable dt, Stream stm) ( WriteDataTable( dt, stm, XmlWriteMode. IgnorsSchema) ;

public static void WriteDataTableCDataTable dt, Stream stm, XmlWritettode mode) { DataSet trap = CreateTeiapDataSet(dt);

tmp.WriteXmlCstis, mode);

public static void WriteOataTable( DataTable dt, string output-File) { WriteDataTabIe{dt, output File, XmlWriteHode.IgrtoreScnema);

public static void WriteDataTable(OataTable dt, string outputFils, XmlWriteMode mode) см. след. стр.

Microsoft ADC.NET Рис. 1. Класс MsdnMagAdoNet&erializer (окончание) DataSet trap - CreateTempDataSet(dt);

tmp.WriteXflil(QutputFile, mode);

public static void WriteDataTable{DataTable dt, string outputFile, XmlWriteHode mode) { DataSet tmp - CreateTempDataSet(dt);

tmp.WriteXmKoutputFile, mode);

private static DataSet GreateTempDataSet(DataTable dt) // Создать временный DataSet DataSet ds = new DataSet{"DataTable");

// Убедиться, что этот DataTable // не принадлежит какому-нибудь DataSet if(dt.DataSet == null) ds.Tables.Add(dt);

else ds.Tables.Add(dt.Copy());

return ds:

Также заметьте, что объект DataTable нельзя связать более чем с одним объектом DataSet;

одновременно. Если DataTable принадлежит родитель скому объекту, тогда его свойство DataSet не равно null. В этом случае временный объект DataSet. используемый для сериализации таблицы, сле дует связать с копией таблицы в памяти. Метод Сору просто создает пол ную копию указанного объекта DataTable. При желании для сериализации этого объекта можно реализовать интерфейс ISerializable (см. врезку «Ре ализация ISerializable»).

Библиотеку классов MsdnMagAdoNetSerializer, содержащую перегружен ные версии метода Write DataTable, см. в исходном коде (http://msdn.mic rosoft.com/msdnmag/code02.aspx в разделе за декабрь). В клиентском при ложении эта библиотека используется так:

StringWriter writer = new StringWriterO;

MsdnMagAdoNetSerial:izer.WriteDataTable( table, writer);

// Показать результат сериализации OutputText.Text = writer.ToString();

writer.Close();

Двоичная сериализация ADO.NET-объектов 3, В этом фрагменте кода метод WriteDataTable записывает свое содержимое в строку. Класс StringWriter принимает данные через интерфейс класса записи текста (text writer), а затем ToString возвращает их в виде строки.

Ну и хватит об объектах DataTable. Посмотрим, как сериализовать содер жимое представления, которое находится в памяти (и, возможно, отфиль тровано).

Внутреннее устройство объекта DataView Класс DataView является настраиваемым представлением объекта Data Table. Связь между ними подчиняется правилам стандартной концепции — модели «документ-представление» (document/view model). Б соответ ствии с этой моделью объект DataTable выступает в роли документа, а DataView — представления. В любой момент может существовать несколь ко представлений одних и тех же данных. Важно, что каждое представле ние является отдельным объектом с собственным набором свойств, мето дов и событий. Кроме того, создание представлений не приводит к дубли рованию или репликации данных.

Под капотом DataView тесно связан с конкретным экземпляром класса DataTable. Представление реализовано в виде массива, который содержит индексы строк исходной таблицы, удовлетворяющих заданным для пред ставления критериям. По умолчанию представление не отфильтровано и содержит ссылки на все записи таблицы.

Используя Data View-свойства RowFilter и Row State Filter, вы можете су зить набор строк, попадающих в конкретное представление. Свойство Sort позволяет применять к строкам в представлении выражение, обеспечива ющее сортировку. Внутренняя архитектура объекта DataView представле на на рис. 2. Когда какие-либо критерии фильтрации заданы, DataView получает от нижележащего DataTable обновленный индекс строк, удовлет воряющих этим критериям. Индекс представляет собой массив абсолют ных позиций. Физические объекты-строки не копируются, на них даже не делаются ссылки до тех пор, пока клиент не запросит их. Связь между объектами DataTable и DataView можно установить передачей DataTable конструктору класса DataView:

public DataView(DataTable table);

Кроме того, вы могли бы создать новое представление и связать его с таб лицей позже, с помощью свойства Table объекта DataView:

DataView dv = new DataViewO;

dv.Table = dataSet.Tables["Employees"];

Microsoft ADO-NET Фильтр Массив индексов Выборка Dataflow Dataflow Ссылка на Dataflow Кэш строк Рис. 2. Архитектура DataView Связь между этими объектами на самом деле двусторонняя: вы можете получить объект DataView из любой таблицы благодаря DataTable-свой ству DefaultView. Оно возвращает неотфильтрованный объект DataView, инициализированный для работы с этой таблицей:

DataView dv = dt.DefaultView;

Получаемый таким образом объект представления содержит столько эле ментов, сколько строк в таблице. Обращаться к содержимому этого Data View можно через самые разнообразные программные интерфейсы, в том числе наборы (collections), списки и перечислители (enumerators). В час тности, перечислители позволяют через метод GetEnumerator перебирать записи в представлении, используя привычный оператор foreach. В следу ющем фрагменте кода дехюнстрируется обращение ко всем строкам, вклю ченным в представление:

DataView myView = new DataView(table);

foreach(DataRowView rowview in myView} { // Разыменовать (dereference) объект Dataflow Dataflow row = rowview.Row;

Когда клиентское приложение обращается к конкретной строке в пред ставлении, класс ожидает, что она находится во внутреннем кэше строк (рис. 2). Строка, уже присутствующая в кэше, упаковывается в промежу точный объект DataRowView и возвращается клиенту. DataRowView — это оболочка ссылки на объект DataRow, содержащий реальные данные. Data RowView служит программируемым интерфейсом, который управляет объектом строки и ссылается на него как на единое целое. При необходи мости вы можете обратиться к нижележащему объекту DataRow из экзем пляра DataRowView через свойство Row представления.

Если строки, запрошенной клиентом, во внутреннем кэше нет, класс Data View загружает ее из исходной таблицы. Точнее, кэш строк DataView очи Двоичная сериализация ADO.NET-объентов щается и заполняется при первом запросе. Этот кэш может оказаться пус тым либо потому, что еще ни разу не использовался, либо потому, что вы ражение сортировки или критерии фильтрации изменились. Всякий раз, когда фильтр или выражение сортировки изменяется, кэш очищается, Если какая-либо строка запрашивается впервые и кэш пуст, Data View быстро заполняет его массивом объектов Data Row View, каждый из кото рых ссылается на исходный объект DataRow.

Сериализация объектов DataView Расширим класс MsdnMagAdoNetSerializer, приведенный на рис. 1, и включим в него перегруженные методы для сериализации объекта Data View, Идея в том, чтобы создать копию исходного DataTable со строками, которые удовлетворяют критериям представления (рис. 3).

Рис. 3. Копирование DataTable public class MsdnHagAdoNetSerlalizer public static void WriteDataViewC DataView dv, string outputFile, XmlWriteMode mode) { OataTable dt = CreateTefflpTable(dv);

WriteDataTable(dt, outputFile, aode);

private static DataTable CreateTefpTable(DataView dv) { // Создать временный DataTable, структура которого // совпадает со структурой оригинала DataTable dt * dv. Table, CloneO;

// Заполнить DataTable всеми строками представления foreach

return dt;

Сначала создается временный DataTable с той же структурой, что и у таб лицы, для которой создан сохраняемый объект DataView. Затем временная копия заполняется строками, на которые ссылается представление. Нако нец, таблица сериализуется в XML вызовом уже определенных методов WriteDataTable.

Microsoft ADO,NET j_4Q DataTable-метод ImportRow играет ключевую роль в коде на рис. 3, созда вая новый объект DataRow в контексте таблицы, для которой он вызван.

На DataRow, как и на другие ADO.NET-объекты, не могут ссылаться сра зу два объекта-контейнера. Применение ImportRow логически эквивален тно копированию строки и добавлению ее клона к таблице в виде ссылки.

Этот метод сохраняет все значения свойств — исходные и текущие. В от личие от метода NewRow, который добавляет новые строки со значения ми по умолчанию, ImportRow сохраняет текущее состояние строки.

Сериализация объектов DataRow Два уже рассмотренных примера демонстрируют стандартный способ со хранения ADO.NET-объектов в XML. Его суть в создании иерархии роди тельских объектов — от сериализуемого до DataSet, Так, чтобы сериализо вать один автономный объект DataRow, вам потребуется добавить его к временному DataTable, который в свою очередь надо добавить к рабочему DataSet.

ADO.NET-сериализация в период выполнения Как я уже упоминал, есть два базовых способа сериализации ADO.NET объектов: через собственный XML-интерфейс объекта, если таковой име ется, или с помощью стандартных в.NET Framework форматирующих объектов. До сих пор я рассматривал методы для сериализации ADO.NET объектов в XML, в том числе API-расширения, необходимые для поддер жки этой функциональности объектами, отличными от DataSet. Теперь обсудим, как сериализовать ADO.NET-объекты, используя стандартный в.NET Framework механизм сериализации объектов в период выполнения.

Главное отличие между методами вроде WriteXml и форматирующими объектами.NET Framework в том, что в первом случае объект сам управ ляет своей сериализацией. С другой стороны, когда в дело вступают фор матирующие объекты, у каждого сериализуемого объекта есть два пути.

Он может просто объявить себя сериализуемым и безучастно взирать на то, как форматирующий объект будет извлекать все значимые данные, подлежащие сериализации. Такой тип сериализации поддерживается вклю чением атрибута [serializable] в каждый класс индивидуально. Для пере числения всех свойств, отвечающих за состояние объекта, в процессе се риализации используется API отражения (reflection), поддерживаемый.NET Framework.

Второй вариант требует реализации интерфейса ISerializable;

тогда сериа лизуемый объект отвечает за передачу форматирующим объектам всех данных, которые нужно сериализовать. Однако, как только объект переда ет эти данные, он теряет контроль над процессом. Класс, не помеченный Двоичная сериализация ADO.NET-объектов атрибутом [serializable] и не реализующий интерфейс ISerializable, сериа лизовать нельзя. Так вот, никакие классы ADO.NET не объявляют себя сериализуемыми, а упомянутый интерфейс реализуют лишь DataSet и DataTable. Например, объект DataColumn или DataRow сериализовать нельзя. Однако, как я уже говорил, обойти это препятствие иногда позво ляют суррогатные типы;

при этом сравнительно простые несериализуемые типы можно сделать сериализуемыми. Но сначала вспомним основные аспекты.NET-сериализащш в период выполнения.

Pages:     | 1 || 3 | 4 |   ...   | 6 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.