Автономные данные

177

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

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

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

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

Веб-приложения и DataSet

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

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

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

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

Итак, что же остается веб-приложениям ASP.NET? По сути, есть два выбора. Можно использовать объект DataSet либо применять прямые команды, исключающие необходимость в DataSet. Говоря в общем, когда необходимо добавлять, вставлять или обновлять записи, без DataSet можно обойтись. Однако полностью избежать применения DataSet не удастся.

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

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

Интеграция с XML

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

DataSet

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

На рисунке ниже показана базовая структура DataSet:

Базовая структура DataSet

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

Как видно на рисунке, каждая запись коллекции DataSet.Tables — это DataTable. Объект DataTable содержит собственные коллекции — коллекцию Columns объектов DataColumn (описывающие имя и тип данных каждого поля) и коллекцию Rows объектов DataRow (содержащих действительные данные каждой записи).

Каждая Запись в DataTable представлена объектом DataRow. Каждый объект DataRow представляет одиночную запись в таблице, которая была извлечена из источника данных. DataRow является контейнером для действительных значений полей. Получить доступ к ним можно по имени поля, как в случае myRow["FieldName"]. Всегда помните, что данные в источнике вообще никак не затрагиваются, когда вы работаете с объектами DataSet. Вместо этого все изменения проводятся локально в объекте DataSet, расположенном в памяти. DataSet никогда не полагается на какое-либо соединение с источником данных.

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

Некоторые методы класса DataSet
Метод Описание
GetXml() и GetXmlSchema()

Возвращают строку данных (в разметке XML) или информацию схемы для DataSet. Информация схемы — это структурированная информация вроде количества таблиц, их имен, столбцов, типов данных и установленных отношений

WriteXml() и WriteXmlSchema()

Сохраняют данные и схемы, представленные DataSet а файле или потоке формата XML

ReadXml() и ReadXmlSchema()

Создают таблицы в DataSet на основе существующего документа XML или документа схемы XML. Источником XML может быть файл или любой другой поток

Clear()

Очищает все данные таблиц. Однако этот метод оставляет нетронутой информацию о схеме и отношениях

Copy()

Возвращает точный дубликат DataSet с тем же набором таблиц, отношений и данных

Clone()

Возвращает DataSet с той же структурой (таблицами и отношениями), но без данных

Merge()

Принимает другой DataSet, DataTable или коллекцию объектов DataRow и объединяет с текущим объектом DataSet, добавляя новые таблицы и объединяя данные в существующих

Класс DataAdapter

Чтобы извлечь записи из базы и использовать их для наполнения таблицы в DataSet, нужно использовать другой объект ADO.NET — DataAdapter. Объект DataAdapter является специфичным для поставщика, поэтому для каждого поставщика предусмотрены разные классы DataAdapter (SqlDataAdapter, OracleDataAdapter и т.д.).

DataAdapter служит мостом между одним DataTable в DataSet и источником данных. Он включает все доступные команды для выполнения запросов и обновления источника данных. Чтобы позволить DataAdapter редактировать, удалять и добавлять строки, потребуется указать объекты Command для свойств UpdateCommand, DeleteCommand и InsertCommand объекта DataAdapter. Чтобы применить DataAdapter для наполнения DataSet, необходимо установить SelectCommand.

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

Методы DataAdapter
Метод Описание
Fill()

Добавляет DataTable к DataSet за счет выполнения запроса в SelectCommand. Если запрос возвращает множественные результирующие наборы, этот метод добавит множество объектов DataTable за раз. Этот метод можно также использовать для добавления данных к существующему объекту DataTable

FillSchema()

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

Update()

Проверяет все изменения отдельного объекта DataTable и применяет пакет этих изменений к источнику данных за счет выполнения соответствующих операций InsertComrnand, UpdateCommand и DeleteCommand

На рисунке ниже показано, как DataAdapter и его объекты Command работают вместе с источником данных и объектом DataSet:

Взаимодействие DataAdapter с источником данных

Наполнение объекта DataSet

В следующем примере будет показано, как извлекать данные из таблицы SQL Server и использовать их для наполнения объекта DataTable, принадлежащего DataSet. Вы также увидите, как отображать данные программно, организуя цикл по записям с отображением их одна за другой. Вся логика находится в обработчике события Page.Load:

protected void Page_Load(object sender, EventArgs e)
{
        // Извлечь строку подключения из файла web.config и создать соединение
        string connectionString = 
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        SqlConnection connect = new SqlConnection(connectionString);
        string command = "SELECT * FROM Employees";
        
        SqlDataAdapter adapter = new SqlDataAdapter(command, connect);

        // Заполнить DataSet
        DataSet dataset = new DataSet();
        adapter.Fill(dataset, "Employees");

        // Используя DataSet вывести все строки таблицы Employees в элемент Label1
        foreach (DataRow row in dataset.Tables["Employees"].Rows)
        {
            Label1.Text += String.Format("<li>{0} {1} {2} (<em>{3:d}</em>)<br>",
                    row["EmployeeID"].ToString(), row["LastName"],
                    row["FirstName"], row["BirthDate"]);
        }
}

Давайте разберем этот пример. Сначала код создает соединение и определяет текст запроса SQL. На следующем шаге создается новый экземпляр класса SqlDataAdapter, который извлечет список сотрудников. Хотя каждый объект DataAdapter поддерживает объекты Command, только один из них (а именно — SelectCommand) необходим для наполнения DataSet. Чтобы еще более облегчить жизнь, можете создать необходимый объект Command и присвоить его свойству DataAdapter.SelectCommand за один прием. Для этого потребуется только применить объект Connection и строку запроса в конструкторе DataAdapter, что и сделано в примере.

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

Обратите внимание, что в коде не открывается соединение за счет вызова Connection.Open(). Вместо этого DataAdapter открывает и закрывает связанное с ним соединение автоматически, когда вызывается метод Fill(). В результате единственная строка кода, которую нужно рассмотреть для помещения в блок с обработкой исключений — вызов DataAdapter.Fill(). В качестве альтернативы соединение можно также открывать и закрывать «вручную». Если соединение открыто на момент вызова Fill(), то DataAdapter использует это соединение и не станет закрывать его автоматически. Такой подход удобен, если нужно быстро выполнить множество последовательных операций с источником данных и избежать дополнительных накладных расходов, связанных с повторным открытием и закрытием соединения.

Последний шаг — отображение содержимого DataSet. Быстрее всего это можно сделать с применением того же приема, что был продемонстрирован в предыдущей статье, и построить строку HTML, обращаясь к каждой записи. Естественно, модель ASP.NET спроектирована так, чтобы избавить от написания низкоуровневой HTML-разметки. Поэтому гораздо лучше привязать данные DataSet к элементу управления, связанному с данными, который автоматически сгенерирует необходимую HTML-разметку на основе шаблона. Такие элементы управления мы рассмотрим позже.

Работа с множественными таблицами и отношениями

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

protected void Page_Load(object sender, EventArgs e)
{
        string connectionString = 
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        SqlConnection connect = new SqlConnection(connectionString);

        string sqlCat = "SELECT CategoryID, CategoryName FROM Categories";
        string sqlProd = "SELECT ProductName, CategoryID FROM Products";

        SqlDataAdapter adapter = new SqlDataAdapter(sqlCat, connect);
        DataSet dataset = new DataSet();

        try
        {
            connect.Open();

            // Наполнить DataSet данными из таблицы Categories
            adapter.Fill(dataset, "Categories");

            // Изменить текст команды и извлечь данные таблицы Products. 
            // Для решения этой задачи можно было бы также использовать 
            // другой объект
            adapter.SelectCommand.CommandText = sqlProd;
            adapter.Fill(dataset, "Products");
        }
        finally
        {
            connect.Close();
        }
}

На первом шаге осуществляется инициализация объектов ADO.NET и объявление двух SQL-запросов (для извлечения категорий и товаров). Далее код выполняет оба запроса, добавляя две таблицы к DataSet. Обратите внимание, что соединение открывается явно вначале и закрывается после двух операций, обеспечивая тем самым наилучшую производительность.

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

В коде мы имеем DataSet с двумя таблицами. В базе Northwind эти две таблицы связаны отношением по полю CategoryID. Оно является первичным ключом таблицы Categories и внешним ключом таблицы Products. К сожалению, в ADO.NET не предусмотрено никакого способа прочитать информацию об отношениях между таблицами в источнике данных, чтобы применить ее к DataSet автоматически. Поэтому для представления этого отношения приходится вручную создавать DataRelation.

Отношение создается путем определения объекта DataReiation и добавления его к коллекции DataSet.Relations. При создании DataRelation конструктору передаются три аргумента: имя отношения, DataColumn первичного ключа родительской таблицы и DataColumn внешнего ключа дочерней таблицы. Для этого понадобится следующий код:

protected void Page_Load(object sender, EventArgs e)
{
        // ...

        // Определить отношение между таблицами Categories и Products
        DataRelation relat = new DataRelation("CatProds",
            dataset.Tables["Categories"].Columns["CategoryID"],
            dataset.Tables["Products"].Columns["CategoryID"]);

        // Добавить отношение в DataSet
        dataset.Relations.Add(relat);

        // Пройти в цикле по всем записям о категориях
        foreach (DataRow row in dataset.Tables["Categories"].Rows)
        {
            Label1.Text += String.Format("<b>{0}</b><br><ul>", row["CategoryName"]);

            // Получить связанные товары
            DataRow[] childRows = row.GetChildRows(relat);

            foreach (DataRow childRow in childRows)
            {
                Label1.Text += String.Format("<li>{0}</li>", childRow["ProductName"]);
            }
            Label1.Text += "</ul>";
        }
}

Здесь присутствует интересная часть. Внутри блока можно обратиться к связанным записям о товарах для текущей категории, вызвав метод DataRow.GetChildRows(). Этот метод ищет данные — соответствующие строки в памяти, в связанном объекте DataTable. Получив массив записей о товарах, можно пройтись по ним с помощью вложенного цикла foreach. Это намного проще, чем код, который понадобился бы для поиска этой информации в отдельном объекте или для выполнения множества запросов с традиционным доступом через соединение.

Запустив эту страницу, вы увидите вывод, показанный на рисунке:

 Список товаров в каждой категории

Вопрос, который часто задают программисты-новички в ADO.NET звучит так: когда нужно использовать запросы JOIN, а когда — объекты Data Relation? Наиболее важное условие для правильного ответа — собираетесь ли вы обновлять извлеченные данные? Если да, то применение отдельных таблиц и объекта DataRelation всегда обеспечивает большую гибкость. Если нет, то можно применять любой подход, хотя запрос JOIN и может оказаться более эффективным, потому что включает лишь одно обращение к базе по сети, в то время как вариант с DataRelation требует два, чтобы наполнить отдельные таблицы.

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

Например, если имеется полный список клиентских заказов, но только часть списка клиентов, может случиться так, что заказ будет ссылаться на заказчика, которого не существует, потому что запись о нем в DataSet отсутствует. Один из способов обойти эту проблему — создать DataRelation без соответствующих ограничений. Чтобы сделать это, используйте конструктор DataRelation, который принимает булевский параметр createConstraints, и установите его значение в false, как показано ниже:

DataRelation relat = new DataRelation("CatProds",
	dataset.Tables["Categories"].Columns["CategoryID"],
	dataset.Tables["Products"].Columns["CategoryID"], false);

Другой подход предусматривает отключение всякого рода проверок ограничений (в том числе проверки уникальных значений) за счет установки свойства DataSet.EnableConstraints в false перед добавлением отношения.

Поиск определенных строк

Класс DataTable предлагает удобный метод Select(), позволяющий искать строки на основе SQL-выражения. Выражение, которое используется с методом Select(), играет ту же роль, что и конструкция WHERE в операторе SELECT, но имеет дело с данными в памяти, находящимися в объекте DataTable (так что никаких операций с базой данных не производится).

Например, следующий код извлекает все товары, которые помечены как продающиеся со скидкой:

protected void Page_Load(object sender, EventArgs e)
{
        string connectionString = 
            WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        SqlConnection connect = new SqlConnection(connectionString);

        string sqlProd = "SELECT ProductName, CategoryID, Discontinued FROM Products";

        SqlDataAdapter adapter = new SqlDataAdapter(sqlProd, connect);
        DataSet dataset = new DataSet();
        adapter.Fill(dataset, "Products");

        // Получить продукты со скидкой
        DataRow[] rows = dataset.Tables["Products"].Select("Discontinued > 0");

        // Отобразить
        foreach (DataRow row in rows)
        {
            Label1.Text += String.Format("<li>{0}</li>", row["ProductName"]);
        }
}

В этом примере в методе Select() используется достаточно простая строка фильтра. Однако вы вольны применять более сложные операции и комбинации различных критериев. Более подробную информацию можно найти в руководстве MSDN по библиотеке классов, в описании свойства DataColumn.Expression.

Метод Select() имеет одно потенциальное ограничение — он не принимает параметризованных условий. В результате он открыт для атак внедрением SQL. Ясно, что такие атаки, которые может предпринять злоумышленник в этой ситуации, достаточно ограничены, потому что нет способа получить доступ к реальному источнику данных или выполнить дополнительные команды. Однако тщательно написанные значения могут заставить ваше приложение вернуть дополнительную информацию из таблицы. Если фильтрующее выражение содержит значение, передаваемое пользователем, итерацию по DataTable для нахождения требуемых строк нужно выполнять вручную, не пользуясь методом Select().

Пройди тесты
Лучший чат для C# программистов