Этюды в тональности C#. ADO.NET — работа с базами данных

Этюды в тональности C#. ADO.NET — работа с базами данных

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

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

Повсеместное развитие Интернет внесло свои коррективы в этот накатанный годами алгоритм работы с базами данных. Идеология, при которой множество клиентов может быть одновременно подключено к одному серверу, и при этом отсутствует какая-либо возможность выяснить, активен ли сейчас каждый клиент или, напротив, давно отключился, очень плохо ложилась на существующие механизмы работы с базами данных. Программисты выкручивались, как могли, и вскоре пришли к такому, ставшему почти стандартом, алгоритму работы: клиент подключается к базе, считывает себе порцию данных и отключается от сервера, продолжая обрабатывать данные на своей стороне. Если ему требуется удалить из базы данных какие-то записи или модифицировать значение их полей, он повторно подключается к серверу и посылает ему команду на совершение необходимых ему действий, после чего снова считывает очередную порцию данных и отключается, продолжая свою работу в "оффлайне".
В очередных версиях MDAC Майкрософт попыталось облегчить программисту работу в подобном отсоединенном режиме. Появились понятия серверного курсора и отключенного рекордсета, но эти технологии реализованы были достаточно сложно и несколько неуклюже и поэтому не получили широкого распространения в создаваемом программистами коде программ. Интернет также развивался, и на первый план в его среде стал выходить язык XML. Применение XML для построения баз данных позволяло строить структуры значительно более сложные, чем обычные не связанные таблицы реляционных баз данных. Предположим, у нас имеется следующий XML файл:

<personal>
<otdel>
<name> Отдел кадров</name>
<worker> Вася Пупкин
<child> Света</child>
</worker>
<worker> Федя Сергеев</worker>
</otdel>
<otdel>
<name> Гараж</name>
<worker> Сергей Федин</worker>
</otdel>
</personal>

Эта несложная структура данных уже раскладывается, по правилам реляционных баз данных, в три разные таблицы с достаточно сложными связями между ними. К слову, чувствуете мощь и выразительность XML, как языка баз данных? Так как средств чтения записи в XML в ADO не было, то программисту приходилось немало извращаться для того, чтобы практически "вручную" разобрать структуры XML перед записью их в таблицы базы данных или, напротив, из имеющихся таблиц построить корректный файл XML. Майкрософт на скорую руку были сделаны "костыли" поддержки XML в своих продуктах Microsoft SQL Server 2000 и MDAC, но назвать работу с ними удобной мог только новичок, никогда ранее не работавший с прежними версиями их программных API.

Реализовать достаточно сложную "отсоединенную" таблицу XML силами ADO так и вовсе практически невозможно, так как, несмотря на наличие в API понятия отсоединенного рекордсета, объединить содержимое нескольких таких рекордсетов связями нельзя. Нет, разумеется, вы вполне можете вручную реализовать подобное объединение, но задача эта весьма нетривиальна, особенно при условии, что данные могут быть изменяемыми. Да и вообще, модификация данных, хранящихся в отсоединенных рекордсетах ADO, это отдельная головная боль. Стандартной практикой администраторов баз данных является создание хранимых процедур для обновления данных в подведомственных им таблицах, а отсоединенные рекордсеты не могут передавать отложенные изменения через хранимые процедуры. Шероховатостей, подобных вышеперечисленным, накопилось так много, что в Майкрософт не стали в очередной раз латать дыры, а вместо этого выпустили принципиально новый механизм работы с базами данных названный ADO.NET. Давайте вместе посмотрим, что же он принес программисту.

Нововведения в ADO.NET
Написав заголовок этой главы, я задумался. Не столько о том, с чего начать, сколько о том, как провести параллель между ADO и ADO.NET. Дело в том, что параллелей между ними практически нет. Несмотря на схожесть в названии и одинаковое предназначение, это принципиально разные движки баз данных. Давайте поступим так: я начну вам рассказывать об устройстве ADO.NET и, если по ходу изложения материала наткнусь на схожесть в их реализации, сразу и проведу параллель. Ок? Ну, поехали.

Класс DATASET
Класс DataSet является центральным классом в иерархии ADO.NET. Обрабатывая данные в программе, вы чаще всего будете общаться именно с ним. Класс DataSet содержит в себе набор из одной или нескольких таблиц баз данных, вместе с информацией об их полях и связях таблиц между собой. То есть, в чем-то он похож на каталог ADOX.

Также в DataSet содержится информация об SQL запросах, необходимых для заполнения этого объекта данными, модификации, вставки и удаления данных на сервере. Все четыре операции программируются и действуют независимо друг от друга, поэтому вы вполне можете реализовать свою собственную, даже весьма замысловатую логику работы с сервером базы данных. К примеру, получить данные в виде XML от веб-сервиса, обработать их так, как вам нужно, а затем добавить в базу Microsoft SQL Server, попутно вернув обновленные данные веб сервису. Разумеется, все четыре метода поддерживают хранимые процедуры. Любопытным свойством DataSet является и то, что вам никогда не придется работать с этими запросами вручную. Для вас, как программиста, предусмотрены наборы объектов, в которых хранятся измененные вами записи. В любой момент вы можете просмотреть сделанные вами изменения и, если вам это зачем-то нужно, вернуть их к исходному значению. Наигравшись с записями в таблице, вы вызываете метод, ответственный за обновление данных на сервере, и этим даете ход вашим изменениям. Вы даже можете при этом и восе не знать языка SQL. Мастера Visual Studio сделают всю черновую работу за вас. В качестве ложки дегтя в бочку меда достоинств от использования мастеров замечу, что код SQL запросов они генерируют совершенно жуткий и неэффективный.

Почему-то все, кто пишет статьи об DataSet, сразу же начинают рассказывать о широких возможностях, предоставляемых им при работе с промышленными серверами баз данных. При этом авторы откладывают на потом одну немаловажную (и неочевидную) особенность этого класса ADO.NET. А особенность эта заключается в том, что DataSet абсолютно ничего не знает о конкретном сервере базы данных, с которым он работает. Другими словами, это полностью автономный объект! Ему совершенно безразлично, откуда взялась записанная в нем структура базы данных. Не играет никакой роли, считана ли она с реальной базы данных SQL Server или же создана вами вручную "на ходу". Вся его функциональность, в обоих случаях, будет полностью аналогичной. Вы вполне можете создать "с нуля" экземпляр DataSet, заполнить его данными из некоего бинарного файла и впоследствии пользоваться всей мощью функций этого объекта для их анализа. Эта способность DataSet предоставляет программисту широкие возможности по переносу уже имеющегося программного кода на платформу Net Framework.
Единственным видом базы данных, изначально поддерживаемым самим классом DataSet, являются файлы в формате XML. В любой произвольный момент времени вы можете сгрузить хранящиеся в DataSet таблицы в этот тип файла или, напротив, загрузить их оттуда. При этом в файле сохраняются не только сами таблицы, но и связи между ними. Также DataSet позволяет сохранять в файле XML только структуру базы данных, ее схему. Эта возможность класса довольно удобна для создания баз данных по готовым шаблонам. Шаблон вы можете считать с диска или, скажем, получить в виде потока, например, с некоего веб сервиса в Интернет.

По своему внутреннему устройству, объект DataSet представляет собой нечто вроде матрешки. В классе имеется коллекция (массив) Tables, описывающая входящие в него таблицы. Каждой таблице в коллекции соответствует свой собственный объект DataTable, который, в свою очередь, содержит по одному объекту DataColumn для каждого из своих полей. Все эти объекты можно удалять или, напротив, добавлять "на ходу", в соответствии с вашими текущими потребностями. У всех объектов в наличии имеется большое количество свойств, предназначенных для тонкой подстройки их функционирования, и не меньшее количество методов, предназначенных для выполнения типовых задач.
Со многими из этих методов мы познакомимся во время этого моего краткого практикума, но еще большее их количество останется за бортом моего повествования. Как и обычно, я даю вам только общий поверхностный обзор технологии, мы с вами совершаем обзорный полет вокруг верхушки айсберга. Если вы заинтересуетесь ADO.NET и захотите узнать о ней побольше, я вам рекомендую обратиться к книге Давида Сеппа "Microsoft ADO.NET". Книга выпущена издательско-торговым домом "Русская Редакция" в 2003 году. Это, пожалуй, лучшее из руководств по этой технологии из тех, что попадались мне на глаза.
Как известно, лучшим способом разобраться с языком программирования является создание программ с его помощью. Давайте и мы с вами поиграем с возможностями, предоставляемыми классом DataSet, и напишем тестовое приложение. Создавая объект DataSet "с нуля", нам с вами предварительно необходимо создать все его внутренние объекты типа таблиц и полей, а затем задать связи между ними, таким образом сложив матрешку нашей будущей базы данных.

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

Второе служебное поле, parentid, — это ссылка на поле rowid, "предка" нашей записи в иерархии базы данных. Для записей в таблице работников предприятия worker, такими "предками" служат записи таблицы их подразделений otdel, а для записей таблицы детей, child, предком являются записи таблицы worker. Так как поле rowid у каждой записи уникально, то, сославшись на него в поле parentid, мы однозначно связываем одну запись с другой.
Итак, на данном этапе мы с вами готовы приступить к кодированию приложения. Я надеюсь, вы уже в достаточной мере изучили язык C# для того, чтобы самостоятельно собрать готовый пример из кусков кода, которые я вам сейчас приведу. Дело в том, что пример довольно объемный, и приводить вам его одним большим куском мне не хочется. Лучше я буду давать вам код небольшими фрагментами, попутно комментируя их назначение. Для того чтобы получить полную программу, вам достаточно просто сложить все приведенные мной фрагменты кода вместе в единое целое.

Начнем мы нашу программу, как и обычно, с подключения пространства имен. Помимо ставшего уже привычным пространства System, нам, на этот раз, потребуется подключить еще и пространство, называющееся System.Data. Именно в нем и проживает большинство классов отсоединенной модели данных ADO.NET.

using System;
using System.Data;

Подключив пространства имен, мы с вами теперь объявим новый класс, являющийся потомком класса System.Data. DataSet. Собственно говоря, мы бы могли обойтись и без наследования базового класса, построив наш пример в стиле "процедурного программирования с использованием чужих объектов". Я считаю такую практику, к слову, активно навязываемую большинством учебников по программированию, порочной и недостойной настоящего ООП программиста. А так как это моя статья, то я буду писать свои примеры так, как я хочу. Сказано — создаем класс наследник — значит, создаем класс наследник и точка!

class Dataset1:System.Data.DataSet {
}

Следующим этапом мы с вами опишем конструкторы нашего класса. Их у нас будет два. Один, не имеющий параметров, создает класс Dataset "с нуля". В момент создания класса Dataset1 в нем создаются три таблицы otdel,workers и child. Затем к этим вновь созданным таблицам добавляются поля и, напоследок, между таблицами устанавливаются реляционные связи. Последним шагом я сохраняю получившуюся структуру базы данных в файл стандарта XML. Этот файл нам потребуется во втором конструкторе нашего объекта. В этом конструкторе мы создадим новую базу данных, воспользовавшись данным файлом как шаблоном. Разумеется, в реальных приложениях сохранять сформированную базу данных в файл вовсе не обязательно.

public Dataset1(){
// Создаем таблицы
this.Tables.Add(new DataTable("otdel"));
this.Tables.Add(new DataTable("worker"));
this.Tables.Add(new DataTable("child"));
// Создаем поля в таблицах
// Таблица отделов
this.Tables["otdel"].Columns.Add(new DataColumn("rowid",typeof(long)));
this.Tables["otdel"].Columns["rowid"].AutoIncrement=true;
this.Tables["otdel"].Columns.Add(new DataColumn("name",typeof(string)));
// Таблица работников
this.Tables["worker"].Columns.Add(new DataColumn("rowid",typeof(long)));
this.Tables["worker"].Columns["rowid"].AutoIncrement=true;
this.Tables["worker"].Columns.Add(new DataColumn("parentid",typeof(long)));
this.Tables["worker"].Columns.Add(new DataColumn("name",typeof(string)));
// Таблица детей
this.Tables["child"].Columns.Add(new DataColumn("rowid",typeof(long)));
this.Tables["child"].Columns["rowid"].AutoIncrement=true;
this.Tables["child"].Columns.Add(new DataColumn("parentid",typeof(long)));
this.Tables["child"].Columns.Add(new DataColumn("name",typeof(string)));
// Добавляем отношение между таблицами
DataColumn parentCol;
DataColumn childCol;
// Таблицы Otdel и Worker.
parentCol= this.Tables ["otdel"].Colu-mns["rowid"];
childCol= this.Tables["worker"].Colu-mns["parentid"];
this.Relations.Add(new DataRelation ("workerRelation",parentCol,childCol));
// Таблицы Worker и Child.
parentCol= this.Tables["worker"].Colu-mns["rowid"],
childCol= this.Tables["child"].Columns ["parentid"]));
this.Relations.Add(new DataRelation ("childRelation", parentCol,childCol));
// Пишем полученную структуру в файл для дальнейшего использования.
this.WriteXmlSchema("xml1.xml");
}

А вот и обещанный второй конструктор. Как я вам рассказывал выше, в этом конструкторе мы загружаем структуру базы данных из файла стандарта XML. Параметром конструктора является строка с именем файла.

public Dataset1(string TemplateFile){
this.ReadXmlSchema(TemplateFile);
}
Для того чтобы вы смогли пронаблюдать результат работы конструкторов, я написал метод, выводящий на экран структуру заданной в DataSet1 базы данных.
public void ShowTablesInfo(){
// перебираем в цикле все таблицы и выводим имеющиеся в ней поля.
foreach(DataTable tbl in this.Tables){
Console.WriteLine("\n[+]"+tbl. TableName);
foreach(DataColumn column in tbl. Columns){
Console.WriteLine("|-"+column. ColumnName+":"+column.DataType+";");
}
}
// выводим все имеющиеся связи между таблицами.
Console.WriteLine("\nRelations:");
foreach(DataRelation rel in this.Relations){
Console.WriteLine("[+]"+rel.Relation Name);
for(int i=0;i<rel.ParentColumns. Length;i++) {
Console.Write(" |-" +
rel.ChildTable.TableName+"."+
rel.ChildColumns[i].ColumnName+"-> ");
Console.WriteLine(
rel.ParentTable.TableName+"."+
rel.ParentColumns[i].ColumnName);
}
}

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

class MainApp{
static void Main(string[] args){
// Создаем Dataset обычным путем
Dataset1 ds1 = new Dataset1();
ds1.ShowTablesInfo();
// Создаем Dataset с помошью файла XML схемы.
Dataset1 ds2 = new Dataset1 ("xml1.xml");
ds2.ShowTablesInfo();
Console.ReadLine();
}
}
Запустите это приложение на исполнение. Если вы все набрали правильно, в обоих случаях вы получите на экране совершенно одинаковые строчки текста.

Продолжение следует.


Компьютерная газета. Статья была опубликована в номере 11 за 2004 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета