WWW.DISSERS.RU

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

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

Pages:     | 1 || 3 | 4 |

«Предисловие Криса Селлза Development ADDISON Series WESLEY Практическое использование ADO.NET Доступ к данным в Internet Шон Вилдермьюс Практическое использование ADO.NET Pragmatic ADO.NET for ...»

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

или Общаясь с я часто слышу вопрос: "Какой подход является наиболее пра Ответ на него целиком и полностью от предметной области. Объект DataReader представляет собой однонаправленный курсор, предназначенный для чтения дан ных. В контексте создания на основе информации, хранящейся в базе данных, тую именно его использование является наиболее оптимальным решением. Однако если мы будем полагаться лишь на объект DataReader, нам, возможно, потребуется создать собствен ный контейнер, подобный DataSet, предназначенный для хранения считанной строки данных.

Объект DataSet, с другой стороны, является отличным способом хранения информации, извлеченной из данных. Тем не менее его заполнение связано с накладными расходами и негативно влияет на производительность системы. Если вам необходимо манипулировать /56 Часть Класс DataSet Переопределив мы пользователей применять для уста новки отдельных значений свойства, что позволяет не дублировать бизнес-логику или перенаправлять вызовы через свойства.

В рассмотренных выше примерах классы порождались от классов DataTable и DataRow, таблице счетов. Однако наш типизированный класс DataSet имеет также обеспечивающие безопасность типов классы DataTable и DataRow, соответствующие таблице клиентов. Поскольку к ним не добавляется биз нес-логика, они не порождают новых классов. Другими при наследовании типизированного класса DataSet нет необходимости наследовать все его классы DataTable, а достаточно наследовать только те классы, к которым необходимо доба вить Новые классы создавались нами путем ручного редактирования сгенерированного кода. Поскольку сгенерированный код может изменяться, обычно такие действия не приветствуются. Для того чтобы облегчить наследование, Крис (Chris Sells) и я создали дополнительный модуль для Visual Studio, стандартный генера тор типизированных классов DataSet и все необходимые изменения в ге нерируемый код. При желании вы можете загрузить этот модуль с моего Web-узла по адресу:

Резюме Безопасность типов — это отличная штука! Создавая типизированные классы DataSet, мы сокращаем количество возможных ошибок и ненужных обращений к базе данных. В этой главе описано создание типизированных классов DataSet с помощью Visual Studio и посредством инструмента командной строки. Мы рассмотрели так же способы уточнения генерируемых классов с помощью специальных аннотаций.

Типизированные классы DataSet позволяют реализовать новую парадигму про граммирования, в соответствии с которой генерируемые классы могут включать в себя приложения. Код, генерируемый инструментами Microsoft, не совсем удовлетворяет такой парадигме, однако вы всегда можете воспользоваться инструмен том, размешенным на моем Web-узле.

Глава 6. Типизированные классы DataSet set if (value > t throw new "Invoice Date Cannot in the future", I else I = value;

Наша бизнес-логика предполагает проверку даты счета (не указывает ли она на бу дущее). этой логики в свойство позволяет гарантировать, что пользовате ли не смогут установить дату счета неправильно, но так как базовым классом является класс DataRow, нам необходимо удостоверится, что пользователи не смогут устано вить дату счета с помощью индексатора (который пропускает вызов нашего свойства).

Избежать этого можно посредством переопределения индексатора, сделав его предна значенным только для чтения (листинг 6.19).

Листинг 6.19. Защита public class :

" public class :

// только public new object index] get f return f 154 Часть II. Класс DataSet I { } I public new InheritedlnvoiceRow XnvoiceDate, string Terms, string FOB, string PO, public InheritedlnvoiceRow Для начала мы создаем новый индексатор, который будет возвращать экземпляр нового класса DataRow вместо экземпляра базового класса DataRow. После этого соз даются методы AddlnvoiceRow () и NewInvoiceRow () (созданные в типизированном классе DataSet), которые также возвращают экземпляр нового класса DataRow. Те перь порожденный класс будет обеспечивать безопасность типов и полностью насле довать базовый класс.

После выполнения "технической" работы в порожденный класс DataRow можно поместить необходимую ( листинг 6.18).

Листинг 6.18. Добавление бизнес-логики public class :

public class InheritedlnvoiceRow : InvoiceRow public new DateTime InvoiceDate get return Глава 6. Типизированные классы DataSet В случае наследования типизированного класса DataRow нужно реализовать только один конструктор, принимающий в качестве параметра объект Этот конструктор используется методом который вы зывается объектом Table при создании новой строки. При вызове данного базовый класс обеспечивает безопасность типов. Для того чтобы это работало, необ ходимо переопределить методы класса DataTable и GetRowType так, как показано в листинге Листинг создания новых экземпляров порожденного public class InheritedTDS :

public class :

protected override DataRow builder) i return new I protected override i return Это позволяет удостовериться, что все строки, созданные в объекте DataTable, имеют тип порожденного класса (такой, как InheritedlnvoiceRow). Нам необходи мо также, чтобы объект DataTable работал с новыми объектами DataRow, а не с их в базовом классе. Для того чтобы это сделать, нужно создать несколько новых методов, как показано в листинге 6.17.

Листинг Переопределение метода создания объектов public class InheritedTDS : FixedCustomerTDS • public class : InvoiceDataTable public new InheritedlnvoiceRow index] 152 Часть II. Класс Теперь при попытке обратиться к объекту DataTable и добавить новый счет можно гарантировать, что клиент имеет достаточный кредит;

в противном случае будет сге нерировано исключение. Реализация бизнес-логики на уровне таблицы может ока заться недостаточной. Вполне возможно, нам придется реализовать определенный контроль также и на уровне строк. Для того чтобы удостовериться в том, что ни один из пользователей не сможет создать счет, датированный будущим числом, необходимо добавить к свойству класса бизнес-объект (листинг 6.14), Листинг 6.14. Защита свойства InvoiceDate public new DateTime InvoiceDate get f return set { if (value > { return new Date" + "cannot be in the" I else { = value;

} Чтобы это необходимо наследовать типизированный класс как показано в листинге 6.15.

Листинг Наследование класса public class :

I public ;

public Глава 6, классы 75/ Последний шаг заключается в создании нового свойства, скрыть свойство DataTable базового как показано в листинге 6.12.

Листинг 6.12. Сокрытие свойства public class : FixedCustomerTDS public new Invoice I get return Создав новое свойство Invoice, мы не только скрываем свойство invoice базового класса, но также гарантируем, что весь код, ссылающийся на свойство Invoice, будет ра ботать с версией класса DataTable, производной от типизированного класса Как только внесение изменений мы помешаем бизнес-логику в класс потомок типизированного класса DataTable, как показано в листинге 6.13.

Листинг 6.13. Добавление к производному классу public class InheritedTDS ;

FixedCustomerTDS public class :

t public void row) { if ( else I throw Invoice cannot created, + credit I I 150 Часть Класс Листинг Изменение поведения объекта по умолчанию // public FixedCustomerTDS : DataSet private void /* = new */ // Новый = protected info, StreamingContext /* Исходный new */ // Новый Нам необходимо заменить два фрагмента кода, в которых создается экземпляр типизированного класса DataSet. Каждый из этих фрагментов встречается в сге нерированном коде два раза. Первый фрагмент включает в себя оператор new который необходимо заменить на а блицы второй оператор который необходимо заменить на В предыдущем примере оба фрагмента показаны в исходном и измененном виде.

Глава 6. Типизированные классы DataSet Метод возвращает типизированный объект DataTable.

Если мы хотим наследовать типизированный класс DataSet, нам придется переопре делить данный метод и возвратить тот же самый типизированный объект Это позволяет удостовериться в том, что наш порожденный класс вительно наследует типизированный класс DataTable. Поскольку сгенерированный код будет содержать зависящий от типизированного класса метод, написанный таким образом, позволяет гарантировать работоспособность сгенериро ванного кода.

Далее нам необходимо переопределить этот метод в случае наследования от типи зированного класса DataSet. Так как метод CreatelnvoiceDataTable является виртуальным (переопределяемым в то при создании экземпляров класса DataTable типизированный объект DataSet будет вызывать нашу версию метода CreatelnvoiceDataTable. Итак, создадим класс-потомок типизированного класса DataSet и переопределим метод создания таблицы, как показано в листинге 6.10. метода создания таблицы public class :

{ public :

I } protected context) : context) XnvoiceDataTable table) I if (table null) ( return as I { return new as I I Данный метод очень похож на метод базового класса, за исключением того что в нем создается экземпляр типизированного класса DataTable, кото рый возвращается как экземпляр базового класса.

После этого необходимо модифицировать сгенерированный код для замены всех вызовов конструкторов типизированного класса DataTable на вызов созданного нами метода (листинг 148 Часть Класс DataSet Листинг 6.8. Наследование public class : FixedCustomerTDS \ public InheritedlnvoiceDataTable :

{ internal :

internal I Наследование типизированного класса DataTable также требует создания двух кон структоров. В качестве параметра второй конструктор принимает объект DataTable и используется для Создать класс InheritedlnvoiceDataTable было несложно, гораздо труднее будет заставить типизированный класс DataSet ис пользовать полученный класс вместо класса InvoiceDataTable. Сгенерированный код это усложняет, так как схема всего класса (включая класс DataTable) была опре делена на этапе создания экземпляра базового класса. Это означает, что для того что бы заменить старый класс DataTable новой версией, нам придется заново создать большую часть схемы, которая находится в конструкторе. Этого можно избежать, если слегка изменить сгенерированный кол одним из нескольких способов.

можно добавить новый виртуальный метод, нужную нам кото рый будет вызываться на этапе создания типизированного объекта DataSet, как пока зано в листинге 6.9.

Листинг 6.9. Добавление метода создания таблицы // Сгенерированный класс DataSet.

public class FixedCustomerTDS : DataSet protected virtual InvoiceDataTable table) if null) return new else return new Глава 6. классы DataSet // от класса DataSet, dataset = // Использование объектов DataAdapter // для заполнения DataSet, // Выполнение следующего кода к генерированию // исключения, как дата создания счета invoice;

Invoice = new Использование типизированного класса DataSet идентично ис пользованию его родителя. Ну а поскольку мы зарегистрировались для получения уведомлений о событиях, возникающих при проведении определенных это позволяет нам бизнес-логику. В данном случае при добавлении нового счета его дата намеренно смещается на четыре дня в будущее, что приводит к генери рованию исключения с уведомить пользователя об ошибке.

Наследование типизированного класса DataSet Второй подход заключается в том, чтобы породить не только класс-потомок типи зированного класса DataSet, но и классы-потомки типизированных классов DataTable и DataRow. Это позволяет переопределить любое поведение при реализа ции бизнес-логики. Отметим, что наследовать каждый класс DataTable и DataRow не обязательно, достаточно сделать это только для тех классов, которые нуждаются в реа лизации специальной логики. При этом если нам необходимо наследовать класс DataRow, то придется наследовать и его родительский класс DataTable.

В приведенном ниже примере разрабатывается логика для Invoice, пре дусматривающая проверку кредита перед добавлением в нового счета. В итоге наша логика должна выглядеть так, как показано в листинге 6.7.

Листинг 6.7.

public void row) ( if •;

i throw new "Customer Invoice cannot be + "no credit } !• Для того чтобы иметь возможность определить подобную логику, необходимо на чать с наследования типизированного класса как показано в листинге 6.8.

146 Часть II. Класс DataSet protected info, context) :

context) { Register private void new private void source, InvoiceRowChangeEvent { if • if > throw Invoices" + "in I I В приведенном выше листинге создаются новый класс-потомок типизированного класса DataSet и два его конструктора, Первый конструктор предна значен для обычного создания экземпляра класса, а второй — для десериализации эк земпляра класса из документа XML. Поддержку обеспечивает конструктора десериализации. Кроме вызова конструктора базового клас са, все, что необходимо при этом сделать, — это вызвать метод Register позво ляющий зарегистрироваться для получения уведомлений об нас собы тиях. В данном случае мы регистрируемся для получения уведомления о событии которое срабатывает перед фактическим изменением стро ки. В методе-обработчике (InvoiceChanging) производится проверка того, была строка изменена или добавлена в таблицу. В каждом из этих случаев необходимо убедиться в том, что дата счета не на иначе будет сгенерировано исключение.

Использование нового типизированного класса DataSet показано в листинге 6.6.

Листинг 6.6. Проверка в управляемом событиями классе DataSet // Создание объекта DataAdapter для каждой // извлекаемой из базы данных.

SqlDataAdapter = new * FROM // Создание объекта DataAdapter для таблицы Invoice.

SqlDataAdapter = new * FROM INVOICE", // порожденного непосредственно Глава 6. классы DataSet объект DataSet с несколькими таблицами и отношениями, вы тем самым автоматически определяете реляционно-иерархическое отображение. Приняв единственную строку единственной таблицы за вершину объектного вы можете перемещаться по отношениям для получения дерева элементов. Это очень похоже на создание уровней бизнес-объектов с помощью слияний нескольких таблиц для получе ния объектного графа базы данньгх. Разница заключается в том, что в объекте DataSet и в типизированном объекте DataSet перемещаться по графу можно только по направле нию к связанным строкам связанных таблиц. Таким образом, типизированный класс DataSet избавляет программиста от необходимости создавать отображение, потому что оно является неотъемлемой частью самого класса DataSet.

Следующая задача программиста заключается в создании кода для манипулиро вания базой данных. Так как в класс DataSet от кода управления базой данных (обычно подобный код сконцентрирован в управляемом задача программиста существенно упрощается. Несмотря на то что вам все еше может понадобиться создавать хранимые процедуры для операций уда ления, создания, обновления и чтения (Create, Read, Update, Delete — свя зывая их с объектом (см. главу 8, "Обновление базы данных"), в целом всю основную работу по взаимодействию с базой данных берет на себя Таким образом, нам остается разработать только бизнес-логику, что, на мой взгляд, является самой простой частью всего задания. В некоторых системах бизнес логика — это просто проверка данных на достоверность, в то время как в других сис темах сложность бизнес-логики сравнима со сложностью интеграции и синхрониза ции нескольких различных систем. На самом деле бизнес-логика — это любая логика, которая должна быть применена к информации, хранящейся в базе данных. При этом возникает вполне законный вопрос: если мы используем типизированный класс DataSet для чтобы избавиться от необходимости создавать уровень бизнес логики, куда мы ее должны поместить? На мой наиболее место для бизнес-логики — это класс, порожденный непосредственно от типизированного класса DataSet.

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

Управляемый событиями класс DataSet При небольшом объеме бизнес-логики можно создать класс-потомок типизиро ванного класса DataSet и зарегистрировать его для получения уведомлений о собы тиях. Из всего множества различных событий класса DataSet особый интерес пред события и RowChanged (листинг 6.5).

Листинг 6.5. Получение уведомлений о событиях из типизированного public class :

{ public : base { Register () /44 Часть Класс DataSet new * FROM INVOICE", // Создание типизированного объекта DataSet.

CustomerTDS typedDS new () // Заполнение DataSet с помощью объектов // Вывод адреса и счетов для каждого cuatRow in I Console, :

? :

+ I После заполнения типизированного объекта DataSet с ним можно точно так как и с обычным объектом DataSet (потому что он является экземпля ром класса, непосредственно порожденного от класса DataSet). Как только объект будет заполнен данными, мы можем использовать типизированные методы доступа к строкам и столбцам, причем это будет делаться более коротким путем, чем в случае использования обычного (не типизированного) класса DataSet.

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

Упрощение уровня бизнес-объектов В течение своей профессиональной карьеры я занимался разработкой уровней дольше, чем любой другой работой. Создание уровня бизнес объектов обычно включает в себя несколько задач:

• отображение реляционной модели на иерархическую • чтение и запись объектов в базу данных;

• добавление бизнес-логики для удовлетворения требований к данным.

Самыми сложными аспектами всей работы являются отображение реляционной модели на иерархическую а также написание кода для манипулирования базой данных, К счастью, использование типизированных классов DataSet позво ляет избавиться от необходимости самостоятельно разрабатывать этот код. Создав Глава 6, классы DataSet Проанализируем попытавшись разобраться в том, куда и зачем были вставлены аннотации.

1. Первое, что необходимо сделать, — это разместить ссылку на пространство имен в заголовке схемы. Таким образом мы об определении про имен и разрешаем использование атрибутов.

2. После этого мы переходим к элементу Customer. Атрибут typedName исполь зуется для изменения имени типизированного объекта Row в объекте DataTable на Другими словами, при запросе отдельной строки объекта DataTable вместо экземпляра класса Customer будет экзем пляр класса OurCustomer. Аналогично, атрибут typedPlural используется для изменения имени свойства объекта DataSet, получить доступ к конкретному объекту DataTable. Другими словами, при необходимости полу чить таблицу Customer нужно обращаться к свойству 3. Далее мы добавили атрибут к элементу MiddleName. Это указывает на необходимость интерпретации значения в элементе MiddleName как пустой строки в типизированном объекте DataSet.

4. Изменения, внесенные в элемент Invoice, совпадают с изменениями, внесен ными в элемент Customer. Так, класс был переиме нован в а свойство, которое экземпляр этого клас са, — в 5. Наконец, мы изменили имена из сторон нашего отношения с помо щью дескриптора и атрибутов и typedChildren. Теперь класс Consumer будет иметь метод invoices (вместо по перемещаться вниз по отношению, а класс метод Customer (вместо GetCustomerTable), позволяющий перемещаться вверх по отношению.

Использование типизированного класса DataSet В основном типизированный класс DataSet можно использовать везде, где можно использовать обычный класс DataSet. Поскольку типизированный класс DataSet непосредственно наследует класс DataSet, все связанные с ним методы и классы бу дут работать как ни в чем не бывало. Одним из самых использования типизированного класса DataSet является сильная типизация и стро гое именование элементов объекта Использование нового класса несколько отличается от использования класса DataSet (листинг 6.4).

Листинг 6.4. Использование типизированного DataSet // Создание объекта DataAdapter для таблицы Customer, new * FROM conn) // объекта DataAdapter для таблицы Invoice.

SqlDataAdapter = 142 Часть II. Класс DataSet • При попытке доступа к значению DBNull генерируется исключение (codegen: Если не указано обратное, это поведение принято по умолчанию.

• При попытке доступа к значению DBNull указатель null. Если приложение ожидает получение значения структурного типа (value type), генерируется исключение.

• При попытке доступа к значению DBNull ссылка, соз данная конструктором пустого объекта (к примеру, для строк). Для структурных типов генерируется исключение.

Эти аннотации должны быть добавлены непосредственно в файл Если вы используете Visual Studio то для внесения изменений в файл перейдите в режим XML-представления. Ниже приведен пример в файле ?> > /> ice element string" 30" /> /> /> Глава 6. Типизированные классы DataSet В результате выполнения этого фрагмента кода будет создан файл который можно передать на вход программе Для того чтобы создать типизи рованный класс DataSet, перейдите в командную строку и воспользуйтесь инстру ментом ехе так, как показано на рис.

6.17. Запуск программы XSD.exe из строки Параметрами, интерес, являются следующие.

• указывает на необходимость создания класса, производного от класса DataSet. Этот параметр указывается каждый раз при создании ного класса DataSet.

• /1 — определяет язык программирования, использующийся при генерировании классов. CS соответствует С#, VB — a JS — • /n — определяет пространство имен, в котором должны находиться мые классы. Этот параметр является необязательным.

• /о — определяет каталог, в который необходимо поместить файлы. Если вы используете этот параметр, то учтите, что каталог, в котором будут размещаться выходные файлы, должен инструмент XSD.exe не сможет создать каталог за вас. Если выходной каталог не указан, классы будут созданы в текущем каталоге.

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

Настройка сгенерированного кода с помощью специальных аннотаций позволяет управлять некоторыми аспектами сгенерированного с помощью добавления в XSD-файл, для генерации типизированного класса DataSet, Для этого в XSD-файл нужно добавить не сколько атрибутов, имеющих префикс • — определяет имя объекта;

• имя коллекции объектов;

• typedParent — определяет имя родительского отношения;

• typedChildren — определяет имя дочернего отношения;

указывает способ обработки значения DBNull для заданного столбца;

этот атрибут имеет несколько возможных значений.

• Значение по умолчанию. которое будет вместо DBNull по умолчанию пустая строка — codegen 140 Часть II. Класс DataSet Использование программы XSD.exe для создания типизированного класса DataSet С инструмента командной строки XSD.exe класс DataSet можно создать непосредственно из файла.XSD (XML Schema Definition — определение схемы XML). Программа XSD.exe входит в состав набора инструмен тальных средств разработки (SDK) Framework. Процесс создания типизирован ного класса DataSet с помощью программы по своей сути практически ни чем не отличается от создания типизированного класса в Visual Studio Если у вас нет Visual Studio или вы отдаете предпочтение инструментам ко мандной строки, то программа подходит как нельзя лучше. Если же вы ис пользуете Visual Studio то реализованная в ней поддержка типизированных классов DataSet поможет вам создать те же самые классы, затратив на это намного меньше усилий.

Программа используется для исходного кода на основе файлов — как классов, наследующих классы XML, так и классов, семейство классов DataSet. Перед тем как начать создание типизированного класса DataSet, нам необходимо получить документ XSD, для создания которого можно воспользоваться объектом DataSet, как показано в листинге 6.3.

Листинг 6.3. Создание на основе объекта // объекта DataAdapter для каждой таблицы, // извлекаемой из базы данных.

= new * FROM CUSTOMER", Создание пустого объекта DataSet.

DataSet dataSet = new // Заполнение объекта DataSet с помощью объектов DataAdapter.

// переменной для упрощения // доступа к таблице Customers.

DataTable = // Уточнение схемы.

- true;

= true;

50;

false;

50;

= false;

50;

= "MA";

2;

= который будет // для типизированного класса DataSet.

Глава 6. Типизированные классы DataSet Выполнение команды key...

in Г Г Определение имени ключа Рис. ключа Если на данном этапе попросту ввести имя и щелкнуть на кнопке ОК, то будет создано ограничение уникальности. Однако возможны случаи, в которых необходимо определить ключ, состоящий из нескольких Для того чтобы это сделать, не обходимо добавить входящие в ключ поля (рис. 6.16).

Созданное ограничение заставит объект DataSct проверять уникальность всех ком бинаций значений полей и Теперь вас можно поздра типизированный класс DataSet полностью определен!

Перед тем как просмотреть код, сгенерированный Visual Studio осуществить сборку проекта. После этого вновь созданный типизированный класс будет доступен для использования.

138 Часть II. Класс с созданным отношением ffinAm V Si - 0! я i.

i. FOB ) В XML as!

6.13. Добавление вычисляемого столбца Глава 6. Типизированные классы В диалоговом окне Edit определяются особенности отношения. Из рас крывающегося списка Child element (Дочерний элемент) выберите дочернюю таблицу (в данном случае таблицу Invoice). Обычно Visual Studio корректно определяет поля внешнего ключа. При необходимости вы можете правила каскадного обновления объекта с помощью раскрывающихся списков Update Delete rule и После внесения всех изменений диалоговое окно Edit Relation должно выглядеть примерно так, как показано на рис.

То в and the ft* окно Edit Relation после внесения всех изменений После щелчка на кнопке ОК вы увидите вновь созданное отношение (рис.

Добавив две таблицы и отношение между ними, добавим к таблице вычисляемый столбец. Для того чтобы это сделать, перейдем в конец таблицы Customer и добавим новый столбец В свойствах этого столбца опреде лим его выражение:

+, ' FirstName В результате все должно выглядеть так, как показано на рис. 6.13.

Наконец, добавим ограничение уникальности для столбца чтобы быть уверенными в том, что домашние телефоны наших клиентов не повторяются. В кон тексте типизированного класса DataSet это будет означать добавление ключа. Выбе рите столбец, щелкните на нем правой кнопкой мыши и выполните команду key... ключ), как показано на рис. 6.14. На экране поя вится диалоговое окно Edit Key (Редактирование ключа), показанное на рис.

II. Класс DataSet Начнем с добавления отношения между таблицами Customer и invoice. Пере тащите новый элемент Relation из панели Toolbox в таблицу (рис. 6.9).

На экране появится диалоговое окно Edit Relation (Редактирование отношения), как показано на рис. 6.10.

It I* «ring Las tiring string -• о Terms ED 6.9. Добавление отношения To the child child field to each parent fiefat OK ] /ft окно Edit Глава 6. классы Project ftJd ' • •. Debug - :

О To ftem of surface, fe-JKT ОТО j Окно Server Explorer ДО a CD Ш. в 134 Часть II. Класс DataSet Praiea Typed. I ' ' an Добавление класса file • Widow Пустая основа класса DataSet На этом этапе новый типизированный класс DataSet включает в себя две табли цы, но между ними отсутствует отношение. К сожалению, Visual Studio даже не пытается получить полную о схеме, которая содержится в базе данньгх.

Для того чтобы уточнить информационную схему объекта DataSet, ее необходимо до бавить в файл вручную.

6. Типизированные классы DataSet / Deployment ASP NET Создание нового проекта Add frihented Save V TOCO:

Add to Add Добавление нового элемента После добавления к проекту типизированного класса DataSet вы можете с по диалогового окна Server Explorer (Обозреватель добавить к нему таб лицы, а с помощью панели Toolbox (Панель инструментов) — элементы схемы. Ис пользование диалогового окна Server Explorer показано на рис. 6.7.

После того как в окне Server Explorer будет выбрана существующая база данных, перетащите все необходимые таблицы в файл В итоге у вас должно получится нечто, подобное что показано на рис. 6.8.

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

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

Типизированный класс DataSet наряду с обычными столбцами может содержать вычисляемые столбцы, что также позволяет обеспечить безопасность типов. В преды примере мы можем обратиться к свойству FullName таблицы Customer, где FullName — вычисляемый столбец, в котором объединяются имя и фамилия клиента.

Так как указанное свойство является частью типизированного класса DataSet, ре зультат вычисления выражения возвращается в виде строки.

Наконец, как будет показано далее в этой главе, использование типизированных классов DataSet в качестве основы для уровней объектов данных или бизнес объектов является чрезвычайно мощным инструментом. Непосредственное наследо вание от типизированного класса DataSet позволяет избавиться от необходимости самостоятельно разрабатывать эти уровни и обеспечить безопасность типов.

Создание типизированного класса DataSet В данном разделе мы попытаемся создать свой собственный типизированный класс DataSet. Это можно сделать двумя способами: с помощью инструмента командной строки или посредством Visual Начнем со второго способа.

Использование Studio для создания типизированного класса DataSet В этом разделе описывается создание типизированного класса DataSet с помощью средств среды разработки Visual Studio Для начала создайте новый проект кон сольного приложения С# (рис. 6.3). После этого с помощью диалогового окна Solution Explorer (Обозреватель решения) добавьте к проекту новый элемент (рис. 6.4).

Из вложенной в папку Local Project Items (Элементы локального проекта) папки Data (Данные) выберите элемент DataSet и назовите его (рис. 6.5). Если на этом этапе добавить XML-схему (которая также является файлом то типизированный класс DataSet создан не будет. нам нужно добавить именно класс DataSet. После этого окно Visual Studio будет выглядеть примерно так, как показано на рис. 6.6.

Легко заметить, что типизированный класс DataSet имеет расширение. xsd. Это связано с тем, что исходный код представляет собой Другими словами, все, что требует от пользователя Visual Studio — это создать новый файл со держащий схему объекта DataSet. Более подробно схема объекта DataSet рассматривается в главе 5, "Создание объекта DataSet". Файл может включать в себя имена таблиц и столбцов, а также ключи, отношения и ограничения.

Глава 6. классы DataSet // Заполнение объекта DataSet с помощью объектов DataAdapter.

// Вывод имени клиента на ) ;

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

// код не будет выполняться, так как // поле хранить только // целочисленные значения.

"15234";

Для доступа ко всем уровням иерархии объекта DataSet (вплоть до строк) необхо димо использовать индексаторы. В любой момент можно неправильно написать ка кое-то имя, в результате чего при выполнении кода будет выдано сообщение об Кроме того, в последней строке примера продемонстрирована попытка зане сти в поле номера счета строковое значение. Поскольку объект DataSet знает, что этот столбец может содержать только целочисленные значения, на этапе выполнения будет выдано об ошибке. В листинге 6.2 показано выполнение тех же самых операций с использованием типизированного класса DataSet, Листинг 6.2. Использование типизированного класса DataSet // Создание объекта DataAdapter для каждой // извлекаемой из базы данных.

SqlDataAdapter = new * FROM // Создание объекта DataAdapter для таблицы Invoice.

SqlDataAdapter = new * FROM INVOICE", // Создание пустого объекта DataSet.

dataSet = new // Заполнение объекта DataSet с объектов DataAdapter.

// Вывод имени клиента занести в поле номера счета строку.

// Этот код не будет компилироваться, так как // поле InvoiceNumber может хранить только // целочисленные значения.

"12345";

/30 Часть //. Класс DataSet Типизированный класс DataSet, в свою Очередь, наследует эти классы (рис. 6.2).

DataSet I Customers Constraint Легенда Класс Обобщение t Рис. 6.2. Структура класса Откуда же типизированный класс DataSet получил свое название? Как рассказыва лось в главе 5, для того чтобы указать тип данных, который будет храниться в таблицы нужно создать объект для каждого столбца. Это гаранти рует проверку типов на этапе выполнения, но было бы неплохо быть уверенным в безо пасности типов объекта DataSet еще на этапе написания кода. класс DataSet предоставляет именно такую функциональность. В листинге 6.1 приведен код, в котором используется обычный (не типизированный) класс DataSet.

Листинг Использование класса DataSet // Создание объекта для каждой таблицы, // извлекаемой из базы данных.

SqlDataAdapter = new * FROM // объекта DataAdapter для таблицы Invoice.

SqlDataAdapter = new * FROM INVOICE", объекта DataSet.

DataSet dataSet = new 6. классы DataSet Глава Типизированные классы DataSet этой Что такое класс DataSet?

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

Что такое типизированный класс DataSet?

Типизированный класс отличается от всего, что было рассмотрено ранее в этой книге. Он не является классом Framework, а представляет собой набор классов, порожденных непосредственно от классов семейства DataSet. На рис. 6. показана диаграмма классов из главы 5, демонстрирующая отношение между элемен тами класса DataSet.

Constraint Dataflow Легенда Класс t Рис. Структура класса DataSet 128 Часть Я. Класс DataSet Резюме В этой главе были показаны все многочисленные возможности объекта DataSet, в качестве базы данных, хранящейся в Возможность определе ния схемы объекта DataSet позволяет увеличить производительность механизма про верки данных на стороне клиента (или на среднем уровне). Несмотря на то что база данных может и сама обеспечить хранящейся в ней информации, для этого требуется, чтобы последняя была сначала занесена в базу данных. В то же время схема объекта DataSet позволяет выявить все проблемы, связанные с целостностью информации, без необходимости использования дорогих сетевых ресурсов.

Глава 5. Создание объекта DataSet = SqlDataAdapter = new // Создание пустого объекта DataSet.

DataSet dataSet = new // Заполнение объекта DataSet с объекта DataAdapter.

// Создание DataColumn exColumn = = = - * * // Добавление вычисляемого столбца в таблицу.

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

// + ', ' + // Агрегация.

ion = Следует также отметить возможность агрегирования информации вдоль отноше ний. Для того чтобы обратиться к родительской таблице, нужно воспользоваться пре фиксом Parent:

// Обращение к родительской = Для обращения к дочерней таблице служит префикс child:

// Обращение к дочерней = Если с таблицей связано более одной дочерней необходимо обращаться к ней по имени:

// Обращение к заданной дочерней = Обычно перемещение вдоль отношений осуществляется только при проведении агрегирования информации. Существуют ситуации, в которых вам может понадобить ся включить связанное поле в вычисляемый столбец. Единственный способ сделать это заключается в имитации агрегации с помощью функции Мах:

// Определение стоимости заказа.

= "Quantity * Полное описание синтаксиса выражений вычисляемых столбцов можно найти в документации к свойству См.

126 Часть Класс DataSet Листинг 5.24, Использование столбцов // Создание пустого DataSet.

DataSet dataSet * new DataSet О;

// Создание объекта DataTable.

DataTable = // Создание нового столбца типа // (столбец типа // должен иметь тип = Определяем столбец как = true;

// Определение начального = 1;

// Определение приращения // (число, на которое должно увеличиваться значение // автоинкрементного столбца при добавлении каждой новой = 1;

При создании столбцов типа Autolncrement в объекте DataColumn должно хра ниться значения типа int. В противном случае при попытке установки свойства Autolncrement в значение true будет сгенерировано исключение.

Использование столбцов Для использования вычисляемых столбцов в предусмотрены две стан дартные стратегии. Первая из них заключается в том, чтобы производить вычисления при получении данных с помощью SQL-запроса, например:

SELECT Description, Vendor, Price, Quantity, Discount, - (Price * as FROM JOIN Product on = Второй подход состоит в использовании вычисляемых столбцов. Его преимущество заключается в возможности применять расчеты к только что добавленной информа ции, а не только к информации, извлеченной из базы данных (листинг 5.25).

Листинг 5.25. Использование вычисляемых столбцов // Создание объекта DataAdapter для каждой // извлекаемой из базы данных.

string = Description, Vendor, Price, Quantity, Discount FROM Invoiceltem Product on Глава 5. Создание объекта DataSet form.WriteLine Max Allow Ниже показан результат выполнения приведенного выше фрагмента кода.

Guid True True Max Null: True String Default:

False False Max Length: Allow Null: False String False False Max Length: Allow Null: False Type: String Default:

False False Max Length: Allow Null: True State String MA False False Max Length: Allow Null: True В примере мы продемонстрировали возможности объекта от носительно проверки достоверности данных. Создание схемы в базе данных гаранти рует целостность хранящейся в ней информации, в то время как с помощью "усиления" этой схемы в объекте DataSet можно обнаруживать проблемы, касающие ся данных, на раннем этапе, что позволяет избежать лишнего обращения к базе дан ных. Другими словами, схемы объекты DataSet может существенно об легчить труд программиста.

Использование автоинкрементных столбцов Если после прочтения раздела "Стратегии, использующиеся при определении пер вичного ключа" вы все же решились применять автоинкрементные столбцы, обратите внимание на приведенный в листинге 5.24 код, позволяющий сделать установку зна чения таких столбцов практически тривиальным заданием.

124 Часть II. Класс DataSet False False Max Length:

- Allow Null: True Поскольку объект был создан непосредственно на основе запроса к базе данных, ADO.NET не имеет возможности определить его полную схему. Управляемые поставщики Microsoft определяют информационную схему объекта DataSet на основе сведений, предоставленных объектом (который также для за полнения объектов DataTable). Судя по полученному результату видно, что тип дан ных был определен правильно, однако остальная часть информационной схемы отсут ствует ввиду недостатка сведений, предоставленных объектам DataSet и В этом случае схему необходимо доработать, чтобы сделать объект DataSet более ус тойчивым. примеру, столбец Customer ID должен быть предназначен только для чтения, так как это позволит предотвратить случайное изменение первичного ключа.

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

Листинг 5.23. Улучшенная схема столбцов // Создание объекта DataAdapter для каждой извлекаемой // Сазы данных таблицы.

SqlDataAdapter new * FROM // Создание пустого объекта DataSet DataSet dataSet = new // Заполнение объекта DataSet с помощью объекта DataAdapter // Создание упрощающей доступ к таблице.

DataTable customersTable // Улучшение true;

true;

50;

= false;

= 50;

= false;

= = "MA";

Columns = // Вывод информации о схеме таблицы Customers.

foreach col in { Глава 5, Создание объекта DataSet SqlDataAdapter = new * FROM // Создание пустого объекта DataSet.

DataSet = new DataSet // Заполнение объекта DataSet с помощью объекта DataAdapter.

// Создание переменной, упрощающей доступ к DataTable customersTable = // Вывод информации о схеме столбцов таблицы col in I ) ) ) ) WriteLine } ) Max !

Ниже показан результат выполнения приведенного выше фрагмента кода.

Guid False False Max Length:

- Allow Null: True String Default:

False False Max Length:

- Allow Null: True LastName String Default:

False False Max Length:

- True Allow Null:

String Default:

False False Max Length:

- Allow Null: True State Type: String Default:

122 Часть //. Класс Column + original В этом примере мы добавляем новые обработчики событий и в объект DataTable. При срабаты вании этих событий объект получает уведомление. Следует отметить, что события изме нения срабатывают только при вставке и обновлении, но не при удалении информации.

Два различных события изменения служат разным целям. События Changing и используются в случае, когда нам необходимо отследить факт изменения строки. В то же время события и Changed предназначены для отслеживания факта изменения столбца в пределах строки.

Аргументы, пересылаемые вместе с этими двумя событиями, также служат разным це лям. При срабатывании события объекта передается вся изменившаяся строка, а при срабатывании события объекта изменившийся столбец.

Схема столбца Определив схему необходимо также указать, какие данные могут в ней храниться. Объект DataTable состоит из строк данных. Каждая строка в свою очередь представляет собой коллекцию столбцов. Для того чтобы определить правило для хра нимой в столбце информации, необходимо создать его схему. Объект DataTable имеет свойство типа в котором хранится набор правил для столбцов таблицы. Каждый объект DataColumn является одним столбом в таблице DataTable. В нем хранятся сведения:

• DataType данных, которые могут храниться в столбце;

• — имя столбца;

• — максимальный размер данных, которые могут храниться в столбце;

• aultValue — значение столбца по умолчанию;

• Readonly — определяет возможность внесения изменений в столбец;

• AllowNull — определяет возможность хранения в столбце значения • определяет, должно ли значение столбца быть уникальным в преде лах объекта DataTable.

При создании нового объекта DataSet на основе информации из базы данных или XML-файла в ваше распоряжение предоставляется минимальная схема столбцов, стандартный набор необходимой для обеспечения функ ционирования объекта DataSet. В листинге 5.22 показан пример извлечения инфор мации о схеме столбцов объекта DataSet.

5.22. Схема столбцов объекта DataSet // Создание объекта DataAdapter для каждой извлекаемой из базы данных таблицы.

Глава 5. Создание DataSet Перечисление DataRowAction дает возможность точно определить, что произошло со строкой. Так как объект используется для всех событий строк, можно ожидать, что события RowChanging и сработают и при удалении строки. На самом деле, это не так — при удалении строк срабатывают толь ко события и При отслеживании изменений строки может возникнуть необходимость в точной модифицированной информации. Именно для этого предназначены события и которые при срабатывании предоставляют ссылку как на изменившуюся строку, так и на изменившийся столбец.

Для того чтобы определить триггер, необходимо добавить новый обработчик для требуемого события (листинг 5.21).

Листинг События объекта // Создание объекта DataAdapter для каждой базы данных таблицы.

new "SELECT * FROM // пустого объекта DataSet.

DataSet dataSet = new // Заполнение объекта DataSet с помощью объектов DataAdapter Добавление нового обработчика события изменения строки // (изменения отслеживаются на уровне строки и столбцов;

// это напоминает триггер базы += += new // Изменение строки с целью убедиться в срабатывании = // Обработчик события RowChanged.

void sender, DataRowChangeEventArgs e) I Row + Action + Row Value:

// Обработчик события public void sender, e) 120 Часть II. Класс DaiaSet Задача ограничения достаточно проста, а следовательно, прост и одноименный класс. Он содержит массив столбцов, к которым применяется огра ничение, и булево значение, на факт применения ограничения к пер вичному ключу.

С другой стороны, ограничение типа является более сложным. Его назначение состоит в хранении набора правил, определяющих дейст вия, предпринимаемые по отношению к дочерним строкам, в случае изменения соот ветствующих строк родительской таблицы. К примеру, таблица Customer является родительской, а таблица дочерней. Должен ли объект DataSet ски применять каскадное удаление всех строк заданного потребителя из таблицы Invoice? Это решение определяется в ограничении типа ForeignKeyConstraint.

Класс ForeignKeyConstraint содержит два массива объектов Пер вый массив (Columns) предназначен для хранения столбцов, частью те кущей таблицы. Второй массив состоит из столбцов связанной таблицы. Следует отметить, что на этом этапе объект DataSet уже может проводить проверку своей целостности.

Триггеры (события объекта Большинство систем управления базами данных позволяет определить набор дей ствий, выполняемых при изменении, добавлении или удалении информации. Напри мер, при добавлении в базу данных нового потребителя может сработать ко торый внесет этого потребителя в таблицу поиска. Объект DataSet поддерживает реги страцию для получения уведомлений о проведении определенных операций над объектами DataTable, что позволит выполнять соответствующий код обработки. Ниже перечислены типы событий, поддерживаемых объектом DataSet:

• RowChanging — срабатывает перед изменениями строк объекта DataTable;

• — срабатывает после изменения строк объекта DataTable;

• ing — срабатывает перед изменениями данных в столбцах;

• — срабатывает после изменений данных в столбцах;

• — срабатывает перед удалением строки из объекта DataTable;

• RowDeleted — срабатывает после удаления строки из объекта DataTable.

Подозрительным выглядит отсутствие события добавления строки. На самом деле оно неявно есть, поскольку рассматривает любые изменения свойства Rows (включая добавление новой строки) как предпосылку для срабатывания событий RowChanged и RowChanging. События RowChanging, RowChanged, RowDeleting и RowDeleted предоставляют ссылку на измененную, удаленную или добавленную строку и объект который содержит значение из перечис ления типа DataRowAction. Последнее можно использовать для выяснения того, что же произошло со строкой. Ниже перечислены возможные значение этого пере числения:

• Add — строка была добавлена в таблицу DataTable;

• Change — строка была изменена;

• Commit — измененная строка были внесена в базу данных;

• Delete — строка была удалена;

• Nothing — строка не была изменена;

• Rollback — произошла отмена последних внесенных в строку изменений.

Глава 5. Создание объекта DataSet 1 Is on Primary Key: True Columns Column:

Related Table: Customers Columns Column:

Related Columns CustomerlD Delete Rule: Cascade Update Rule: Cascade None UNIQUECONSTRAINT Is on Primary Key: True Columns Related Table: Invoices Columns Column: InvoicelD Related Columns Column: InvoicelD Delete Rule: Cascade Update Rule: Cascade None Можно заметить, что поскольку для всех таблиц были указаны первичные ключи, для каждого первичного ключа существует ограничение типа UniqueConstraint.

Кроме этого, существуют также два ограничения типа ForeignKeyConstraint — отношение между таблицами Customer и invoice и отношение между Invoice Несложно догадаться, что наличие в классе UniqueConstraint свойства свидетельствует о возможности создавать собственные ограничения типа UniqueConstraint, не относящиеся к ограничению ключа. В дейст вительности подобное ограничение должен иметь каждый столбец базы данных, отме ченный как уникальный (листинг 5.20).

Листинг 5.20. Создание ограничения типа UniqueConstraint для нескольких столбцов // Создание ограничения UniqueConstraint, гарантирующего // уникальность пары // Создание состоящего из // и uniqueCols = new { // Создание ограничения uniqueCols, 118 Часть II. Класс DataSet Вывод состояния ограничений таблиц.

(DataTable in • foreach con in '.

if is { UniqueConstraint u = con as UniqueConstraint;

Is on Primary Key:

else if (con is { ;

ForeignKeyConstraint f con as ForeignKeyConstraint;

Related Table:

Columns", Delete Rule:

Update Rule:

else UNKNOWN TYPE") private void title, cols, string indent) { title) foreach col in cols) I I Выполнение этого фрагмента кода программы приведет к следующему результату:

Table: Customers Name: Constraintl UNIQUECONSTRAINT Is on Primary Key: True Columns Column:

Table: Invoices Глава 5. Создание объекта DataSet Обратившись к одному из примеров, мы попытаемся считать состоя ние ограничений (для удобства в листинге 5.19 приведен полный вариант исходного кода этого примера).

Листинг 5,19. полной схемы программным путем // объекта для каждой извлекаемой Сазы данных SqlDataAdapter = new * FROM SqlDataAdapter new * FROM SqiDataAdapter new * FROM // DataSet, DataSet dataSet - new // Заполнение объекта DataSet с помощью DataAdapter.

// Создание переменных, доступ к таблицам.

DataTable customerTable DataTable = DataTable // Определение первичных = new { = new { } = new { // Определение // Создание первого отношения (между таблицами Invoices и // При создании отношения определяется // ограничение, гарантирующее, что отношение // на основе ключей dataSet ( // Создание второго отношения // (между таблицами Customers и ( 116 Класс DataSet Ограничения Ограничения, к объекту DataSet, являются правилами, которым он должен неуклонно следовать. В существуют ограничения двух типов:

• позволяет гарантировать уникальность строки на основе столбца или набора столбцов;

• — указывает объекту DataSet на необходимость при менения определенных действий по отношению к строке дочерней таблицы в ответ на изменение строки в родительской таблице (таким действием может быть, к примеру, каскадное удаление).

ограничения создаются при определении первичного ключа таблицы и созда нии с ограничением. Каждое ограничение ForeignKeyConstraint пред полагает определение трех правил:

• — поведение дочерних строк при удалении родительской строки;

• — поведение дочерних строк при обновлении дочерней строки;

• правило, определяющее поведение измененных и уда ленных строк при вызове методов и объекта DataSet, DataTabEe или Различие между этими правилами заключается в что правила DeleteRule и UpdateRule применяются при фактическом удалении строк из объекта DataSet, a правило AcceptRejectRule применяется только при вызове методов AcceptChanges и RejectChanges.

Свойства DeleteRule и UpdateRule содержат значение из перечисления Rule.

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

• Cascade — изменения в родительской строке дублируются в дочерней строке.

Это поведение принято по умолчанию.

• None — изменения в родительской строке не оказывают никакого эффекта на дочерние строки, что может привести к появлению "осиротевших" строк в до черних таблицах.

• — изменения в родительских строках приводят к установке столб цов дочерних строк в значения, принятые по умолчанию. Это также может привести к появлению "осиротевших" строк в дочерних таблицах, однако тогда значение ключа, входящего в ограничение, устанавливается согласно схеме в значение, принятое по умолчанию.

• изменения в родительской строке приводят к установке столбцов дочерних строк в значения DBNull. Как и SetDefault, значение SetNull мо жет привести к появлению "осиротевших" строк, однако при этом значение ключа, входящего в ограничение, устанавливается равным Перечисление AcceptRejectRule содержит только подмножество значений из пе речислений DeleteRule/AcceptRule, а именно Cascade и None.

• Cascade — изменения в родительской строке дублируются в дочерней строке.

Это поведение принято по умолчанию.

• None — изменения в родительской строке не оказывают никакого влияния на дочерние что может привести к появлению "осиротевших" строк в до черних таблицах.

Глава 5. Создание объекта DataSet Если мы решим расширить предыдущий пример, то к исходному коду должен быть добавлен код из листинга ответственный за создание отношений между Создание отношений // // первого отношения (между таблицами Invoices и // При создании отношения определяется ограничение, что отношение // создается на основе первичных ключей таблиц.

// второго (между // Customers и Новое добавляется с помощью метода DataRelationCollection.

Add Первым параметром является имя отношения. два параметра представляют собой имена родительского и дочернего столбца в отношении соответ ственно. последний параметр является булевой переменной, определяющей необходимость создания ограничения для этого отношения. Создав ограничение, вы тем самым сообщаете объекту DataSet, что каждое значение уникального ключа роди тельского должно присутствовать в дочернем объекте Но что делать в том случае, когда первичный ключ таблицы состоит из нескольких столбцов? Пример создания массива ключевых столбцов и определение соответст вующего ограничения приведены в листинге Листинг Добавление отношения для таблиц с составным первичным ключом // Создание массива столбцов, входящих а первичный // ключ родительской таблицы.

aParentCols = { // Создание массива столбцов, входящих в первичный // ключ дочерней таблицы.

= { Tables t ]. Columns [ ], // нового отношения на основе // составных первичных ключей таблиц, aParentCols, Часть Класс DataSet являются реляционными т.е. такими, в которых одни наборы данных свя заны с другими наборами данных. В нашем случае таблица связана с таб лицей Invoice, которая, в свою очередь, связана с таблицей а та — с таблицей Product (рис. 5.2).

РК D MlddleName Address Apartment State Zip DOB Stamp ID voice Date Terms FOB PO • ProductID FK Quantity Discount Рис. 5.2. Отношения, существующие между четырьмя таб лицами базы данных 5. Создание объекта ключа, in the + "not Exception Стратегии, использующиеся при определении первичного ключа Одно первых (и самых важных) которое необходимо принять при базы данных, касается выбора первичных ключей таблиц. Ниже при ведены три наиболее распространенных методики первичного со стоящего из одного столбца.

• Автоинкрементное Столбец содержит число, однозначно рующее строку. При добавлении к таблице новой строки число в этом столбце автоматически увеличивается на заданную величину (обычно на единицу). Если вам использовать автоинкрементные ключи на стороне сервера в SQL Server, обратитесь к главе 8, "Обновление базы данных", для получения более подробной информации.

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

• Глобальный уникальный идентификатор Unique Клю чевой столбец содержит GUID, который однозначно идентифицирует строку.

Каждый из подходов имеет свои плюсы и минусы. В контексте объекта DataSet я бы порекомендовал использовать в качестве первичного ключа столбец, содержащий уникальное имя или GUID. Главная проблема при использовании автоинкрементного заключается в неизбежном перекрытии значений ключа в том случае, если вставка данных в одну таблицу осуществляется посредством более чем одного объекта DataSet. Если значения ключа пересекаются, их (а также ссылки на них) необходимо изменить еще до того, как новые строки будут сохранены в базе данных. При исполь зовании GUID или уникальных имен этих проблем можно избежать. Следует отме тить, что все приведенные выше предположения делаются с учетом того, что сущест вует контроль над схемой объекта DataSet.

Бывают ситуации, в которых программисту приходится работать со старыми сис темами. Если в вашей базе данных используются автоинкрементные поля, не пытай тесь применять их также и в объектах DataSet. В этом случае создание идентифика ционных номеров можно возложить на базу данных и извлекать их в процессе об новления (более подробно возврат идентификатора из базы данных рассматривается в главе 8, "Обновление базы данных").

Создание отношений После определения первичного ключа необходимо сообщить объекту об между таблицами. Большинство баз данных, которые ис пользуются на сегодняшний день (SQL Server, Oracle, Microsoft Access, DB2 и т.д.), GUID — это 128-битовые числа, которые гарантированно являются уникальными. В Framework класс может использоваться для генерации, анализа и чтения идентификаторов GUID.

112 Часть Класс DataSet // Создание переменных, доступ к таблицам.

DataTable DataTable invoiceTable = DataTable // первичных ключей = new ( new { invoiceltemTable. PrimaryKey = new [] { Как вы, наверняка, в листинге 5.15 первичные ключи представляют собой массивы объектов DataColumn. И это действительно так! Хотя в большинстве ситуаций первичный ключ представляет собой единственный столбец, иногда необ ходимо использовать ключ, состоящий из нескольких столбцов. Для достижения единообразия обработки первичного ключа требует, чтобы свойство PrimaryKey являлось столбцов.

При определении первичного ключа для таблицы, которая уже содержит данные, нарушающие условие первичного генерируется исключение Argument Exception. Пример обработки исключения типа ArgumentException приведен в листинге 5.16.

Листинг 5.16. Обработка исключения ArgumentException объекта DataAdapter для таблицы Customer.

daCustomers = new * FROM CUSTOMER", // Создание пустого DataSet.

DataSet dataSet = new // Заполнение объекта DataSet с помощью объекта DataAdapter.

// переменной, упрощающей доступ к Customer.

DataTable Добавление дублирующей строки перед определением первичного // ключа - это нам обработать // Определение первичного = new { I catch ex) // Вывод на сообщения об В данной случае // типа говорит нам о что // столбцы, определенные в (части) первичного Глава 5. Создание объекта DataSet Создание схемы объекта DataSet программным путем Бывают в которых создание полной схемы объекта DataSet возможно только программным путем. На первый взгляд это может показаться очень утоми тельным занятием, требующим написания кода огромного размера, однако существу ют и весьма приемлемые альтернативы. В конце концов на определенном этапе у вас обязательно возникнет желание разобраться во внутренней структуре объекта DataSet.

Кроме того, я советую создавать классы, которые непосредственно наследуют класс DataSet (несмотря на то что в примерах данной главы это не отражено). Создав класс-потомок класса вы можете указать его схему на этапе инициализации, как показано в листинге Листинг Наследование класса DataSet class : DataSet ( public { // Здесь можно разместить определение схемы.

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

Пример добавления первичного ключа приведен в листинге Листинг 5.15. первичного ключа // Создание объекта DataAdapter для каждой извлекаемой // из базы данных таблицы.

SqlDataAdapter daCustomers = new * FROM CUSTOMER", SqlDataAdapter new * FROM INVOICE", SqlDataAdapter = new * FROM // Создание пустого объекта DataSet dataSet = new // Заполнение объекта DataSet с помощью объектов DataAdapter.

10 Часть It. Класс DataSet базы данных таблицы.

SqlDataAdapter dataAdapter new * FROM // При DataAdapter схему, в нее информацию о = // Создание пустого объекта "DataSet.

DataSet dataSet new // Добавление в DataSet новой // с именем Использование языка XSD для определения схемы объекта DataSet Как вы далее увидите, создание сложной схемы объекта DataSet может потребовать написания большого объема кода, С целью производительности и облег чения поддержки стоит обеспечить возможность сериализаиии схемы объекта DataSet так, чтобы ею можно было манипулировать отдельно от кода, заполне ние объекта DataSet данными. Это можно сделать путем создания объекта DataSet и сохранения его схемы в файле В результате при каждом создании объекта DataSet файл можно будет использовать для определения схемы, как показано в листинге 5.13.

Листинг Определение схемы объекта с помощью файла // Создание объекта DataAdapter для каждой извлекаемой // базы данных таблицы.

dataAdapter new * FROM // Если DataAdapter обнаружит отсутствие // схемы, он сгенерирует // Создание пустого объекта DataSet.

DataSet dataSet new // из // В этом случае не должно генерироваться исключение, так как // в файле содержится схема таблицы Fill (dataSet, Обратите внимание, что в приведенном коде загружается схема для всего объекта DataSet. Создание модульных файлов использующихся для заполнения отдельных объектов DataTable, не поддерживается. Более подробно XML и объекта DataSet рассматривается в главе 9.

Глава 5. Создание DataSet живать поставщик данных OLE Более подробно перечисле ние описано в документации MSDN.

• Error — если в объекте DataSet отсутствует схема, необходимая для добавления данных из объекта генерируется исключение.

• столбцы, о которых "знает" объект DataAdapter и которые отсутствуют в схеме объекта DataSet, игнорируются.

Значения из перечисления MissingSchemaAction позволяют указать поведение объекта DataAdapter в том случае, если необходимая схема отсутствует. Проще указать схему заранее и установить свойство MissingSchemaAction в значение Error. После этого, если объект DataAdapter попытается внести в объект DataSet не данные, будет сгенерировано сообщение об ошибке, и нам не придется иметь дело с "фантомными" строками (листинг 5.11) Листинг объекта DafaSet с использованием параметра MissingSchemaAction. Error // Создание объекта DataAdapter для каждой базы данных таблицы.

SqlDataAdapter = new * FROM // Если DataAdapter обнаружит // он сгенерирует = // Создание пустого объекта DataSet.

dataSet = new // В данном случае будет // поскольку DataSet пуст содержит Для того чтобы схема была создана объектом DataAdapter, можно методом DataAdapter. Этот метод выводит схему на основе инфор хранящейся в базе данных, точно так же, как это делается обычно;

отличие заключается в том, что схема создается заранее. Метод schema может оказать ся полезным при создании объекта DataSet, который будет заполняться позднее или будет заполнен пользовательскими данными. В качестве параметров метод FillSchema () принимает объект DataSet или и значение из Параметр типа используется для указания объекту DataAdapter на необходимость использования свойства TableMappings В противном случае используется схема, в базе данных (SchemaType. Source). Так как определение значения свойства TableMappings объекта DataAdapter предполагает проведение активных действий, па раметр типа SchemaType почти всегда будет иметь значение SchemaType Ес ли же свойство TableMappings необходимо игнорировать, просто не задавайте его значение. Свойство MissingSchemaAction, как и прежде, используется для указания методу FillSchema на способ добавления схемы, как показано в листинге 5.12.

Листинг 5.12. Заполнение объекта с параметром Создание объекта DataAdapter каждой извлекаемой Часть //. Класс DataSet шлось познакомиться с сотнями различных вариантов схем баз часть рых представляла собой минимально необходимые схемы. Однако как можно определить этот минимум? Несмотря на то что каждому конкретному случаю соответствовала своя схема, чаще всего она включала в себя определения таблиц, столбцов и, иногда, первичных ключей. При более глубоком исходного кода приложения что в нем была реализова на функциональность, легко могла обеспечить сама база данных Тем не менее не хочу выступать здесь в роли беспощадного критика, так как сам иногда опускался до подобной Как определить, обладает ли схема базы данных достаточной полнотой? Если вы когда либо писали код (это касается и класса ADO.NET DataSet) для реализации отношений, огра ничений, триггеров и других функциональных возможностей схемы базы то вам опре деленно стоит уделить более пристальное внимание вопросу проектирования схемы данных.

На самом деле я хочу сказать только что база данных (объект DataSet) должна выпол нять работу, например:

• если в приложении присутствует код для проверки уникальности строк таблицы, до бавьте к таблице первичный ключ;

• если в приложении присутствует код для проверки уникальности отдельного столбца, добавьте к таблице ограничение;

• если в приложении присутствует код для связывания таблиц родителей и потомков, создайте отношение;

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

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

Более подробно использование схемы базы данных рассматривается в книгах Джо (Joe Вывод схемы объекта DataSet с помощью объекта DataAdapter Каждый раз, когда объект DataSet заполняется с помощью объекта он должен содержать схему столбцов (объекты для обеспечения возможно сти принимать новые строки данных. Если на текущий момент объект DataSet не со держит схемы, объект DataAdapter выведет ее на основе результатов выполнения за проса Процессом вывода схемы можно управлять. Объект DataAdapter содержит свойство позволяющее указать дейст вия, которые необходимо предпринять в том если в объекте DataSet отсутству ет нужная схема. Свойство MissingSchemaAction может принимать следующие зна чения.

• Add — к схеме будут добавлены столбцы, необходимые для заполнения объекта DataSet с помощью объекта DataAdapter (значение по умолчанию).

• к схеме будут добавлены столбцы, необходимые для заполнения объекта DataSet с объекта DataAdapter. Кроме того, объект DataAdapter пытается извлечь из базы данных информацию о ключах и опреде ляет первичные ключи заполняемых им объектов DataTable. При использова нии управляемого поставщика OLE DB эту функциональность должен 5. объекта DataSet // строк = "Smoltz", aMaddux = aGlavine = aMaddux aGlavine После завершения этих действий с полученным объектом DataSet можно обра точно так же, как и с объектом DataSet, полученным на основе информации из базы данных или на основе информации из XML-файла.

Определение схемы объекта DataSet Правильна ли ваша схема? Не мешает ли она в работе? Предназначение схемы ба зы данных заключается в определении правил, которым должна соответствовать хра нящаяся в базе данных информация. Проблема при этом заключается в том, что программисты вынуждены отражать такие правила в своем коде, чтобы не допустить ошибок в программе. Схема объекта DataSet позволяет эффективно определить эти правила, избавляя программиста от необходимости писать сложный код.

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

Для того чтобы заставить объект DataSet вести себя так же, как база данных, необ ходимо определить все перечисленные выше элементы. Для этого можно воспользо ваться несколькими способами: объект вывести схему из запро сов, загрузить содержащий необходимую схему XSD-файл, а также создать схему программным путем. Все эти способы определения схемы объекта DataSet рас смотрены в следующих разделах данной главы.

Зачем нужно определять схему объекта DataSet?

Каждый объект DataSet, данные, имеет определенную схему. Таблица DataTable с заданными правилами для каждого столбца строки включает в себя объекты Column. А как бы со схемой, содержащейся в базе данных? Зачем вам необходимы в объекте DataSet все эти ключи, отношения, ограничения и тригге если точно такая же схема присутствует в базе данных? Причина дубли рования схемы базы данных в объекте DataSet заключается в возможности обнаруже ния ошибки задолго до того, как будет предпринята сохранить информацию в базе данных. Таким способом можно не только уменьшить количество обра щений к базе данных, но также избежать результатом которых станет всего лишь уведомление о нарушении схемы. Чем раньше мы найдем ошибки в схе ме, тем их будет исправить.

Обладает ли схема данных достаточной полнотой?

Я разработкой приложений, ориентированных на взаимодействие с базами данных, дольше, чем, возможно, мне этого хотелось бы. В течение этого времени Часть II. Класс DataSet SqlDataAdapter = new // DataSet заполнение информацией, // хранящейся в базе данных.

DataSet dataSet = new // Заполнение объекта DataSet информацией, // хранящейся в базе // в DataSet данных из Как показано в приведенном выше примере, часть информации считывается из базы данных, а часть — из При вызове метода ReadXml использу ется директива потому что по умолчанию при чтении XML-данных в объект DataSet, уже содержащий данные, метод ReadXml попытает ся сопоставить схему XML-файла с схемой объекта DataSet. Поскольку при вызове метода в объекте DataSet отсутствует таблица Product, без указания на необ ходимость вывода схемы объект DataSet не сможет принять данные из XML-файла.

При желании эту проблему можно было загрузив XML-данные прежде ин формации из базы данных, так как в случае отсутствия схемы объект DataSet создает ее сам (согласитесь, что вывод схемы только в случае ее отсутствия является несколь ко сбивающим с толку поведением). Если же считываемый XML-файл содержит в се бе дополнительные строки таблиц, уже в объекте DataSet, директива нам просто не нужна.

Более подробно интеграция и объекта DataSet рассматривается в главе 9, и Создание объекта DataSet программным путем В некоторых случаях нам может понадобиться создать объект DataSet программным путем. Создание объекта DataTablc вручную и его заполнение данными позволит вам лучше узнать структуру объекта DataSet. Объект DataSet создается сверху вниз. Другими словами, сначала мы создаем сам объект DataSet, затем добавляем в него объект и определяем информационную схему, создавая объекты Нако нец, в объект DataTable добавляются строки данных, как показано в листинге 5.10.

Листинг 5.10. Создание объекта DataSet программным путем // Создание объекта DataSet.

DataSet dataSet = new // Добавление нового DataTable в коллекцию // объектов DataTable объекта DataSet.

DataTable dataTable -. ;

// столбцов (определение информационной Глава 5, Создание DataSet из XML-файла или из места, указанного в URL-адресе, не используя при этом управ ляемый как в листинге 5.8.

Листинг 5.8. Создание объекта DataSet на основе XML-документа // Заполнение объекта DataSet из XML-файла // или из места, указанного DataSet dsFile new // Заполнение DataSet на основе объекта // (класс является от DataSet = new StringReader textReader new // Заполнение объекта DataSet на основе объекта // (класс XmlTextReader производным от DataSet new XmlTextReader = new // Заполнение объекта DataSet на основе объекта Stream // (класс является производным от DataSet = new FileStream fs = new Метод ReadXml О может принимать несколько вариантов входных данных: текст в формате путь к файлу, XmlReader, объект StringReader или даже объект Stream.

Так как объект DataSet имеет реляционную структуру, его создание на основе XML-документа может показаться странным — ведь по своей природе последний ие рархичен, Несмотря на то что объект DataSet не является самым лучшим XML-данных, он подходит в качестве средства "смешивания" информации из базы данных и данных в формате XML. Чтобы "приготовить" эту необходи мо использовать объект для извлечения информации из базы данных и метод ReadXml для ее загрузки из XML-документа, как показано в листинге 5.9.

Листинг 5.9. "Смешивание" информации из базы данных и XML документа // Создание строки-запроса query.

// Для того чтобы извлечь из базы данных несколько // таблиц, мы используем пакетный запрос, возвращающий // несколько результирующих наборов string query = query "SELECT * FROM "SELECT * FROM query "SELECT FROM // Создание объекта DataAdapter для извлечения информации базы II. Класс DataSet // Определение.

"ID") // Создание пустого объекта DataSet.

DataSet dataSet new // Заполнение объекта DataSet с помощью объекта DataAdapter.

Как показано в листинге 5.6, объект DataAdapter получает указание переимено вать поле Customer, поле таблицы Invoice и по ле таблицы в ID. Единственным требованием для пере именования столбцов в объекте Data able является существование соответствующего объекта Если использовать объект для конкрет ного объекта DataTable нет необходимости, то с целью обеспечения поддержки объек та Mapping можно создать пустой объект DataTableMapping, как показано в листинге 5.7.

Листинг 5.7. Использование свойства без изменения имен таблиц Создание объекта DataAdapter для каждой извлекаемой // из базы данных таблицы.

SqlDataAdapter dataAdapter = new "SELECT * FROM CUSTOMER", // Определение // отображения для имени // Создание пустого объекта DataSet dataSet = new // Заполнение объекта DataSet с помощью объекта DataAdapter.

В этом примере создается "фиктивный" объект DataTableMapping, не производя щий переименования таблицы. Обратите что имена таблиц в вызове метода должны совпадать с именем таблицы в вызове метода Создание объекта DataSet на основе Времена меняются — сейчас мы должны быть готовы интегрировать наши данные с данными из произвольных источников (отличных от базы данных). Для того чтобы создать объект DataSet на основе XML-документа, необходимо считать в него данные 5. Создание объекта DataSet ЮЗ Листинг 5.5. при заполнении объекта DalaSet таблицами с измененными именами // Создание строки-запроса query.

// Для того чтобы извлечь из базы данных несколько таблиц, мы используем пакетный запрос, возвращающий // несколько результирующих наборов string query = query "SELECT * FROM query "SELECT * FROM query "SELECT * FROM // Создание объекта для извлечения SqlDataAdapter = new // Определение // Создание пустого объекта DataSet.

DataSet = new DataSet // Заполнение объекта DataSet с В этом листинге метод () использует в качестве базового имени таблицы имя "ADONET", в результате чего после выполнения пакетного запроса получают имена и ADONET2, а стандартным базовым именем таблицы является "Table". На практике одновременное использование свойства TableMappings и определение имени таблицы при вызове метода встречается редко.

Кроме определения имен таблиц, свойство TableMappings может использовать ся для переименования столбцов (при этом первоначальные имена столбцов, воз в результате выполнения запроса, заменяются новыми именами, ис в объекте Для поддержки этой функциональности класс содержит коллекцию объектов отображение между именами полей в запросе и именами полей в объекте DataSet (листинг 5.6).

Листинг 5.6. Использование свойства // Создание строки-запроса query.

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

string query query 4= "SELECT * FROM query += * FROM query += * FROM отображений имен таблиц.

102 Часть II. Класс DataSet DataSet dataSet = new // Заполнение DataSet с // присваивая информативные Хотя теперь таблицы имеют более понятные имена, при этом совершается три об ращения к базе данных, что очень неэффективно. Для того чтобы иметь информатив ные имена таблиц при использовании пакетного запроса, следует воспользоваться свойством объекта DataAdapter Использование свойства TableMappings Свойство TableMappings объекта DataAdapter является коллекцией объектов которые используются для объекту DataAdapter имен объектов DataTable внутри объекта DataSet, а также имен столбцов внутри каждой Для того чтобы использовать свойство TableMappings для назначения имен таблицам, просто добавьте к нему отображение старого имени таблицы на новое имя таблицы, как показано в листинге 5.4.

Листинг 5.4. Использование свойства TableMappings Создание строки-запроса query.

// Для того чтобы извлечь из базы данных несколько таблиц, мы используем пакетный запрос, // несколько результирующих string query = query += "SELECT * FROM query "SELECT * FROM query += "SELECT * FROM // объекта DataAdapter для извлечения данных.

SqlDataAdapter dataAdapter = new // // Создание пустого объекта DataSet.

DataSet dataSet = new DataSet // Заполнение объекта DataSet с помощью объекта DataAdapter.

Как уже упоминалось ранее, в результате метода DataAdapter. () создаются таблицы с именами Table, Tablel и Table2. Поскольку нам известен алгоритм назначения стандартных имен, достаточно просто задать отображение имен с целью переименования Table в Customers, Tablel в Invoices, а в Invoiceltems. Кроме того, методу О можно указать на не присвоения объектам DataTable более полезных имен, как показано в листинге 5.5.

Глава 5. Создание DataSet Множественные DataTable Вскоре вы поймете, что больше всего достоинства объекта DataSet проявляются при его использовании в качестве реляционного данных, расположенного в памяти. Вам довольно часто придется создавать объекты DataSet, содержащие более одного объекта DataTable. Это можно сделать разными способами. Мы рассмотрим каждый из них, отмечая по ходу их плюсы и В листинге 5.2 показано ис пользование пакетных для получения из базы данных нескольких таблиц, Листинг 5.2. Использование пакетного запроса // Создание строки-запроса query.

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

string query = query +- "SELECT * FROM query += "SELECT * FROM query += "SELECT * FROM // Создание объекта DataAdapter для извлечения данных, SqlDataAdapter new // Создание и заполнение объекта DataSet.

DataSet dataSet = new Все было бы хорошо, если бы только автоматически не "универсальные" имена. В этом примере объекты названы Table, и соответственно. Если вам необходимо иметь более информативные имена таблиц, вы можете использовать один объект DataAdapter на каждую из таблиц та DataSet, как показано в листинге 5.3.

Листинг 5.3. Заполнение объекта DataSet с использованием информативных таблиц // объекта DataAdapter для каждой // из daCustomers = new * FROM SqlDataAdapter = new * FROM SqlDataAdapter - new * FROM // Создание пустого объекта Обратите внимание, что не все производители баз данных поддерживают пакетные запро сы. Для того чтобы ознакомиться со всеми способами заполнения объекта DataSet таблицами, прочитайте весь раздел. Другие описанные в нем методы более независимы от про изводителя базы данных.

/00 //. Класс DataSet DataAdapter в качестве "моста" между объектом DataSet и базой данных можно встретить на всем протяжении этой главы, а также в главах части II этой книги.

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

Создание объекта DataSet на основе информации, хранящейся в базе данных В самом простом случае объект DataSet будет использоваться для хранения одной таблицы, как показано в листинге 5.1.

Листинг Создание объекта на основе информации, хранящейся в базе данных Создание объекта Command для извлечения из данных таблицы Customer.

cmd = = "SELECT * FROM CUSTOMER";

// объекта DataAdapter для объекта DataSet.

SqlDataAdapter dataAdapter new // пустого объекта DataSet.

DataSet dataSet = new // Заполнение DataSet с помощью В этом примере объект DataSet заполняется результирующим набором данных — таблицей CUSTOMER. На данном этапе читатель должен быть знаком с созданием объектов Connection и Command, а поэтому нам достаточно рассмотреть подробно только выделенный код.

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

2. Создается экземпляр класса DataSet. Только что созданный объект DataSet является пустым.

3. Вызывается метод Fill о объекта DataAdapter для заполнения объекта DataSet данными из запроса, определенного ранее в объекте Command.

Это очень простой пример заполнения объекта DataSet информацией из одной таблицы на основе одного SQL-запроса. Далее в этой главе мы рассмотрим более сложные аспекты создания объектов не затрагивая при этом вопрос обновле ния базы данных. Более подробно обновление базы данных рассматривается в главе 8.

Глава 5. Создание DataSet • содержит базовые классы и интерфейсы данных;

• содержит классы управляемого дан ных SQL Server;

• содержит классы управляемого поставщика данных OLE DB;

• содержит классы управляемого поставщика данных ODBC;

содержит классы управляемого данных Oracle.

В отличие от уровней доступа к данным Microsoft, объект DataSet не имеет никакого внутреннего представления об источнике данных. Таким образом, объект DataSet оказывается отсоединенным не только от данных, но и от информа об их происхождении. Следовательно, нам необходимо иметь что-то, что позво лило бы соединить объект DataSet и источник данных.

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

Объект DataAdapter Объект DataAdapter является связующим звеном между объектом DataSet и храни лищем С помощью объекта DataAdapter можно заполнять объект ин формацией из хранилища данных, а также обновлять хранилище данных на основе объекта DataSet. Фактически объект DataAdapter представляет собой метакоманду, объект DataAdapter состоит из четырех объектов каждый из которых выполняет строго определенную задачу:

• — используется для извлечения данных из хранилища;

• используется для добавления новых записей, созданных в объекте DataSet, в нижележащее хранилище;

• — используется для обновления существующих записей в хра нилище на основе изменений в объекте DataSet;

• — используется для удаления существующих записей из храни на основе удалений в объекте DataSet.

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

INSERT, UPDATE и DELETE. Основное предназначение объекта DataAdapter заключается в формировании "моста" между объектом DataSet и базой данных. Поскольку таким "мостом" является сам объект DataAdapter, он должен иметь возможность выполнять все основные операции над базой данных. Примеры использования объекта //. DataSet • — коллекция правил, описывающая данные, которые можно хра нить в объектах DataRow.

• DataRow — коллекция данных, которая представляет собой одну строку табли цы DataTable. Объект DataRow является фактическим хранилищем данных.

• правило, которое задает допустимость хранения определенных данных в объекте DataTable. Объект используется для определения бизнес-правил объекта DataSet.

• DataRelation — описание навигационных связей между различными объектами DataTable. В большинстве случаев объект DataRelation сопровождается объектом Constraint, позволяющим строго задать отношение.

Как показано на рис. 5.1, внутри объекта DataSet может храниться коллекция объектов DataTable. В свою очередь внутри объектов DataTable хранятся коллекции объектов DataRow, DataColumn, Constraint и две коллекции объектов DataRelation.

Коллекция объектов DataRelation соответствует формирующим связь между объектами DataTable отношениям "родитель-потомок". Коллекция объектов DataRelation внутри объекта DataSet является представлением всех объектов DataRelation во всех объектах DataTable.

DataSet t DataTable I DataRelation Легенда Класс Композиция Рис. 5.1. Структура DataSet Объект DataSet и управляемые поставщики данных В главах рассматривались методы взаимодействия с данных (как правило, с базами данных). По идее, было бы вполне логично предполо жить существование какой-либо связи между объектом DataSet и доступом к храни лищу данных. К сожалению, это не так. Компания Microsoft специально спроектиро вала пространства имен, с тем чтобы подчеркнуть отсутствие подобной связи:

• содержит классы, которые входят в объект DataSet, а также интерфейсы, использующиеся для определения управляемых 5. DataSet Глава Создание объекта DataSet В этой главе...

Что такое объект DataSet?

Заполнение объекта DataSet Определение схемы объекта DataSet Так что же представляет собой объект DataSet? Для начала попытайтесь отвлечься от сложностей отсоединенного режима доступа к данным. Объект DataSet — это про сто реляционная структура, хранимая в памяти. Но что это дает в контексте дневной работы с объектом? Если вспомнить мои замечания о стоимости сетевого трафика и соединений с данных, то становится понятно, что хранение данных и правил, связанных с этими данными, на стороне клиента или на среднем уровне мо жет сократить стоимость применения правил к данным.

Что такое объект DataSet?

Если вы знакомы с другими API доступа к данным, то можете попытаться найти в них объекты, аналогичные объекту DataSet. He скрою, что этот всего, завершится неудачей. Какое отношение имеет объект DataSet к доступа к базе данных или, скажем, к XML? А может это что-то совершенно новое?

Объект DataSet — это:

• набор извлеченной из базы данных;

доступ к этому набору осу ществляется в отсоединенном режиме;

• база данных, расположенная в памяти;

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

Структура объекта DataSet Объект DataSet состоит из нескольких связанных друг с другом структур данных.

Концептуально он представляет собой полный набор реляционной информации.

Внутри объекта DataSet могут хранится пять объектов.

• DataTable — прямоугольный набор данных, организованный в столбцы и строки. объект является аналогом объекта ADO Recordset и объекта OLE DB RowSet.

96 //. Класс DataSet Часть II Класс DataSet 5. Создание объекта DataSet Глава б, Типизированные классы DataSet Глава 7. Манипулирование объектом DataSet Глава 8. базы данных // Запрос к базе string query = "SELECT FirstName, + City, Zip, DOB, + InvoiceDate, PO, Terms + FROM Customer JOIN Invoice on = + + ORDER BY // Подключение к базе данных и выполнение conn = new + + "Integrated // Создание объекта команды для выполнения запроса.

= query;

// Создание объекта DataReader.

SqlDataReader | // Использование объекта DataReader для создания // объектной модели нашего = new // Закрытие объекта DataReader.

// Заполнение комбинированного // Установка фокуса на первом элементе // в комбинированном = 0;

Поскольку мы спроектировали достаточно неплохую объектную модель, реали зация в приложении достигается путем создания экземпляра класса CustomerList, конструктору которого передается объект DataReader. Полный исходный код прило жения можно загрузить с моего Web-узла по адресу:

Резюме Дочитав книгу до этой страницы, вы уже должны были проникнуться идеей "пожарного шланга" и понять всю ее мощь. Так как в ADO.NET однонаправленный курсор представлен отдельным типом объекта (DataReader), считывание данных с по мощью "пожарного шланга" стало куда более простым занятием, чем в старых API доступа к данным. Кроме того, вы должны были узнать о способе объекта DataReader с целью обеспечения доступа к данным для ваших собственных классов. Во второй части этой книги мы воспользуемся полученными знаниями для того, чтобы рассмотреть, какую роль во всей этой схеме играет объект 94 Основы ADO.

Класс InvoiceList Класс (как и класс — это простая оболочка клас са ArrayList, позволяющая иметь конструктор, которЬЕЙ принимает интерфейс Самая важная часть исходного кода данного класса — это часть, в которой реализуется добавление счета в список. Если посмотреть на запрос, который используется для извлечения из базы данных, можно заметить, что каждая строка резуль тирующего набора содержит сведения как о клиенте, так и о его счете. Следует отметить, что эти же сведения применяются и при в базу данных счета определенного клиента. В самом начале исходного кода конструктора я номер клиента В цикле do while я добавляю все счета клиентов до тех пор, пока идентификационный номер клиента текущей строки совпадает с идентификационным номером. Вы могли обратить внимание на что когда я заношу в "кэш" идентификационный номер клиента, я не делаю проверку на null.

В этом нет необходимости, так как идентификационный номер клиента — это первичный ключ соответствующей таблицы. Если идентификационный номер клиента был бы равен null, это означало бы наличие неожиданной и неисправимой ошибки, требующей гене рации исключения. Исходный код класса InvoiceList показан в листинге 4.16.

Листинг Класс InvoiceList public class InvoiceList : ArrayList I public rdr) { // текущего клиента.

Guid customerlD = ) // Добавляем счета до тех пор, пока не изменится идентификатор клиента или // не будет достигнут конец объекта DataReader.

do Invoice inv = new, while customerlD == Код Windows-формы Написав запрос и разработав классы, использующиеся для представления данных, рассмотрим код, непосредственно для создания объектной модели, как показано в листинге 4.17.

Листинг 4.17. Код Windows-формы Необходимо для поддержки проектировщика Глава 4. Получение данных Добавление клиентов в список, while { Customer = new Класс Invoice Класс Invoice очень похож на класс Customer. Это простой класс, в котором со держится информация о счете клиента. Для представления списка счетов я решил применить элемент пользовательского интерфейса Listview, самый эффективный способ заполнения которого заключается в создании массива. Ответственность за созда ние массива я решил возложить на класс Invoice, разработав метод Исходный код класса Invoice приведен в листинге 4.15.

Листинг 4.15. Класс Invoice public class Invoice public rdr) I // Если столбец не равен null, извлечь всю информацию о счете клиента из объекта DataReader.

= = (rdr, InvoiceDate = PO FOB = ;

// Члены класса - открытые на случай, если нам // понадобится получить к ним прямой доступ.

public InvoicelD;

public Int public InvoiceDate;

public string PO;

public string public string public { // Создание массива необходимого для более эффективного заполнения объекта values = new = = = PO;

= FOB;

= return values;

Листинг 4.13. Класс Customer public class Customer public rdr) i // Если столбец не равен null, извлечь всю информацию // о клиенте из объекта DataReader.

= = = = Address = = State Zip DOB // Создать объект InvoiceList.

Invoices = new, // Члены public Guid CustomerlD;

public string public string FirstName;

public string HomePhone;

public string public string City;

public string public string Zip;

public DOB;

public InvoiceList // Переопределение метода для обеспечения // вывода полного имени клиента в комбинированном public override string () • return LastName + FirstName;

Класс CustomerList Класс CustomerList — это тонкая "оболочка" класса ArrayList, необходимая для создания простого упорядоченного списка клиентов. Цель создания подобной оболочки состоит в том, чтобы иметь принимающий в качестве аргумен та объект DataReader. Исходный код класса CustomerList приведен в листинге Листинг 4.14. Класс CustomerList // Оболочка класса ArrayList, поддерживающая создание // объекта CustomerList на основе DataReader.

public class CustomerList : ArrayList public rdr) Глава 4. данных Доступ к базе данных Получить всю информацию о клиентах и их счетах можно несколькими способами.

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

Для того чтобы упростить задачу, я написал запрос, который возвращает всю необ ходимую информацию одном наборе данных. Этот запрос выбира ет из базы данных все необходимые поля таблиц и Invoice. Ниже приве дена такого запроса:

SELECT Customer. Customer ID, City, State, Zip, InvoiceDate, PO, Terms Обратите внимание, что единственным полем с полностью определенным именем является поле так как оно существует более чем в одной таблице. Так как мы запрашиваем информацию из нескольких таблиц, их необходимо объединить, Ниже приведена запроса:

FROM Customer JOIN Invoice on = И, наконец, для того чтобы упорядочить извлеченные данные, нам вить предложение ORDER BY (иначе счета клиентов могут ORDER BY Customer. CustomerlD, Invoice. А вот как выглядит наш запрос целиком:

SELECT FirstName, Address, City, State, Zip, DOB, InvoiceNumber, InvoiceDate, FOB, PO, Terms FROM Customer JOIN Invoice on = ORPER BY Объекты данных Одним из решений, принятых мною в самом начале разработки было создание классов для представления клиентов и счетов. Эти классы предназначены для простого хранения информации, извлеченной из базы данных. Поскольку для выполне ния последней операции я планировал использовать объект эти классы по могли бы мне создать копию данных в оперативной памяти и я мог бы закрыть соеди нение с базой данных сразу же после считывания всей нужной информации.

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

Класс Customer Начнем с класса Customer. В нем должны содержаться демографические сведения о клиенте, взятые из базы данных. Кроме того, здесь должен храниться список всех счетов клиента, представленный классом (см. листинг 4.16).

Когда я отлаживал этот класс, то понял, что для хранения информации о кли ентах мне необходимо воспользоваться комбинированным списком. Чтобы изменить вид имени клиента, я переопределил метод класса Object так, чтобы он возвращал имя и фамилию клиента. Когда объект Customer добавляется в комбини рованный список, метод ToString автоматически вызывается для определения ото бражаемого имени клиента. Исходный код класса Customer показан в листинге 90 I. Основы судят о том, использует ли база данных SQL Server в качестве ключа таблицы глобальный уникальный идентификатор • Булево выражение, указывающее на уникальность хранящихся в столбце данных. Если значение столбца таблицы метаданных равно true, то значение столбца IsUnique также будет равно true.

• IsKeyColumn. Булево выражение. Значение true указывает на то, что столбец либо первичным ключом таблицы, либо его частью.

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

• Имя схемы в базе данных, содержащей таблицу столбца.

null, если это невозможно определить.

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

null, если это невозможно определить.

• Имя таблицы базы данных, в которой содержится null, если это невозможно определить.

• Имя столбца в таблице базы данных. Не зависит от имени столбца в объекте Command.

Если вам необходимо получить более подробные сведения об информационной схеме базы данных, обратитесь к разделу "Получение информационной схемы базы данных с помощью поставщика OLE главы Создание простого приложения, взаимодействующего с базой данных В этом разделе мы постараемся объединить весь рассмотренный ранее материал в простом приложении Windows Forms, просматривать счета клиентов (диалоговое окно приложения показано на рис. 4.2).

При желании вы можете загрузить исходный код этого приложения с моего Web узла по адресу: В следующих разделах данной главы оста новимся на рассмотрении способа использования в приложении объекта DataRcadcr.

Рис. 4.2. Forms Глава 4. Получение данных :

Type:

False False Result Set :

False False Column:

:

False False InvoiceDate :

IsUnique: False AllowDBNull: False В схемы хранится различная информация о возвращенных В сле дующем списке перечислены все столбцы таблицы метаданных результирующего набора.

• Имя столбца, определенное поставщиком. Если имя столбца ука зано в объекте Command, используется это имя. С конкретной ме тода не связано никаких гарантий того, что имя столбца будет уникальным или что оно не будет равно null.

• Порядковый номер столбца. Отсчет порядковых номеров столбцов начинается с единицы.

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

• Максимальная точность числа (если в столбце хранятся числовые данные). Если в столбце хранятся не числовые данные, то здесь со держится значение • Количество знаков после запятой (если в столбце хранятся число вые данные). Если в столбце хранятся не числовые данные или числовые данные, которые не поддерживают дробные значения, то здесь содержится null.

• DataType, CLR-тип столбца.

• DbType. Тип специфичный для поставщика данных.

• Булево выражение, возможность хранения в столбце длинных двоичных данных (например, BLOB).

• AllowDBNull. Булево выражение, определяющее возможность хранения в столбце значения null.

• Булево выражение, определяющее возможность изменения значе ния столбца.

• Булево выражение. Значение true указывает на что в столбце хранится идентификатор строки. Как правило, по значению этого выражения Основы ADO. NET метод GetSchemaTable который возвращает объект DataTable, содержащий ин о типах данных набора. Более подробно объект DataTable рассматривается в главе 6. В листинге 4.12 приведен упрощенный пример извлечения о столбцах результирующего набора данных.

Листинг 4.12. Работа с метаданными объекта объекта команды.

= conn.

= "SELECT * FROM + "SELECT * FROM Создание объекта DataReader в выполнения запроса SqlDataReader rdr = // Итерация по результирующим наборам do // Получение метаданных результирующего набора // в виде объекта DataTable.

DataTable schema = // Вывод заголовка // Вывод информационной foreach (DataRow row in :

while ) Если выполнить этот код, на консоль будут выведены метаданные таблиц клиентов и счетов. Ниже приведен отрывок из них:

Result Set :

False AllowDBNull: False Column:

FirstName :

IsUnique:. False False Column:

Глава 4. Получение данных В большинстве остальных случаев имеет смысл использовать поведение команды Блокировка базы данных без необходимости приводит к недоступности строк для других процессов, работающих с базой данных, и может стать причиной снижения производительности.

Что такое результирующие наборы данных ?

В ответ на запрос база данных формирует так называемый набор данных. Если в запросе требовалось возвратить только одну информации (т.е.

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

SELECT FROM CUSTOMER SELECT * FROM Обработка множественных результирующих наборов данных Извлечение множества результирующих наборов данных из одного объекта Reader — вполне обыденное занятие. Для итерации по этому множеству исполь зуется метод объекта DataReader. В отличие от метода Read он не должен вызываться перед началом считывания данных. Пример использования метода NextResult () приведен в листинге 4.11.

Листинг Использование объекта DataReader для возврата множественных результи рующих наборов // Создание объекта команды.

= = "SELECT * FROM + "SELECT FROM INVOICE";

// Создание объекта DataReader в результате выполнения запроса.

SqlDataReader rdr // Итерация по всем результирующим do \ Итерация по всем записям while ( } while Как видно из этого примера, метод NextResult вызывается только после того, как мы закончили обработку первого результирующего набора данных.

Работа с метаданными объекта DataReader В большинстве случаев нам известен тип информации, извлекаемой из базы дан ных, но так бывает не всегда. К счастью, объект DataReader предоставляет сведения о типах данных для каждого результирующего набора. Все имеют 86 Часть Основы Этот класс предлагает более простой метод определения прием лемого значения для null-полей. Используя класс Field, я могу сделать код более удобочитаемым. Листинг 4.9 — яркий этому пример (для сравнения см. листинг 4.7).

Листинг 4.9. Использование public rdr) { // Если столбец не содержит значение null, извлечь // хранящуюся нем информацию.

= InvoiceNumber = InvoiceDate = = PO FOB = единичные случаи, когда поле не может содержать значения null. Ти пичными примерами являются первичные ключи и обязательные поля. Фактически для определения подобных полей мы могли бы воспользоваться информационной схемой базы данных. Если информационная схема показывает, что поле является обязательным, проверка — это простая трата времени. Если поле обязательно, однако сообщает, что оно является null-полем, генерируется исключение. Следует отметить, что в данном случае это весьма кстати, поскольку после возникновения такой неожиданной ситуации приложение вряд ли может продолжать нормально свою работу.

В любом случае если вы решите использовать мой класс Field, то можете обойти это требование, рискуя встретить null-строку в совершенно неожиданном месте.

Блокировка базы данных Каждая современная многопользовательская база данных поддерживает блокировку.

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

Листинг 4.10. Использование объекта DataReader с блокировкой записей базы данных // Записи базы данных SqlDataReader rdr = // записей базы данных не происходит.

SqlDataReader rdr = Блокировать записи при работе с объектом DataReader нужно по двум причинам.

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

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

Глава 4. данных if = if I InvoiceDate = if I Terras = if { PO = } if ] { FOB = Достаточно много работы для простого копирования значений из шести Необходимо предложить более эффективное решение. Вы можете загрузить класс Field с моего Web-узла, расположенного по адресу: В лис тинге 4.8 приведена часть его (класса исходного кода.

Листинг 4.8. Класс Field public class Field -' // Доступ к полю осуществляется по его public string rec, string fldname) I return static public decimal rec, string fldname) { return 84 I. Основы помогает более эффективно работать с запросами большого количества двоич ных данных, когда реально работа ведется только с их частью.

• В результате выполнения запроса ожидается возвращение толь ко одного набора данных.

• В результате выполнения запроса ожидается возвращение только одной строки.

Работа с Null-столбцами Обеспечивающие безопасность типов get-методы объекта DataReader (например, GetString, и т.д.), удобно использовать при извлечении значений столбцов из объекта DataReader. В идеале их существование могло бы полностью удовлетворить разработчика. К сожалению, при считывании данных из столбца нет никаких гарантий того, что они там вообще Начинающих разработчиков это может сбить с толку. Например, если в клиентов есть запись о клиенте, который не сообщил свой адрес, то это поле может иметь значение null. На самом деле содержимое этого поля полностью зависит от разработчика базы данных. В том случае, если он решит вообще не заполнять это поле в записи клиента (вместо того, чтобы заполнить его пустой строкой), оно будет иметь значение null.

Когда вы совершаете итерацию по записям в объекте DataReader, при попытке извлечь значение из null-поля (а точнее, при попытке преобразования null-поля в требуемый тип данных) сгенерирует исключение. Чтобы избежать подобной ситуации, объект DataReader имеет метод предназначен ный для индикации null-поля. Пример использования метода пока зан в листинге 4.6.

Листинг 4.6. Работа с ми // - это экземпляр класса SqlDataReader rdr while // Вывод на консоль значения поля, если оно не null.

if { // Вывод на консоль первого поля строки.

} Несмотря на то что этот метод работает, в приложений он приводит к созданию огромного и некрасивого кода. Взгляните на листинг 4.7, представляю щий собой пример типичного копирования содержимого объекта DataReader в неко торое локальное Листинг 4.7. Множественная проверка на в объекте DataReader public I // Если столбец не содержит значение null, извлечь // хранящуюся Ё нем информацию.

if = Глава 4. Получение данных // Создание объекта DataReader в результате выполнения запроса.

// Итерация по всем строкам.

while { // этих строк кода } Методы GetOrdinal О и объекта в некотором смысле допол няют друг друга, позволяя узнать порядковый номер и имя столбца, соответственно.

Параметры метода ExecuteReader В большинстве случаев вызов метода ExecuteReader объекта Command без пара метров является оптимальным решением. Тем не менее в более сложных сценариях метод ExecuteReader может быть вызван с флагом который из меняет способ создания объекта DataReader и влияет на содержащиеся в нем типы данных. Флаг CommandBehavior может принимать одно или более из перечисленных ниже значений (поддерживается их побитовая комбинация).

• С данным значением не ассоциировано никакое специальное поведе ние. команда будет выполнена на сервере без установки/снятия блокировки строк;

соединение при этом также не подвергается какому-либо воздействию. Значение Default используется по умолчанию при вызове метода ExecuteReader без указания флага CommandBehavior.

• CloseConnection. Когда объект DataReader достигает конца всех результи наборов данных или попросту уничтожается, уничтожается также и ас социированный с ним объект Запрошенная информация без блокировки каких-либо строк. Данное значение обычно применяется только при чтении, Предполагает ся, что набор данных, объектом DataReader, не будет обновлен. Это очень важно, так как если не заблокировать строки во время работы с объектом DataReader, то любые из них (еще не извлеченные) могут быть изменены, что способно привести к весьма неожиданным результа там. К тому же управляемый SQL Server рассматривает значение Keylnfo как указание поместить предложение FOR BROWSE в выполняемую ко манду. При работе с SQL Server такое поведение может привести к различным побочным эффектам. Более подробно эта тема рассматривается в документации no SQL • В объекте DataReader информационная схема базы однако запрос при этом не выполняется и в базу данных не вносятся никакие изменения.

• SequentialAccess. Строки считываются последовательно на уровне столбцов, что удобно при использовании методов GetChars или GetBytes для ния из строк объекта DataReader длинных двоичных данных. Если не использо вать это значение, то последние будут считаны в объект DataReader вне мости от того, будут они использованы или нет. Таким образом, это значение 82 Часть I. Основы Поскольку индексатор возвращает объект, его можно привести к типу или вызвать для этого один из get-методов объекта безопас ность типов, как показано в листинге 4.3.

4.3. Использование get-метода объекта обеспечивающего безопасность типов // Создание объекта команды.

= "SELECT * FROM CUSTOMER";

// Создание объекта DataReader в результате выполнения запроса.

SqlDataReader rdr = // Итерация по всем строкам.

while ( // Эти строки кода // имеют одинаковую Обеспечивающие безопасность типов get-методы не производят никаких преобра зований — они просто осуществляют приведение типа. Если попытаться извлечь тип которого не соответствует типу, к которому осуществляется приведение, метод сгенерирует исключение такое же, которое бы воз никло в результате выполнения некорректной приведения типа. Если бы в примере первый столбец имел целочисленный тип, было бы сгенери ровано исключение InvalidCastException. В тон случае, когда вам нужно провести именно преобразование типа, воспользуйтесь классом как показано в листинге 4.4.

Листинг 4.4. Использование класса Сол vert = "SELECT * FROM PRODUCT";

// Создание объекта DataReader в результате выполнения запроса.

SqlDataReader rdr = // Итерация по всем строкам.

while { get-методы, обеспечивающие безопасность типов, требуют передачи в качестве параметра порядкового номера столбца, что, естественно, менее удобно, чем исполь зование его имени. Альтернативным способом получения порядкового номера столбца является применение метода GetOrdinal как показано в листинге 4.5.

Листинг 4.5. метода Создание объекта SqlCommand cmd = = "SELECT * FROM INVOICE";

Глава 4. Получение данных - new // Все правильно.

rdr = // Неверный код будет SqlDataReader rdr = new Функциональность объекта DataReader Постараемся разобраться в функциональности, заложенной в объект DataReader.

В главе 1 говорилось о возможности работать с данными в отсоединенном режиме, однако это совершенно не применимо к объекту DataReader. На всем протяжении его жизни (при чтении или даже просто при просмотре строк) соединение оста ваться открытым. объект DataReader очень напоминает своих предше ственников, предполагавших обработку данных в подсоединенном режиме.

Так как при работе с объектом DataReader соединение остается открытым, он мо жет не извлекать за один раз все полученные в результате выполнения запро са. В контексте проектов, ориентированных на работу со складом данных или шой корпоративной базой данных, это имеет решающее значение. Действительно, ведь требование вернуть за один раз миллион записей, которые должны быть разме щены в памяти, — это сущее Объект DataReader позволяет оптимальным способом решить эту проблему, загружая в память за один раз небольшую часть ре набора данных.

Поскольку объект DataReader предполагает работу с данными в подсоединенном режиме, некоторые разработчики не решаются его отдавая предпоч тение объекту DataSet. Более подробно выбор между использованием объектов DataReader и DataSet обсуждается в главе 7.

Доступ к данным с помощью объекта DataReader Как было показано в листинге объект DataReader позволяет доступ к отдельным представляя результирующий набор данных в виде массива.

Посредством индексатора указывается либо порядковый номер либо его имя (листинг 4.2).

Листинг 4.2. Доступ к столбцам набора данных с помощью объекта DataReader Создание объекта команды.

SqlCommand cmd - "SELECT * FROM CUSTOMER";

// Создание объекта DataReader в результате выполнения запроса.

SqlDataReader rdr // Итерация по while { // Эти две строки кода // имеют одинаковую // 30 Часть /.

WHERE = ORDER BY DESC Во всех приведенных выше примерах используется синтаксис SQL-92. Как прави ло, все разработчики баз данных (включая Microsoft, IBM и Oracle) следуют этому синтаксису, несмотря на то что каждый из них создал собственное расширение языка SQL для SQL Server и PL/SQL для Oracle). говоря, написа ние универсального — очень сложное занятие. Если вам крайне необходи мо написать именно такой код, будьте готовы к снижению производительности вашей поскольку каждый разработчик баз данных реализует механизм выполнения по-своему.

Объект DataReader После того как вы научились создавать запросы, пришло время научиться их выпол нять, используя для этого средства В отличие от большинства других интер фейсов доступа к данным, в которых для считывания и изменения данных используется один и тот же объект, в операции чтения и изменения данных разделены.

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

Объект DataReader обеспечивает доступ к данным, извлеченным в результате вы полнения SQL-запроса, в режиме только для чтения. Объект DataReader представлен OleDbDataReader, SqlDataReader, OracelDataReader и OdbcDataReader — в каждом из управляемых Пример использования класса SqlDataReader приведен в листинге 4.1.

Листинг 4.1. Использование // Создание объекта = "SELECT * FROM CUSTOMER";

объекта DataReader в результате выполнения запроса.

SqlDataReader rdr // Итерация по всем строкам.

while I В этом примере приведен наиболее простой способ использования объекта DataReader. Для по результирующему набору данных предназначен метод Обратите что метод О обязательно должен быть вызван один раз до считывания первой записи. Благодаря этому объект DataReader более устой чив к ошибкам, так как строкового курсора и проверка достигнут ли конец набора данных, производится в одном операторе. По достиже нии конца результирующего набора данных метод Read возвратит Создание объекта DataReader Объект DataReader не может быть создан напрямую из клиентского кода — он соз дается объектом команды во время ее с метода ExecuteReader Следует отметить, что конструктор скрыт, для того чтобы предотвратить создание нового объекта DataReader:

Глава 4. Получение данных SELSECT FROM Customer WHERE State = Еще одним из часто использующихся предложений является JOIN, из влекать связанную информацию из разных таблиц. Так, предложение JOIN позволит нам получить не только сведения о клиенте, но и информацию о каждом соответствую щем ему счете. Ниже приведен простой пример использования предложения JOIN:

SELECT HomePhone, FROM Customer JOIN Invoice ON = В этом примере таблицы Customer и Invoice объединяются с общего для обоих поля CustomerlD. При желании можно добавить еще одно предложение JOIN объединения таблиц Invoice и SELECT FirstName, LastName, HomePhone, InvoiceNumber, Quantity, ProductID FROM Customer JOIN Invoice ON = JOIN Invoiceltem ON = Для того чтобы ограничить поиск только счетами клиентов с фамилией Maddux, добавим предложение WHERE:

SELECT FirstName, LastName, InvoiceNumber, Quantity, ProductID FROM Customer JOIN Invoice ON JOIN Invoiceltem ON И, наконец, применим предложение ORDER BY для ре зультата по дате оформления счета. Для этого добавим ORDER BY в коней оператора SELECT и укажем столбец, по которому будет производиться сортировка, например:

SELECT LastName, HomePhone, InvoiceNumber, Quantity, ProductID FROM Customer JOIN Invoice ON JOIN ON WHERE = ORDER Если необходимо сортировать по а не по возрастанию (стандартный порядок сортировки), добавьте в конец оператора ключевое слово DESC:

SELECT FirstName, LastName, InvoiceNumber, Quantity, ProductID FROM Customer JOIN Invoice ON = JOIN Invoiceltem ON = 78 Часть Основы о языке SQL, обратитесь к Web-узлу Кроме того, стоит обратить вни мание на замечательные книги Джо Келко (Joe Instant SQL Programming (ISBN:

1874416509) и, для более опытных SQL for Advanced SQL Programming 1558605762).

Чтобы считывать данные с SQL-запроса, необходимо досконально изу чить возможности оператора SELECT, который используется для извлечения из базы данных набора информации. Предположим, что у нас есть четыре ные на рис. 4,1 (все они взяты из базы данных, в примерах этой книги — см. Web-узел по адресу: com/book).

Рис. в примерах этой книги Ниже приведен пример простейшего запроса к базе данных:

SELECT FROM Customer В результате выполнения этого запроса из базы данных извлекаются все строки таблицы Customer. Символ звездочки (*) — это маска, которая указывает базе дан ных на необходимость извлечения всех столбцов таблицы. Предложение FROM опреде ляет таблицу базы данных, из которой нужно считать информацию;

в нашем случае это таблица Customer.

В большинстве случаев нам не нужны все строки таблицы. Одним из распростра ненных требований к извлекаемым из таблицы данным является их сортировка в оп ределенном порядке. В этом случае символ звездочки заменяется именами столбцов, которые необходимо извлечь. В следующем примере база данных возвратит столбцы и Phone таблицы SELECT Phone FROM Customer SQL поддерживает функции агрегирования, такие как COUNT (используется для подсчета записей, которые были бы возвращены в результате выполнения запроса) и МАХ (используется для получения максимального значения определенного поля):

SELECT FROM Customer SELECT FROM Product Кроме того, часто из таблицы необходимо извлечь только подмножество данных.

Для этого используется предложение WHERE. Например, в результате выполнения сле дующего запроса из базы данных будут извлечены строки таблицы Customer, содер о клиентах, проживающих в штате Массачусетс:

Глава 4. Получение данных Глава Получение данных Чтение данных Объект Создание простого приложения, взаимодействующего с данных Моя первая работа в качестве программиста заключалась в написании отчетов для давно забытой реляционной базы данных в системе В те дни написа ние отчетов состояло в простом считывании из базы данных и выводе на консоль множества строк текста. Несмотря на то что в современных системах написа ние отчетов для баз данных стало гораздо сложнее, в корне так ничего и не измени лось — сначала данные откуда-то считываются, а затем куда-то выводятся.

Одним из первых правил, которые я использовал при программировании доступа к базам данных, являлось правило "пожарного шланга", применявшееся для уменьше ния нагрузки на СУБД. шланг" hose) — это термин, который обозна чал запрос у базы данных однонаправленного курсора. Сообщив базе данных о своих намерениях, можно ускорить процесс чтения данных и уменьшить объем использо вания ресурсов. В концепция курсора представлена объектом DataReader. Именно о нем и пойдет речь в этой главе.

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

Язык структурированных запросов (Structured Query Language — SQL) — это язык баз данных. SQL-86, SQL-92 и SQL-99 — стандарты языка SQL, разработанные националь ным институтом стандартизации США (American National Standards Institute — ANSI) и большинством производителей баз данных. К тому же язык SQL используется для извлечения информации из всех поставщиков Microsoft.

Приведенные в этой книге фрагменты SQL-кода основаны на стандарте SQL-92, Краткий обзор SQL-оператора SELECT Язык SQL — слишком обширная тема для того, чтобы рассматривать ее страни цах этой книги, тем не менее я попытаюсь кратко рассказать вам об основах построе ния запросов к базе данных. Для того чтобы получить более подробную информацию Вот видите какой я старый и мудрый! Для того чтобы познакомиться с системой СР/М, обратитесь по адресу:

76 Часть I. Основы // Перебор всех строк заданного результирующего while ) { } // После просмотра первого результирующего набора // данных переходим к следующему результирующему } while Резюме Несмотря на то что ориентирована на работу с данными в отсоединенном режиме, иногда нам все же нужно подключаться к базе данных. Приведенная в этой главе информация будет использоваться в каждом написанном вами приложении — способ, избежать выполнения команд еще не придуман. В последующих главах этой книги мы сконцентрируемся на обработке полученных данных.

Глава 3. команд В данном примере для выполнения пакетного запроса был использован метод Хотя указанный метод эффективен, он, к сожалению, не обеспечивает обработку ошибок. Для разрешения этой проблемы вам нужно собст венноручно добавить SQL-код с проверкой ошибок и (необязательной) поддержкой транзакций:

TRAN INTO Customer Phone, VALUES IF <> BEGIN ROLLBACK TRAN END INSERT INTO Invoice InvoiceDate, Terms] VALUES IF О О BEGIN ROLLBACK TRAN RETURN END COMMIT TRAN Теперь наш пакетный запрос будет две задачи: осуществлять новой информации в базу данных и проводить проверку на наличие ошибок. Если во время выполнения произошел сбой, она будет отклонена, в противном случае — Пакетный запрос — это не что иное, как несколько запросов, объединенных в одну команду. Поскольку поддерживает множественные результирующие наборы данных, нет никакой серьезной причины не использовать пакетные запросы (листинг Листинг 3.19. Получение множественных наборов данных с помощью объекта DataReader Создание команды, включающей в себя два запроса SELECT // и возвращающей два результирующих набора cmd = ;

"SELECT * FROM + SELECT * FROM // Создание объекта DataReader для навигации по // наборам SqlDataReader rdr = // Перебор всех строк в каждом результирующем // наборе данных.

do •:

74 /. Основы INSERT INTO Invoice VALUES (newid(), В листинге 3.17 приведен ADO.NET-код, в котором используются обычные (а не пакетные) запросы.

Листинг Использование обычных запросов string = "INSERT INTO Customer + + "VALUES + string = "INSERT INTO + (CustomerlD, LastName, Phone, Zip) "VALUES + // Подключение к базе sqlconn new = sqlconn = // Первый запрос.

= sQueryl;

// Второй запрос.

Несмотря на то что этот код будет выполняться, он неэффективен, так как требует трех обращений к базе данных. Его можно упростить, воспользовавшись пакетными запросами, как показано в листинге 3.18.

Листинг Использование пакетных запросов string sQuery = "INSERT INTO LastName, FirstName, Phone, Zip) + "VALUES + + "INSERT INTO Customer + LastName, Phone, Zip) + "VALUES + SqlConnection sqlconn new SqlCommand sqlcmd = // Пакетный запрос.

= sQuery;

Глава З. команд InvoiceDate, Terms) + "VALUES } { // Отклонить транзакцию то точки сохранения // момента внесения в базу данных информации о // новом клиенте.

// Если выполнение было передано этому оператору, // принять транзакцию.

Commit } ex) t + Rolled back", } В этом коде мы создали точку сохранения с именем Customer" (Новый кли ент). Если занесение в базу данных информации о новом клиенте прошло однако во время создания нового счета произошла ошибка, нам не хотелось бы терять уже внесенные в базу данных сведения о новом клиенте. Поэтому мы отклоняем транзакцию вплоть до точки сохранения и затем принимаем ее.

Службы Enterprise Services и СОМ+ Иногда транзакции баз довольно масштабными, включающими в себя выполнение ряда операций на нескольких серверах. Для поддержки подобных транзакций в предусмотрены службы Enterprise Services, которые являются оболочками для компонентов Следует отметить, что в этой книге службы Enter prise Services не описываются. Если вам необходима какая-либо инфор мация по программированию транзакций, советую обратиться к великолепной книге Тима (Tim Programming или к его статье Integration: How Enterprise Services Can Help You Build Distributed Applications", ко торую можно найти на Web-узле MSDN по адресу:

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

INTO Customer Phone, Zip) VALUES (newid(), 72 Часть I. Основы • RepeatableRead. не может считывать данные, которыми манипу лируют другие незавершенные транзакции. Иными словами, данные, которыми манипулирует транзакция, на время ее работы блокируются.

• Serializable. Транзакция полностью изолирована от других транзакций.

Несмотря на то что перечисление IsolationLevel имеет атрибут Attribute, использовать комбинацию нескольких уровней изоляции, подобная функциональность не поддерживается в силу специфики элементов перечисления.

по умолчанию уровень изоляции конечно не может идеально подходить для каждого случая.

Pages:     | 1 || 3 | 4 |



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

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