Читать в оригинале

<< ПредыдущаяОглавлениеСледующая >>


4.4. Работа с таблицами данных

4.4.1. Объекты DataSet, DataTable и DataColumn

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

Всякая таблица состоит из столбцов (называемых также полями или колонками) и строк (записей). Для обращения к ним и для управления столбцами и строками в объекте DataTable предназначены специальные объекты - DataColumn и DataRow. Между таблицами могут быть связи - здесь они представлены объектом DataRelation. Наконец, в таблицах есть первичные и вторичные ключи - объект Constraint со своими двумя подклассами UniqueConstraint и ForeighKeyConstraint описывают их. Отметим, что объекты программы и соответствующие объекты базы данных не эквивалентны.

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

DataSet = <одна или несколько таблиц> = <один или несколько объектов DataTable>.
DataTable = <таблица>.
DataTable = <таблица> = <множество полей, столбцов, колонок> =
                 = <множество объектов DataColumn>.
DataTable = <таблица> = <множество строк> = <множество объектов DataRow>.
DataColumn = <столбец, поле, колонка>.
DataRow = <строка>.
DataRelation = <связь между таблицами>.

Возникает вопрос: для чего нужны эти объекты, если можно обходились и без них для вывода содержимого таблицы, например в элемент DataGridView? Дело в том, что для простого отображения информации создавать эти объекты не требуется, но в этом случае все данные будут однородными текстовыми переменными, подобно таблицам в документе Microsoft Word. DataSet не может сам сформировать структуру данных - тип переменных, первичные и вторичные ключи, связи между таблицами. Для управления такой структурой, для ее адекватного отображения (например, вывод информации с привязкой к элементам, создаваемым в режиме работы приложения) и нужно определение этих объектов.

 

Программное создание объектов DataTable и DataColumn

 

Как было сказано выше, все объекты ADO можно создать программно. Например, создание таблицы и столбцов:

DataSet dsTests = new System.Data.DataSet();
//
// dsTests
//
dsTests.DataSetName = "NewDataSet";
dsTests.Locale = new System.Globalization.CultureInfo("ru-RU");

Отметим, что в последней строке указывается информация о национальных настройках таблицы: ru-RU. Она необходима в том случае, если разрабатываемое приложение будет использоваться вне страны, в которой оно было разработано (в данном случае - России).

Создадим теперь объект DataTable для таблицы Questions (вопросы):

DataTable dtQuestions = dsTests.Tables.Add("Questions");
//Или
//DataTable dtQuestions = new DataTable("Questions");
//dsTests.Tables.Add(dtQuestions);

Здесь создается экземпляр dtQuestions объекта DataTable, затем вызывается метод Add свойства Tables объекта dsTests, которому передается название таблицы Questions. Далее создаем поля в объекте dtQuestions:

DataColumn dсQuestID = dtQuestions.Columns.Add("questID", typeof(Int32)); dсQuestID.Unique = true;
DataColumn dcQuestion = dtQuestions.Columns.Add("question");
DataColumn dcQuestType = dtQuestions.Columns.Add("questType", typeof(Int32));

Мы создаем поля, нужные для отражения соответствующих столбцов в таблице Questions. Перегруженный метод Add свойства Columns объекта dtQuestions позволяет задавать название столбца и его тип данных (рис. 105).

Рис. 105. Создание поля

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

Точно так же создаем объект DataTable для таблицы Variants (варианты ответов) и соответствующие поля:

//Создаем таблицу "Variants"
DataTable dtVariants = dsTests.Tables.Add("Variants");
//Заполняем поля таблицы "Variants"
DataColumn dcID = dtVariants.Columns.Add("id", typeof(Int32));
dcID.Unique = true;
dcID.AutoIncrement = true;
DataColumn dcVariantQuestID = dtVariants.Columns.Add("questID", typeof(Int32));
DataColumn dcVariant = dtVariants.Columns.Add("variant");
DataColumn dcIsRight = dtVariants.Columns.Add("isRight", typeof(Boolean));

Здесь мы дополнительно установили свойству AutoIncrement объекта dcID значение true. Свойство AutoIncrement (счетчик) позволяет создать счетчик для поля, аналогичный типу данных «Счетчик» в Microsoft Access.

Теперь приступим к созданию связи между таблицами. В базе данных между родительской таблицей Questions и дочерней Variants была бы установлена связь по полю questID, которое присутствует в обеих таблицах. При программном создании объектов для поля questID таблицы Questions был создан объект dсQuestID, для этого же поля таблицы Variants создан объект dcVariantQuestID. В коде создание связи между таблицами будет иметь следующий вид:

DataRelation drQuestionsVariants = new DataRelation("QuestionsVariants",
                          dсQuestID, dcVariantQuestID);
dsTests.Relations.Add(drQuestionsVariants);

Здесь в конструкторе объекта DataRelation имя drQuestionsVariants - название экземпляра объекта (класса) DataRelation, а строка "QuestionsVariants" - передаваемое конструктору свойство relationName - название связи, которая будет содержаться в создаваемом объекте drQuestionsVariants. Другими словами, drQuestionsVariants - название экземпляра DataRelation, которое будем использовать в коде, а свойство relationName - всего лишь название отражаемой связи, которую можно удалить или переименовать.

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

 

Свойство PrimaryKey

 

Мы рассмотрели способ конструирования структуры таблицы в объекте DataSet, а также как определять отношения между таблицами. Во всех случаях для выделения первичного ключа в таблице использовалось свойство Unique. Например, первичный ключ «Код туриста» для таблицы «Туристы» определялся так:

DataColumn dcTouristID = new DataColumn("Код туриста", typeof(int));
dcTouristID.Unique = true;

А для таблицы вариантов ответов «Variants»:

DataColumn dcID = dtVariants.Columns.Add("id", typeof(Int32));
dcID.Unique = true;
dcID.AutoIncrement = true;

Для вывода таблиц идентификации записей этого определения вполне хватает. Однако свойство Unique всего лишь указывает на уникальность заданного поля, т. е. на отсутствие повторяющихся записей.

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

DataTable dtTourists = new DataTable("Туристы");
DataColumn dcTouristID = new DataColumn("Код туриста", typeof(int));
dtTourists.PrimaryKey = new DataColumn [] {dtTourists.Columns["Код туриста"]};

В сокращенной записи определение будет такое:

dtTourists.Columns.Add("Код туриста", typeof(int));
dtTourists.PrimaryKey = new DataColumn [] {dtTourists.Columns["Код туриста"]};

Можно применять комбинацию полей для задания первичного ключа:

DataTable dtTourists = new DataTable("Туристы");
DataColumn dcTouristID = new DataColumn("Код туриста", typeof(int));
DataColumn dcLastName = new DataColumn("Фамилия",typeof(string));
dtTourists.PrimaryKey = new DataColumn [] {dtTourists.Columns["Код туриста"],dtTourists.Columns["Фамилия"]};

Здесь первичным ключом будут значения поля «Код туриста» в сочетании со значением поля «Фамилия».

После определения первичного ключа объекта DataTable для свойства AllowDBNull (разрешение значений null) объектов DataColumn, формирующих ключ, будет установлено значение false.

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

 

Ограничения UniqueConstraint и ForeignKeyConstraint

 

Теперь осталось разобраться, как можно определять некоторые свойства таблиц, называемые ограничениями. Свойство Constraint (ограничения) объекта DataTable бывает двух типов - UniqueConstraint и ForeignKeyConstraint.

Свойство UniqueConstraint определяет первичный ключ таблицы, например, в таблице Questions ключевым полем является questID. Объект dсQuestID представляет это поле:

DataColumn dсQuestID = dtQuestions.Columns.Add("questID", typeof(Int32));

Ограничение UniqueConstraint, налагаемое на объект dсQuestID, запрещает появление дублированных строк:

UniqueConstraint UC_dtQuestions = new UniqueConstraint(dcQuestID);
dtQuestions.Constraints.Add(UC_dtQuestions);

Однако при создании объекта dсQuestID мы ведь уже определяли его уникальность:

dсQuestID.Unique = true;

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

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

- UpdateRule - применяется при изменении родительской строки;

- DeleteRule - применяется при удалении родительской строки;

- AcceptRejectRule - применяется при вызове метода AcceptChanges объекта DataTable, для которого определено ограничение.

Для каждого из этих правил, кроме последнего AcceptRejectRule, могут применяться следующие значения:

- Cascade - каскадное обновление связанных записей;

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

- SetDefault - полю внешнего ключа в дочерних записях присваивается значение, заданное в свойстве DefaultValue этого поля;

- SetNull - полю внешнего ключа в дочерних записях присваивается значение Null.

Правило AcceptRejectRule принимает значения только Cascade или None. Значением по умолчанию для правил UpdateRule и DeleteRule является Cascade, для правила AcceptRejectRule - None.

Создадим ограничение для связи QuestionsVariants:

ForeignKeyConstraint FK_QuestionsVariants = new ForeignKeyConstraint(dtQuestions.Columns["questID"],
               dtVariants.Columns["questID"]);
dtVariants.Constraints.Add(FK_QuestionsVariants);

Здесь задается вначале родительская колонка, а затем дочерняя (рис. 106). Добавлять созданное ограничение следует к объекту DataTable, представляющему дочернюю таблицу (в данном случае - объект dtVariants).

Рис. 106. Создание ограничения FK_QuestionsVariants

Этот фрагмент кода оставляет значения правил UpdateRule, DeleteRule и AcceptRejectRule заданными по умолчанию, т. е. Cascade и None, что соответствует значениям по умолчанию. Конструктор объекта является перегруженным.

 

Создание столбцов, основанных на выражении

 

При создании базы данных не следует помещать в нее значения, которые могут быть получены из уже имеющихся данных. Ранее была создана база данных BDTur_firm.mdb, в которой есть таблица «Туры». В этой таблице имеется поле «цена», где указывается стоимость туров. На практике может понадобиться значение цены, выраженное в различных валютах, значение с учетом налогов, значение со скидкой и т. п. Для каждого конкретного случая можно получить дополнительное поле (вычисляемое), не вводя его в саму базу данных.

Технология ADO .NET позволяет создавать объекты DataColumn, основанные на выражении. Создадим новое Windows-приложение.

В коде формы, подключим пространство имен: using System.Data.OleDb;

В классе формы определяем строки CommandText и ConnectionString:

string commandText = "SELECT Информация, [Код тура], Название, Цена FROM Туры";
string connectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" +
                          @”D:\ВМИ\For ADO\BDTur_firm.mdb";

В конструкторе формы программно создаем все объекты для вывода таблицы «Туры» в элемент управления DataGridView:

public Form1() {
         InitializeComponent();
         OleDbConnection conn = new OleDbConnection(connectionString);
         OleDbCommand myCommand = new OleDbCommand();
         myCommand.Connection = conn;
         myCommand.CommandText = commandText;
         OleDbDataAdapter dataAdapter = new OleDbDataAdapter();
         dataAdapter. SelectCommand = myCommand;
         DataSet dsTours = new DataSet();
         DataTable dtTours = dsTours.Tables.Add("Туры");
         DataColumn ddDtour = dtTours.Columns.Add("Код тура", typeof(Int32));
         ddDtour.Unique = true;
         DataColumn dcName = dtTours.Columns.Add("Название");
         DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
         DataColumn dcInformation = dtTours.Columns.Add("Информация");
         conn.Open();
         dataAdapter.Fill(dsTours.Tables["Туры"]);
         conn.Close();
         dataGridl.DataSource = dsTours.Tables["Туры"].DefaultView;
}

Запустим приложение (рис. 107).

Рис. 107. Вывод содержимого таблицы «Туры»

Теперь добавим два объекта DataColumn, в которых будет вычисляться налог и скидка, после добавления объекта dcPrice:

DataColumn dcPrice = dtTours.Columns.Add("Цена", typeof(Decimal));
DataColumn dcPriceNDS = dtTours.Columns.Add("Цена c НДС", typeof(Decimal));
dcPriceNDS.Expression = "Цена*0.15+Цена";
DataColumn dcPricewithDiscount = dtTours.Columns.Add("Цена го скидкой", typеof(Decimal));
dcPricewithDiscount.Expression = "Цена-Цена*0.10";
...

Свойство Expression созданного объекта DataColumn задает выражения для всех значений заданного поля (рис. 108).

Рис. 108. Программное формирование полей, основанных на значении

Свойство Expression поддерживает также агрегатные функции, объединение строк, ссылки на родительские и дочерние таблицы.

 

Отслеживание изменений в базе данных

 

Объекты DataSet и DataTable предоставляют перегруженный метод GetChanges, конструктор которого имеет следующий вид (рис. 109).

Рис. 109. Метод GetChanges

Метод GetChanges возвращает новый объект DataSet или DataTable со структурой исходного объекта, но содержащий только измененные записи из исходного объекта. На рис. 109 исходными объектами являются myDataSet (A) и myDataTable (Б), а новыми, созданными методом GetChanges - newDataSet (А) и newDataTable (Б). Применяя в качестве параметра значение перечисления DataRowState, можно получить записи с конкретным состоянием, например, только удаленные (Deleted) или добавленные (Added).

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

Метод GetChanges также применяется в качестве своеобразного сигнального флага, сообщающего, были ли затронуты данные. Реализовать такую проверку можно, например, при помощи следующего кода:

private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
         DataSet ds = dataSet11.GetChanges();
         if(ds == null) return;
         if(MessageBox.Show("Вы хотите сохранить изменения в базе данных?",
                       "Завершение работы", MessageBoxButtons.YesNo,
                                   MessageBoxIcon.Question) == DialogResult.Yes)
            DataAdapter1.Update(ds);
}

 

 

Обработка исключений

 

В процессе передачи изменений в базу данных могут возникать многочисленные исключения. Объекты DataSet, DataTable и DataRow имеют свойство HasErrors, позволяющее обрабатывать некоторые из них.

Для обработки исключений в процессе обновления таблиц БД можно использовать следующий код:

private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
          try {
            sqlDataAdapter1 .Update(dataSet11);
          }
          catch(Exception ex) {
            if(dataSet11. HasErrors) {
                        foreach( DataTable myTable in dataSet11.Tables) {
                                   if(myTable.HasErrors)
                                    {
                                               foreach(DataRow myRow in myTable.Rows) {
                                                           if(myRow. HasErrors) {
                                                                       MessageBox.Show("Ошибка в записи #: " +
                                                                            myRow["Код туриста"], myRow.RowError);
                                                           foreach(DataColumn myColumn in myRow.GetColumnsInError()) {
                                                                       MessageBox.Show(myColumn.ColumnName, " - в этом столбце ошибка");
                                                           }
                                                           myRow.ClearErrors();
                                                           myRow.RejectChanges();
                                               }
                                   }
                        }
            }
          }
}
}

Здесь происходит проход по каждому объекту, входящему в набор DataTable, DataRow или DataColumn. Метод ClearErrors удаляет все ошибки из объекта myRow, а метод RejectChanges производит откат всех изменений.

 



<< ПредыдущаяОглавлениеСледующая >>