Обработка баз данных на Visual Basic®.NET

Мак-Манус Джеффри П.

Голдштейн Джеки

Прайс Кевин Т.

ГЛАВА 7

ADO.NET: дополнительные компоненты

 

 

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

 

Обнаружение конфликтов при параллельном доступе к данным

Разработчики, имеющие опыт создания многопользовательских баз данных, знают о потенциальных проблемах при параллельном доступе к данным, возникающих при попытке одновременного обновления одних и тех же данных со стороны нескольких пользователей. Допустим, что пользователь А считывает какие-то данные и пользователь Б считывает те же данные, затем пользователь А изменяет эти данные, а пользователь Б также пытается их изменить. Как можно решить эту проблему? Существует два основных способа решения конфликтных ситуаций при параллельном доступе к данным.

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

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

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

Второй способ — оптимистическая блокировка (или оптимистическое управление параллельностью) — основан на очень кратковременной блокировке записей во время их фактического обновления. Этот способ решает проблему управления блокировками и масштабируемости, а также прекрасно подходит для редактирования наборов данных, отключенных от базы данных. Но что произойдет, если пользователь Б захочет обновить данные, уже обновленные пользователем А? Один из вариантов решения этой проблемы основан на признании только последнего обновления. Однако этот способ годится только для ограниченного круга приложений.

Для использования оптимистической блокировки нужно определить, изменялись ли данные после их последнего извлечения, что называется нарушением параллельного доступа (concurrency violation). Для этого применяются две основные стратегии.

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

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

В обоих способах, если исходная запись изменена, условие предложения WHERE не удовлетворяется, запись не будет найдена и не будет обновлена.

НА ЗАМЕТКУ

При использовании оптимистической блокировки в модели ADO 2.X в случае нарушения параллельного доступа появляется сообщение о невозможности найти запись, а не о нарушении параллельного доступа.

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

Команды SQL UPDATE и DELETE, сгенерированные объектом CommandBuilder и программой-мастером Data Adapter Configuration Wizard, содержат предложение WHERE для определения конфликтов параллельного доступа. Рассмотрим более внимательно код из главы 6, "ADO.NET: объект DataAdapter", созданный с помощью программы-мастера Data Adapter Configuration Wizard. Для этого нужно открыть раздел кода с заголовком Windows Form Designer generated code (Код, сгенерированный конструктором Windows Form) в коде формы frmUpdates. Но сначала рассмотрим команду SQL UPDATE, которая приводится в листинге 7.1.

Листинг 7.1. Команда SQL UPDATE, созданная программой-мастером Data Adapter Configuration Wizard

UPDATE tblEmployee

SET FirstName = @FirstName,

 LastName = @LastName,

 DepartmentID = @DepartmentID,

 Salary = @Salary

WHERE

 (ID = @Original_ID) AND

 (DepartmentID = @Original_DepartmentID OR

  @Original_DepartmentID IS NULL AND DepartmentID IS NULL)

 AND (FirstName = @Original_FirstName)

 AND (LastName = @Original_LastName)

 AND (Salary = @Original_Salary OR

  @Original_Salary IS NULL AND Salary IS NULL)

;

SELECT FirstName, LastName, DepartmentID, Salary, ID

FROM tblEmployee WHERE (ID = @ID)

Эта команда SQL выглядит как обычная команда UPDATE, которая задает новые значения для четырех обновляемых полей в качестве параметров объекта UpdateCommand. Предложение WHERE содержит первичный ключ (ID), а также исходные значения для каждого поля и проверяет их соответствие текущим значениям записи в базе данных. Более того, эта команда SQL проверяет наличие неопределенных значений NULL в базе данных и полях таблицы tblEmployee.

Команда SELECT (заданная при конфигурировании объекта DataAdapter) располагается вслед за командой UPDATE после точки с запятой. Точка с запятой всегда используется для разделения команд в пакете команд SQL, а команда SELECT добавляется по умолчанию для возвращения обновленной записи в приложение.

Рассмотрим код указания параметров для объекта UpdateCommand, как показано в листинге 7.2.

Листинг 7.2. Код установки параметров команды, сгенерированной с помощью программы-мастера Data Adapter Configuration Wizard

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@FirstName", System.Data.SqlDbType.VarChar, 50, "FirstName"))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@LastName", System.Data.SqlDbType.VarChar, 70, "LastName"))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@DepartmentID", System.Data.SqlDbType.Int, 4, "DepartmentID"))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@Salary", System.Data.SqlDbType.Money, 8, "Salary"))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@Original_ID", System.Data.SqlDbType.Int, 4, System.Data.ParameterDirection.Input, False, CType(0, Byte), CType(0, Byte), "ID", System.Data.DataRowVersion.Original, Nothing))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@Original_DepartmentID", System.Data.SqlDbType.Int, 4, System.Data.ParameterDirection.Input, False, CType(0, Byte), CType(0, Byte), "DepartmentID", System.Data.DataRowVersion.Original, Nothing))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@Original_FirstName", System.Data.SqlDbType.VarChar, 50, System.Data.ParameterDirection.Input, False, Byte), CType(0, Byte), "FirstName", System.Data.DataRowVersion.Original, Nothing))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@Original_LastName", System.Data.SqlDbType.VarChar, 70, System.Data.ParameterDirection.Input, False, CType(0, Byte), CType(0, Byte), "LastName", System.Data.DataRowVersion.Original, Nothing))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter(System.Data.SqlDbType.Money, 8, System.Data.ParameterDirection.Input, False, CType(0, Byte), CType(0, Byte), "Salary", System.Data.DataRowVersion.Original, Nothing))

Me.SqlUpdateCommand1.Parameters.Add(New System.Data.SqlClient.SqlParameter("@ID", System.Data.SqlDbType.Int, 4, "ID"))

Для данного объекта-команды заданы десять параметров. Первые четыре параметра являются текущими (возможно, измененными) значениями полей, которые следует обновить в базе данных. Как сообщается в главе 5, "ADO.NET: объект DataSet", каждая запись может иметь одну из четырех версий значения в каждой записи. По умолчанию используется текущее значение из считываемого поля.

Следующие пять параметров являются исходными значениями полей, которые упоминаются в предложении WHERE. Учтите, что для извлечения исходной версии значения поля (а не предлагаемой по умолчанию текущей версии) нужно явно указать исходную версию, как показано ниже.

System.Data.DataRowVersion.Original

НА ЗАМЕТКУ

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

Последний параметр — текущее значение поля ID — применяется в качестве параметра команды SELECT, используемой для возвращения обновленных значений записи.

После каждой операции вставки, обновления или удаления объект DataAdapter проверяет количество строк, охваченных операцией. Если количество строк равно нулю, то генерируется исключительная ситуация DBConcurrencyException, поскольку обычно предполагается, что это результат конфликта при параллельном доступе к данным. Для ее перехвата и обработки следует включить блок Try-Catch в подпрограмму btnUpdate_Click, которая показана в листинге 7.3.

Листинг 7.3. Блок Try-Catch для обработки исключительной ситуации DBConcurrencyException

Private Sub btnUpdate_Click(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles btnUpdate.Click

 Try

  daEmployees.Update(dsEmployeeInfo, "Employees")

 Catch ec As DBConcurrencyException

  ' Выполнить какие-то действия.

 Catch es As DBConcurrencyException

  MessageBox.Show(es.Message)

 End Try

End Sub

 

Отображения таблиц и полей

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

В данном примере в методе Fill объекта DataAdapter указывается объект DataSet и имя таблицы в этом наборе данных.

daEmployees.Fill(dsEmployeelnfo, "Employees")

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

Это значит, что можно, например, добавить отображение MappingName и отобразить его на таблицу empDataSetTable с помощью кода

daEmployees.TableMappings.Add("MappingName", "empDataSetTable")

А затем можно вызвать метод Fill следующим образом:

daEmployees.Fill(dsEmployeeInfo, "MappingName")

При этом считанные данные вставляются в таблицу empDataSetTable объекта dsEmployeeInfo.

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

Например, тестовая баз данных pubs, которая входит в состав SQL Server, была создана много лет назад, когда существовали очень строгие ограничения для имен полей, поэтому сейчас они имеют вид малопонятных аббревиатур. Отображения полей позволяют загружать данные из базы данных pubs в объект – набор данных с более понятными именами полей. Благодаря отображению удается удовлетворить все самые строгие требования и соглашения об именах, которые могут предъявляться со стороны администратора базы данных и архитектора приложения.

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

Листинг 7.4. Отображение таблицы и полей

daEmployees.TableMappings.Add("MappingName", "empDataSetTable")

With daEmployees.TableMappings("MappingName").ColumnMappings

 .Add("ID", "empEmployeeID")

 .Add("FirstName", "empFirstName")

 .Add("LastName", "empLastName")

 .Add("DepartmentID", "empDepartmentID")

 .Add("Salary", "empSalary")

End With

daEmployees.Update(dsEmployeeInfo, "MappingName")

В главе 6, "ADO.NET: объект DataAdapter" (см. листинги 6.1 и 6.2) представлена функция Read Data, которая вставляет в объект DataSet данные из источника данных и отображает содержимое объекта DataSet в списке формы frmDataSets (см. проект DataSetCode из главы 5, "ADO.NET: объект DataSet"). Если в подпрограмме обработки щелчков на кнопке DataAdapter Fill вместо функции ReadData вызвать ее измененную версию ReadDataMapped, которая содержит код, показанный в листинге 7.5, то после запуска приложения проекта DataSetCode будет получен результат, показанный на рис. 7.1.

РИС. 7.1. Содержимое объекта DataSet с отображениями таблицы и полей

Листинг 7.5. Измененная версия функции ReadDataMapped для отображения таблицы и полей

Private Sub ReadDataMapped()

 Dim daEmployees As SqlDataAdapter = New _

  SqlDataAdapter("select * from tblEmployee", _

  "server=localhost;uid=sa;database=novelty")

 dsEmployeeInfo = New DataSet()

 ' Конфигурация отображений таблицы и полей.

 daEmployees.TableMappings.Add("MappingName", "empDataSetTable")

 With daEmployees.TableMappings("MappingName").ColumnMappings

  .Add("ID", "empEmployeeID")

  .Add("FirstName", "empFirstName")

  .Add("LastName", "empLastName")

  .Add("DepartmentID", "empDepartmentID")

  .Add("Salary", "empSalary")

 End With

 daEmployees.Fill(dsEmployeeInfo, "MappingName")

 DisplayDataSet(dsEmployeeInfo)

End Sub

НА ЗАМЕТКУ

По умолчанию отображение таблицы называется Table. Оно применяется в том случае, если в методе Fill (или методе Update) используется только имя объекта DataSet. Однако его можно указать явно вместе с нужным именем таблицы с помощью приведенного ниже кода.

daEmployees.TableMappings.Add("Table", "MyTableName")

daEmployees.Fill(dsEmployeeInfo)

Это приведет к созданию таблицы MyTableName и вставке данных в нее.

 

Объект DataView

 

Объект DataView позволяет одновременно создавать разные представления данных из объекта DataTable и обладает перечисленными ниже свойствами, которые позволяют настраивать способ отображения данных.

• Порядок сортировки (нисходящий или восходящий) по одному или нескольким полям.

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

• Фильтр состояния записи, который указывает критерии отображения записей на основе состояния записи (см. перечисление DataViewRowState, показанное в табл. 5.3).

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

НА ЗАМЕТКУ

Хотя объект DataView аналогичен классическому представлению базы данных, он все же отличается от него следующим:

• не может использоваться как таблица;

• не может быть объединением нескольких таблиц;

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

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

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

dsCustomers.Tables("Customers").DefaultView.RowFilter = _

 "LastName = 'Like C* ' "

dsCustomers.Tables("Customers").DefaultView.Sort = "Zip"

Если необходимо отобразить в представлении текущие значения только из тех записей, которые были изменены (но еще не сохранены), то нужно указать новое значение для свойства RowFilter и значение свойства RowState.

dsCustomers.Tables("Customers").DefaultView.RowFilter = " "

dsCustomers.Tables("Customers").DefaultView.RowStateFilter = _

 DefaultView.RowState.ModifiedCurrent

НА ЗАМЕТКУ

Объект DataView также имеет метод Find для поиска одной записи и метод FindRows для поиска и возвращения нескольких записей. Если нужно извлечь или набор записей, которые соответствуют заданному критерию, вместо динамического представления данных, то с помощью методов Find и FindRows (вместо указания свойства RowFilter) будут возвращены только интересующие нас записи. Этот метод обладает более высокой производительностью, чем метод на основе установки значения свойства RowFilter. Дело в том, что указание значения свойства RowFilter вызывает перестройку индекса представления, а методы Find и FindRows используют уже существующие индексы.

Для таблицы могут быть созданы также дополнительные объекты – представления данных. Для определения еще одного представления таблицы Customers нужно создать еще один объект dvView2 и указать его свойства так, как показано ниже.

dvView2 = New DataView(dsCustomers.Tables("Customers"), _

 "", "LastName", DataViewRowState.CurrentRows)

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

dvView2.RowFilter = "LastName > 'F'"

dvView2.Sort = "LastName DESC"

dvView2.RowStateFilter = DataViewRowState.Current 

НА ЗАМЕТКУ

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

В большинстве других случаев объект DataView аналогичен объекту DataTable. Доступ к отдельным записям и полям представления осуществляется с помощью объекта DataRowView. Этот объект также поддерживает отношения между таблицами объекта DataSet.

Объект DataView имеет модель редактирования, аналогичную модели редактирования объекта DataTable. Указание значения True для свойств AllowNew, AllowEdit и  AllowDelete означает разрешение на выполнение соответствующих операций редактирования (вставки, изменения и удаления). Методы BeginEdit и EndEdit и CancelEdit объекта DataRowView управляют внесением изменений в объект DataTable. Метод EndEdit вносит изменения в объект DataRow с версией Current. Эти изменения затем принимаются (или отвергаются) базовым объектом DataTable с помощью метода AcceptChanges (или RejectChanges).

Рассмотрим теперь, как описанные выше идеи применяются на практике. Создайте новую форму frmDataViews в проекте DataSetCode (готовая версия этой формы находится в каталоге с примерами главы 5, "ADO.NET: объект DataSet") для создания двух разных представлений одной таблицы Customers. Каждый объект DataGrid отображает данные представления и содержит кнопки управления сортировкой и фильтрацией. Для этого выполните перечисленные ниже действия.

1. В форме frmDataSets проекта DataSetCode создайте новую кнопку под кнопкой Data Adapter Updates.

2. В окне Properties укажите значение btnDataViews для свойства (Name) и значение Data Views для свойства Text этой кнопки.

3. Создайте новую форму.

4. В окне Properties укажите значение frmDataViews для свойства (Name) и значение Dueling Data Views для свойства Text новой формы Form1.

5. Увеличьте размер формы frmDataViews.

6. Создайте в правой части формы сетку данных DataGrid1, текстовое поле txtFilter1 с надписью, поле со списком cboSort1 и надписью, флажок chkDesc1 с надписью, поле со списком cboRowState1 и надписью, кнопку btnApply1, перетаскивая их из панели элементов управления.

7. В окне Properties укажите значение Descending для свойства Text флажка chkDesc1 и значение Apply для свойства Text кнопки btnApply1. Укажите значение DropDownList для свойства DropDownStyle полей со списком cboSort1 и cboRowState1. Укажите для свойства Text надписей значения by Column: и Row State.

8. Укажите значение Default Data View для свойства CaptionText сетки DataGrid1.

9. Расположите элементы управления так, как показано на рис. 7.2. 

РИС. 7.2. Расположение элементов управления в верхней части формы frmDataViews

РИС. 7.З. Окончательный вид формы frmDataViews

10. Выберите все элементы управления и скопируйте их в нижнюю часть формы frmDataViews. Переименуйте все элементы управления (за исключением надписей) так, чтобы их имена заканчивались цифрой 2, а не 1 (например, btnApply2).

11. Укажите значение DataView2 для свойства CaptionText сетки DataGrid2.

Окончательный вид формы frmDataViews показан на рис. 7.3, а ее код представлен в листинге 7.6.

Листинг 7.6. Код создания двух объектов-сеток для отображения разных представлений одной таблицы данных

Imports System

Imports System.Data

Imports System.Data.SqlClient

Public Class frmDataViews

 Inherits System.Windows.Forms.Form

"Windows Form Designer generated code"

 Private dsCustomers As New DataSet()

 Private dvView2 As DataView

 Private Sub frmDataViewS_Load(ByVal sender As System.Object, _

  ByVal e As System.EventArgs) Handles MyBase.Load

  Dim i As Integer

  Dim col As DataColumn

  ' Инициализация объекта DataAdapter.

  Dim daCustomers As SqlDataAdapter = New _

   SqlDataAdapter("select * from tblCustomer", _

   "server=localhost;uid=sa;database=novelty")

   ' Вставка данных только в ОДНУ таблицу.

  daCustomers.Fill(dsCustomers, "Customers")

  ' Создание второго объекта DataView.

  dvView2 = New DataView(dsCustomers.Tables("Customer"), _

   "", "LastName", DataViewRowState.CurrentRows)

  ' Вставка списка из имен полей.

  For Each col In dsCustomers.Tables("Customers.Columns")

   cboSort1.Items.Add(col.ColumnName)

   cboSort2.Items.Add(col.ColumnName)

  Next

  ' Вставка объекта DataViewRowState.

  Dim names As String()

  names = DataViewRowState.None.GetNames(DataViewRowState.None.GetType)

  For i = 0 To names.GetUpperBound(0)

   cboRowState1.Items.Add(names(i))

   cboRowState2.Items.Add(names(i))

  Next

  ' Указание значений по умолчанию.

  txtFilter1.Text = ""

  txtFilter2.Text = ""

  cboSort1.SelectedItem = "ID"

  cboSort2.SelectedItem = "ID"

  chkDesc1.Checked = False

  chkDesc2.Checked = False

  cboRowState1.SelectedItem = "CurrentRows"

  cboRowState2.SelectedItem = "CurrentRows"

  dsCustomers.Tables("Customers").DefaultView.Sort = "ID"

  dvView2.Sort = "ID"

  ' Связывание сеток данных с таблицей.

  DataGrid1.DataSource = dsCustomers.Tables("Customers").DefaultView

  DataGrid2.DataSource = dvView2

 End Sub

 Private Sub btnApply1_Click(ByVal sender As System.Object, _

  ByVal e As System.EventArgs) Handles btnApply1.Click

  Dim sort As String

  Dim rowState As DataViewRowState

  ' Указание фильтра.

  dsCustomers.Tables("Customers").DefaultView.RowFilter = _

   txtFilter1.Text

  ' Указание сортировки.

  sort = cboSort1.SelectedItem

  If chkDesc1.Checked Then

   sort = sort & " DESC"

  End If

  dsCustomers.Tables("Customers").DefaultView.Sort = sort

  ' Указание состояния записи.

  dsCustomers.Tables("Customers").DefaultView.RowStateFilter = _

   rowState.Parse(rowState.GetType, cboRowState1.SelectedItem)

 End Sub

 Private Sub btnApply2_Click(ByVal sender As System.Object, _

  ByVal e As System.EventArgs) Handles btnApply2.Click

  Dim sort As String

  Dim rowState As DataViewRowState

  ' Указание фильтра.

  dvView2.RowFilter = txtFilter2.Text

  ' Указание сортировки.

  sort = cboSort2.SelectedItem

  If chkDesc2.Checked Then

   sort = sort & " DESC"

  End If

  dvView2.Sort = sort

  ' Указание состояние записи.

  dvView2.RowStateFilter = rowState.Parse(rowState.GetType, _

   cboRowState2.SelectedItem)

 End Sub

End Class

Подпрограмма frmDataViews_Load инициализирует разные объекты формы. Сначала создается объект DataAdapter, который затем используется для загрузки данных из таблицы базы данных в таблицу Customers набора данных DataSet. Далее создаются два представления данных: одно будет содержать создаваемое по умолчанию представление, а другое — новое представление dvView2. Далее объект-представление dvView2 инициализируется для отображения всех текущих записей с сортировкой по фамилии, т.е. по полю LastName.

После этого инициализируются два набора полей со списками. В поле со списком cboSort загружается список имен полей таблицы Customers, а в поле со списком cboRowState — список значений перечисления DataViewRowState.

НА ЗАМЕТКУ

В Visual Basic больше не поддерживается свойство ItemData. Дело в том, что вместо него для преобразования значений перечисления в строки для загрузки их в поле со списком используется метод GetNames перечисления. Аналогично, метод Parse перечисления используется для преобразования строк в значения перечисления при присвоении избранных значений свойству RowStateFilter.

Затем для элементов управления с критериями отбора записей задаются значения по умолчанию, т.е. исходный порядок сортировки для обоих представлений задается по полю ID. Наконец, каждое представление связывается с одной из сеток данных, что приводит к отображению в них всех текущих данных.

Избранные значения критериев применяются для соответствующего представления после щелчка на кнопке Apply. Две подпрограммы btnApplyl_Click и btnApply2_Click идентичны, за исключением того, что они относятся к разным наборам элементов управления. Фильтр записей (значение свойства RowFilter) задается на основании указанного текста в текстовом поле txtFilter, порядок сортировки (значение свойства Sort) — на основании выбранного поля в поле со списком cboSort (с дополнительным модификатором DESC нисходящего порядка), а фильтр состояния записи (значение свойства RowStateFilter) — на основании значения в поле со списком cboRowState2. Изменение свойств представлений, связанных с сетками данных, приводит к автоматическому отображению данных с новыми параметрами представления.

Скомпонуем проект DataSetCode и запустим полученное приложение. Щелкните на кнопке Data Views для отображения новой формы frmDataViews. Внесите необходимые изменения в критерии сетки и щелкните на кнопке Apply для внесения этих изменений в соответствующую сетку. На рис. 7.4 показан пример отображения данных из одной таблицы в двух разных представлениях.

Попробуйте выбрать разные поля сортировки и состояния записей для организации разных представлений. Попробуйте использовать разные фильтры, например сложные выражения ID > 10 AND ID < 18 или LastName Like с. Более подробные сведения о правилах создания фильтров приводятся в разделе с описанием свойства DataColumn.Expression справки Visual Studio .NET.

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

РИС. 7.4. Пример отображения данных из одной таблицы в двух разных представлениях формы frmDataViews

НА ЗАМЕТКУ

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

 

Бизнес-ситуация 7.1: просмотр данных из разных источников

Когда компания Jones Novelties, Inc. приняла решение создать новую систему обработки данных, она уже имела прежнюю систему на основе базы данных Access, в которой директор компании Брэд Джонс хранил данные о заказах. Для директора очевидны преимущества перехода к новой системе на основе СУБД SQL Server, но прежде всего он хотел бы гарантировать сохранность унаследованных данных. Поэтому было решено вносить все изменения постепенно, особенно для сохранения данных компании запрошлые годы.

Итак, Джонс хотел бы создать новую систему на основе SQL Server, но иногда использовать старые данные из базы данных Access до тех пор, пока не будет закончена работа над новой системой. Поэтому часть данных должна храниться в одной базе данных, а часть — в другой. Две базы данных должны совместно использовать и даже объединять свои данные, хотя они относятся к разным типам баз данных.

К счастью, это требование легко удовлетворяется в модели ADO.NET. Как уже описывалось выше в главе, для объекта DataSet совершенно не имеет значения, откуда, т.е. из каких источников, берутся данные. Поэтому разработчик базы данных может создать приложение, загружая данные о заказах из таблицы tblOrder базы данных Access, а данные о клиентах — из таблицы 1 базы данных SQL Server. В дальнейшем, после создания в базе данных SQL Server таблицы tblOrder, достаточно будет только изменить строку подключения в объекте DataAdapter, и приложение будет функционировать, как и прежде.

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

1. Запустите интегрированную среду разработки Visual Studio .NET.

2. Создайте новый проект Visual Basic Windows Application. Для этого в диалоговом окне New Project (Новый проект) выберите тип проекта Visual Basic Project в области Project Types (Типы проектов), а затем шаблон Windows Application (Приложение Windows) в области Templates (Шаблоны).

3. Назовите проект BusinessCase71.

4. Укажите путь к файлам проекта.

5. Увеличьте размер формы Form1.

6. В окне Properties укажите значение frmShowOrders для свойства (Name) и значение Show Orders для свойства Text формы Form1.

7. В верхнем левом углу формы создайте текстовое поле и укажите значение txtCustomerID для его свойства (Name), создайте кнопку и укажите значение btnFinds для ее свойства (Name) и значение Find для ее свойства Text, создайте поле со списком и укажите значение lstCustomer с для его свойства (Name). В правой части формы создайте сетку данных и укажите значение grdOrders для ее свойства (Name).

8. Расположите все эти элементы управления так, как показано на рис. 7.5.

9. Перетащите в форму из панели элементов управления компонент DataSet и укажите значение dsCustOrders для его свойства (Name).

10. Создайте новый объект SqlDataAdapter для таблицы tblCustomers базы данных Novelty СУБД SQL Server, перетаскивая его из панели элементов управления. После запуска программы-мастера DataAdapter Configuration Wizard выберите подключение к базе данных Novelty. Затем в диалоговом окне Choose a Query Туре выберите переключатель Use SQL statements.

11. В диалоговом окне Generate the SQL statements укажите команду SELECT * FROM tblCustomer для загрузки данных в объект — набор данных.

12. По окончании работы с программой-мастером DataAdapter Configuration Wizard укажите значение daCustomers для свойства (Name) для созданного объекта — адаптера данных. 

РИС. 7.5. Расположение элементов управления в форме frmShowOrders

13. Для таблицы tblOrder, которая находится в базе данных Novelty.MDB Access создайте объект OleDbDataAdapter, перетаскивая его из панели элементов управления. После запуска программы-мастера Data Adapter Configuration Wizard создайте новое подключение, щелкнув на кнопке New Connection. Затем в диалоговом окне Data Link Properties выберите вкладку Provider и провайдер Microsoft Jet 4.0 OLE DB Provider.

14. Затем выберите вкладку Connection и укажите путь к файлу базы данных Novelty.MDB.

15. Затем в диалоговом окне Choose a Query Type выберите переключатель Use SQL statements.

16. В диалоговом окне Generate the SQL statements укажите команду SELECT * FROM tblOrder для загрузки данных в объект — набор данных.

17. По окончании работы с программой-мастером Data Adapter Configuration Wizard укажите значение daOrders для свойства (Name) для созданного объекта — адаптера данных.

В верхней части файла введите следующий код:

Imports System

Imports System.Data

Imports System.Data.SqlClient

Imports System.Data.OleDb

Затем в определении класса формы frmOrders введите код из листинга 7.7.

Листинг 7.7. Код объединения данных из источников данных разного типа

Private dvOrders As New DataView()

Private Sub frmShowOrders_Load(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles MyBase.Load

 Dim rel As DataRelation

 ' Вставка таблиц в набор данных

 dsCustOrders. daCustomers.Fill(dsCustOrders, "Customers")

 daOrders.Fill(dsCustOrders, "Orders")

 ' Создание отношения между таблицами.

 rel = dsCustOrders.Relations.Add("relCustOrders", _

  dsCustOrders.Tables("Customers").Columns("ID"), _

  dsCustOrders.Tables("Orders").Columns("CustomerID"))

 ' Указание первичного ключа для таблицы Customers.

 Dim pk(0) As DataColumn

 pk(0) = dsCustOrders.Tables("Customers").Columns("ID")

 dsCustOrders.Tables("Customers").PrimaryKey = pk

 ' Указание сортировки по умолчанию для метода Find.

 dsCustOrders.Tables("Customers").DefaultView.Sort = "ID"

End Sub

Private Sub btnFind_Click(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles btnFind.Click

 Dim RowNum As Integer

 Dim dvRow As DataRowView

 Dim i As Integer

 If IsNumeric(txtCistID.Text) Then

  RowNum = dsCustOrders.Tables("Customers"). _

  DefaultView.Find(txtCustID.Text)

  If RowNum <> -1 Then

   dvRow = dsCustOrders.Tables("Customers").DefaultView(RowNum)

   ' Вставка в поле со списком имен полей из таблицы Customer.

   lstCustomer.Items.Clear()

   For i = 0 To dsCustOrders.Tables("Customers").Columns.Count – 1

    lstCustomer.Items.Add(dvRow.Item(i))

   Next

   grdOrders.CaptionText = _

    "Orders for customer #" & txtCustID.Text

   ' Извлечение связанных дочерних записей для

   ' указанного клиента таблицы Customer.

   dvOrders = dvRow.CreateChild("relCustOrders")

   grdOrders.DataSource = dvOrders

  Else

   MessageBox.Show( _

    "CustomerID not found – Please try again.")

   ' "Клиент CustomerID не найден – Попробуйте еще раз."

   txtCustID.Clear()

  End If

 Else

  Beep()

 End If

End Sub

Все параметры источников данных задаются в подпрограмме frmShowOrders_Load. Затем создаются две таблицы для набора данных DataSet и отношение DataRelation между ними. Наконец, задаются значения свойств РrimaryKey и Sort для таблицы Customers и его представления по умолчанию DefaultView, чтобы для поиска записей можно было использовать метод Find.

Щелчок на кнопке Find приводит к выполнению нескольких операций, которые определены в коде подпрограммы btnFind_Click. После проверки числового типа значения в поле txtCustID начинается поиск этого значения в представлении DefaultView таблицы Customers. Если указанное значение найдено, то в поле со списком DataRowView будут показаны значения всех полей искомой записи с заданным идентификатором из таблицы Customers, а в сетке данных справа будут показаны ее дочерние записи из таблицы Orders.

Обратите внимание, что в данном примере создано отношение между двумя таблицами из баз данных совершенно разного типа!

Скомпонуйте проект BusinessCase7 и попробуйте найти родительскую запись в таблице Customers и ее дочерние записи из таблицы Orders, как показано на рис. 7.6.

РИС. 7.6. Пример представления результатов в форме frmShowOrders

НА ЗАМЕТКУ

Пусть вас не смущает, что сетка данных Orders (см. рис. 7.6) будет содержать поле с именем OrderAmount, а не поле Amount, как определено в таблице SQL Server. Дело в том, что данные о клиенте поступают из базы данных SQLServer, а данные о его заказах — из совершенно другой "унаследованной" базы данных Access типа MDB. Поэтому вполне естественно, что имена объектов в разных базах данных могут иметь различные имена.

Для исправления имени поля можно использовать приведенную ниже команду с предложением AS для изменения имени поля в данных, которые возвращаются для объекта daOrders с помощью команды SELECT.

SELECT CustomerID, ID, As Amount, OrderDate

FROM tblOrder

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

 

Строго типизированные наборы данных

До сих пор обсуждались варианты использования нетипизированного объекта DataSet. Однако в модели ADO.NET и среде Visual Studio. NET предусмотрены способы создания типизированных объектов DataSet. Типизированный объект DataSet является производным от нетипизированного класса DataSet, но содержит объекты, методы, свойства и события, специфические для используемой схемы данных. Она определяется в файле схемы с расширением .xsd, а в среде Visual Studio .NET предусмотрены графические инструменты для генерации набора классов на основе этой схемы.

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

• Типизированные объекты DataSet, DataTable и DataRow являются специализированными для используемой схемы.

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

• Для типизированных объектов можно выполнять проверку типов во время компиляции.

• В редакторе кода в среде Visual Studio .NET предусмотрено использование средства IntelliSense.

• Код имеет более краткую и читабельную форму.

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

dsCustomers.tables("Customers").Rows(row)("FirstName") = NewValue

Здесь row — это индекс в коллекции записей Rows таблицы Customers, FirstName — поле таблицы Customers, a NewValue — новое присваиваемое значение. При таком кодировании нетипизированного набора данных могут возникать разные ошибки. Соответствует ли тип нового значения NewValue типу поля, которому оно присваивается? Существует ли таблица Customers? Существует ли поле FirstName? Правильно ли указано имя поля или таблицы? При этом получить сообщение об ошибке можно будет только во время выполнения приложения.

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

dsCustomers.Customers(row).FirstName = NewValue

Обратите внимание, что объект-таблица Customers теперь является специализированным свойством типизированного набора данных DataSet, а поле FirstName — специализированным свойством типизированного объекта Customers. Продемонстрируем кратко, как в редакторе кода среды Visual Studio .NET используется средство IntelliSense.

Вернемся к таблице Departments для демонстрации этого способа работы на примере проекта DataSetCode из главы 5, "ADO.NET: объект DataSet".

1. Откройте проект DataSetCode в среде Visual Studio .NET.

2. В форме под кнопкой Data Views создайте кнопку, перетаскивая ее из панели элементов управления.

3. В окне Properties укажите значение btnTypedDataSet для свойства (Name) и значение Typed DataSet для свойства Text этой кнопки.

Теперь нужно включить в проект используемую схему: либо обратиться к уже готовому файлу с расширением .xsd, либо создать новый с помощью компонентов вкладки Data панели элементов управления.

1. Вставьте в форму новый компонент SqlDataAdapter. В диалоговом окне программы-мастера Data Adapter Configuration Wizard выберите подключение к базе данных Novelty. Затем в диалоговом окне Choose a Query Type выберите переключатель Use SQL statements. В диалоговом окне Generate the SQL statements укажите команду SELECT * FROM tblDepartment для загрузки данных в объект — набор данных. По окончании работы с программой-мастером Data Adapter Configuration Wizard укажите значение daDepartments для свойства (Name) для созданного объекта — адаптера данных.

2. Откройте диалоговое окно Generate DataSet (Создать набор данных), выбрав команду меню Data→Generate DataSet или щелкнув правой кнопкой мыши на форме и выбрав в контекстном меню команду Generate DataSet. В группе переключателей Choose a dataset (Выбрать набор данных) выберите переключатель New (Создать), введите в текстовом поле имя DepartmentDS, установите флажок Add this dataset to the designer (Включить этот набор данных в приложение) и щелкните на кнопке OK.

3. Укажите значение dsDepartments для свойства (Name) для созданного объекта — набора данных.

После выполнения этих действий генератор набора данных включит в проект файл схемы DepartmentDS.xsd и файл DepartmentDS.vb, который реализует специализированные классы набора данных DataSet. Файлы отображаются только после щелчка на кнопке Show all files (Показать все файлы) в верхней части окна Solution Explorer.

4. Щелкните дважды на файле DepartmentDS.xsd в окне Solution Explorer для отображения диалогового окна редактирования схемы XML.

5. Попробуйте внести небольшие изменения в имена элементов схемы. Вместо имени tblDepartment укажите в самом верхнем левом текстовом поле имя Departments, как показано на рис. 7.7.

РИС. 7.7. Схема DepartmentsDS в окне редактирования схемы

6. Щелкните правой кнопкой мыши на окне редактирования схемы и убедитесь в том, что в контекстном меню установлен флажок параметра Generate Dataset. Он устанавливается по умолчанию, но если он все же не установлен по какой-то причине, то установите его снова.

7. Сохраните схему и закройте окно редактирования схемы. В результате этих действий будет автоматически сгенерирован файл DepartmentsDS.vb.

НА ЗАМЕТКУ

Для просмотра содержимого файла DepartmentsDS.vb откройте его в окне редактирования кода. Учтите, что для указанного имени таблицы Departments генератор кода в дополнение к уже имеющемуся типизированному объекту создаст следующие объекты:

DepartmentsDataTable

DepartmentsDataRow

DepartmentsRowChangeEvent  

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

Листинг 7.8. Код отображения содержимого типизированного объекта DepartmentsDS

Private Sub btnTypedDataSet_Click(ByVal sender As System.Object, _

 ByVal e As System.EventArgs) Handles btnTypedDataSet.Click

 daDepartments.Fill(dsDepartments, "Departments")

 DisplayDepartments(dsDepartments)

End Sub

Private Sub DisplayDepartments(ByVal ds As DepartmentsDS)

 Me.lstOutput.Items.Clear()

 Me.lstOutput.Items.Add("DISPLAY TYPED DATASET")

 Me.lstOutput.Items.Add("=====================")

 ' Каждое поле теперь является свойством объекта DepartmentsDS.

 Dim row As DepartmentsDS.DepartmentsRow

 For Each row In ds.Departments.Rows

  Me.lstOutput.Items.Add( _

   ds.Departments.IDColumn.ColumnName _

   & ": " & row.ID.ToString)

  Me.lstOutput.Items.Add( _

   ds.Departments.DepartmentNameColumn.ColumnName _

   & ": " & row.DepartmentName)

 Next

End Sub

В подпрограмме btnTypedDataSet_Click сначала используется метод Fill объекта daDepartments для вставки данных в таблицу Department. Затем вызывается подпрограмма DisplayDepartments для отображения содержимого таблицы с помощью циклического обхода всех записей таблицы с указанием имени поля и значения в нем. Этот подход несколько отличается от описанных ранее.

• Переменная row создается с указанием типа записи DepartmentDS.DepertmentsRow, а не как универсальный объект DataRow.

• Таблица Departments доступна как свойство объекта ds.Departments, а не как элемент коллекции таблиц Tables, т.е. ds.Tables("Departments").

• Поля таблицы доступны как свойства объекта-таблицы, например ds.Column и ds.Departments.DepartmentNameColumn, а не как элементы коллекции полей Columns, т.е. ds.Tables("Departments").Columns("ID") и ds.Tables("Departments").Columns("DepartmentName").

• Значения полей доступны как свойства объекта-записи, например row.ID и row. DepartmentName, а не как элементы коллекции значений Items объекта-записи Rows, т.е. row("ID") и row("DepartmentName").

На рис. 7.8 показано, как типизированные свойства (например, DepartmentName) представлены в контекстном меню средства IntelliSense, которое ускоряет процесс создания кода.

РИС. 7.8. Контекстное меню средства IntelliSense со свойствами типизированного объекта DataSet

 

Резюме

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

 

Вопросы и ответы

Мне понятно, как определить нарушение параллельного доступа, но как его устранить?

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

Всегда ли следует использовать типизированные наборы данных?

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

Еще одним недостатком использования типизированных наборов данных является необходимость повторной генерации и компиляции типизированных объектов — наборов данных при изменении схемы данных.

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