|
||||
|
ГЛАВА 6ADO.NET: объект DataAdapter При изучении объектов DataSet, DataTable, DataRelation, DataRow, DataColumn, DataRelation и Constraint в главе 5, "ADO.NET: объект DataSet", у читателя возможно не раз возникало желание воскликнуть: "А где же, собственно, сама база данных?". Действительно, до сих пор ничего не говорилось о способе загрузки данных в объект DataSet из базы данных или другого источника, а также о последующем обновлении источника данных в соответствии с изменениями данных в объекте DataSet. Именно эту задачу и выполняет объект DataAdapter. Объект DataAdapter является промежуточным звеном между объектом DataSet и физическим источником данных. Он играет роль моста между двумя мирами ADO.NET: подключенным к источнику данных миром провайдеров данных .NET (которые рассматриваются в главе 4, "Модель ADO.NET: провайдеры данных") и неподключенным к источнику данных миром объектов DataSet (которые рассматриваются в главе 5, "ADO.NET: объект DataSet"). В структуре провайдеров данных .NET объект DataAdapter занимает центральное место. Каждый такой провайдер должен иметь собственную реализацию объекта DataAdapter (например, SqlDataAdapter, OleDbDataAdapter, OdbcDataAdapter и т.д.). Причина, по которой этот объект рассматривается только сейчас, заключается в том, что без понимания основных принципов работы с объектом DataSet и подчиненными объектами нельзя понять основные концепции работы с объектом DataAdapter. Теперь можно приступить к описанию объекта DataAdapter с учетом всех приведенных ранее сведений. На рис. 6.1 показана схема использования объекта DataAdapter. Основным назначением этого объекта является управление процессом передачи данных от источника данных к объекту DataSet и от объекта DataSet к источнику данных. Это выполняется с помощью небольшого набора свойств и методов, которые описываются далее. РИС. 6.1. Схема работы объекта DataAdapter — как моста между источником данных и объектом DataSet Основными методами объекта DataAdapter являются Fill и Update. Метод Fill наполняет объект DataAdapter данными, полученными от источника данных, а метод Update обновляет источник данных в соответствии с изменениями данных в таблицах объекта DataSet. Объект DataAdapter содержит набор из четырех свойств (Select Command, InsertCommand, UpdateCommand и DeleteCommand), которые являются объектами ADO.NET типа Command, конфигурированными для выполнения одноименных операций. Например, объект SelectCommand выполняется при вызове метода Fill объекта DataAdapter, а остальные объекты выполняются для каждой измененной записи при вызове метода Update объекта DataAdapter. Разработчики базы данных обладают полным контролем над этими командами, которые позволяют настраивать команды, используемые для операций вставки, обновления и удаления. Наконец, коллекция TableMappings объектов DataTableMapping позволяет указать соответствие между таблицей источника данных и объектом DataTable. Передача данных из источника данных в объект DataSetДля использования объекта DataAdapter требуется, как минимум, подключение к базе данных и команда Select. Для этого можно использовать объекты Connection и Command, т.е. создать их, конфигурировать и присвоить их свойствам Connection и SelectCommand объекта DataAdapter. Однако гораздо удобнее использовать для этого конструктор объекта DataAdapter с двумя строковыми параметрами: один для команды SELECT, а второй для строки подключения, как показано ниже. Dim da As SqlDataAdapter = New SqlDataAdapter( _ "select * from tblDepartment" _ "server=localhost;uid=sa;database=Novelty") НА ЗАМЕТКУ Попробуем теперь вызвать метод Fill для извлечения данных из базы данных Novelty и загрузки их в объект DataSet. Для этого вернитесь к проекту DataSetCode из главы 5, "ADO.NET: объект DataSet", и выполните перечисленные ниже действия. 1. Щелкните правой кнопкой мыши на проекте DataSetCode в окне Solution Explorer и выберите в контекстном меню команду Properties, чтобы открыть диалоговое окно DataSetCode Property Pages. 2. Выберите раздел General в папке Common Properties в правой части диалогового окна DataSetCode Property Pages, а затем выберите форму frmDataSets в поле Startup object. 3. Откройте форму frmDataSets в окне конструктора формы. 4. Создайте новую кнопку непосредственно под кнопкой Constraints, перетаскивая ее из панели элементов управления. 5. В окне свойств Properties укажите значение btnDataAdapterFill для свойства (Name) и значение DataAdapter Fill для свойства Text. 6. Для использования провайдера данных SqlClient нужно импортировать его пространство имен с помощью команды, отмеченной полужирным начертанием в следующем фрагменте кода: Imports System Imports System.Data Imports System.Data.SqlClient 7. Вставьте код, показанный в листинге 6.1. Листинг 6.1. Использование провайдера данных SqlClient для вставки данных В Набор данных dsEmployeeInfоPrivate Sub btnDataAdapterFill_Click(ByVal sender As _ System.Object, ByVal e As System.EventArgs) _ Handles btnDataAdapterFill.Click ReadData() End Sub Private Sub ReadData() Dim rows As Integer Dim daDepartments As SqlDataAdapter = New SqlDataAdapter( _ "select * from tblDepartment", _ "server=localhost;uid=sa;database=novelty") dsEmployeeInfo = New DataSet() rows = daDepartments.Fill(dsEmployeeInfo, "Departments") DisplayDataSet(dsEmployeeInfo) End Sub После создания объекта daDepartments с двумя аргументами, которые содержат команду Select и строку подключения, вызывается метод Fill для наполнения данными таблицы Departments набора данных dsEmployeeInfo. Метод Fill также возвращает количество записей, которые включены (или обновлены) в набор данных dsEmployeeInfo. При выполнении метода Fill провайдером данных неявно выполняются перечисленные ниже действия. • Для объекта SelectCommand открывается подключение к источнику данных, если оно еще не открыто. • Выполняется команда, указанная в свойстве CommandText объекта SelectCommand (вместе с параметрами, если таковые имеются). • Создается объект DataReader для возвращения имен полей и типов, использованных для создания нового объекта DataTable в указанном наборе данных DataSet, если этого объекта еще не существует. • Объект DataReader используется для извлечения данных и вставки их в таблицу. • Объект DataReader закрывается. • Подключение к источнику данных закрывается, если оно было открыто объектом DataReader, в противном случае оно остается открытым. НА ЗАМЕТКУ Методу Fill передается ссылка на набор данных dsEmployeeInfo и имя таблицы Departments, в которую вставляются данные. Вместо имени таблицы можно было бы передать ссылку на объект DataTable. Еще один вариант указания параметров основан на передаче только ссылки на объект DataSet, а метод Fill в таком случае по умолчанию загрузит данные в объект DataTable по имени Table. НА ЗАМЕТКУ Для загрузки данных во вторую таблицу можно создать второй объект DataAdapter с другой командой Select. В данном примере для загрузки данных в таблицы Department и Employees из соответствующих таблиц базы данных нужно использовать приведенный ниже код для подпрограммы ReadData() вместо кода из листинга 6.1. Private Sub ReadData() Dim rows As Integer Dim daDepartments As SqlDataAdapter = New SqlDataAdapter(_ "select * from tblDepartment", _ "server=localhost;uid=sa;database=novelty") Dim daEmployees As SqlDataAdapter = New SqlDataAdapter(_ "select * from tblEmployee", _ "server=localhost;uid=sa;database=novelty") dsEmployeeInfo = New DataSet() rows = daDepartments.Fill(dsEmployeeInfo, "Departments") rows = daEmployees.Fill(dsEmployeeInfo, "Employees") DisplayDataSet(dsEmployeeInfo) End Sub Скомпонуйте проект DataSetCode, щелкните на кнопке DataAdapter Fill, и в поле со списком, как и прежде, будет отображена информация о содержании набора данных dsEmployeeInfo, но теперь она извлекается из базы данных под управлением SQL Server, а не генерируется локально кодом приложения. Для установления родительско-дочерних связей между записями в этих таблицах можно создать объект DataRelation, который служит отношением между ними. НА ЗАМЕТКУ Совсем необязательно использовать разные объекты DataAdapter для таблиц одного набора данных. Иногда для них можно использовать один объект DataAdapter, только изменяя параметр с текстом команды SQL. Этот способ прекрасно подходит для многочисленных вызовов метода Fill, так как программирование всех отдельных операций создания и изменения команд обновления (вставки, обновления и удаления) каждого объекта DataTable в соответствии с изменениями источника данных требует много времени и усилий. Итак, для использования одного объекта DataAdapter предыдущий код следует заменить приведенным ниже. Private Sub ReadData() Dim rows As Integer Dim da As SqlDataAdapter = New SqlDataAdapter( _ "select * from tblEmployee", _ "server=localhost;uid=sa;database=novelty") dsEmployeeInfo = New DataSet() rows = da.Fill(dsEmployeeInfo, "Employees") ' Изменение текста команды SQL. da.SelectCommand.CommandText = _ "select * from tblDepartment" rows = da.Fill(dsEmployeeInfo, "Departments") DisplayDataSet(dsEmployeeInfo) End Sub НА ЗАМЕТКУ В листинге 6.2 демонстрируется еще один способ использования одного объекта DataAdapter для выполнения нескольких операций вставки данных в один объект DataTable. Листинг 6.2. Использование одного объекта DataAdapter для нескольких операций вставки данных в один объект DataTablePrivate Sub ReadData() Dim daEmployees As SqlDataAdapter = New SqlDataAdapter(_ "select * from tblEmployee where DepartmentID = 1", _ "server=localhost;uid=sa;database=novelty") dsEmployeeInfo = New DataSet() daEmployees.Fill(dsEmployeeInfo, "Employees") ' Изменение текста команды SQL. daEmployees.SelectCommand.CommandText = _ "select * from tblEmployee where DepartmentID = 3" daEmployees.Fill(dsEmployeeInfo, "Employees") DisplayDataSet(dsEmployeeInfo) End Sub Обратите внимание, что в листинге 6.2 возвращаемое значение метода Fill уже не сохраняется в локальной переменной rows. Совсем необязательно сохранять возвращаемое значение, если только в дальнейшем не предполагается проверять его или использовать каким-то другим способом. Предлагаемый в листинге 6.2 код можно было бы расширить несколькими командами Select для обновления объекта DataTable наиболее свежими данными (например, измененными другими пользователями) из источника данных. НА ЗАМЕТКУ Обновление источника данныхОбычно после внесения всех необходимых изменений в таблицы набора данных DataSet потребуется сохранить эти изменения в источнике данных. Для этого нужно вызвать метод Update объекта DataAdapter, который анализирует изменения в указанной таблице набора данных (или сразу во всех таблицах, если ни одна из них не указана явно). Для каждой измененной записи по отношению к источнику данных выполняется команда вставки, обновления или удаления с помощью соответствующего объекта InsertCommand, UpdateCommand или DeleteCommand. НА ЗАМЕТКУ Каждая измененная запись обновляется отдельно, а не как часть транзакции или пакетной операции. Причем порядок обновления записей определяется порядком их расположения в объекте DataTable. Для явного управления порядком выполнения операций для заданной таблицы можно использовать методы GetChanges или Select, которые доступны на уровне либо объекта DataSet, либо объекта DataTable. Эти методы используются для извлечения отдельных наборов записей с разными состояниями записей. Допустим, необходимо обновить базу данных Novelty данными из объекта dsEmployees с помощью объекта daDepartments. Причем сначала требуется выполнить все вставки, затем все обновления, а потом все удаления. Это можно сделать, трижды вызывая метод GetChanges с указанием соответствующих разных состояний записей. После каждого вызова метода GetChanges вызывается метод Update объекта DataAdapter с передачей объекта DataTable, возвращенного методом GetChanges. dt = dsEmployeeInfo.Tables("Departments") Внести изменения для каждого типа состояния. dtChanged = dt.GetChanges(DataRowState.Added) daDepartments.Update(dtChanged) dtChanged = dt.GetChanges(DataRowState.Modified) daDepartments.Update(dtChanged) dtChanged = dt.GetChanges(DataRowState.Deleted) daDepartments.Update(dtChanged) Этот код можно записать более компактно. dt = dsEmployeeInfo.Tables("Departments") ' Внести изменения для каждого типа состояния. daDepartments.Update(dt.GetChanges(DataRowState.Added)) daDepartments.Update(dt.GetChanges(DataRowState.Modifled)) daDepartments.Update(dt.GetChanges(DataRowState.Deleted)) Аналогичный результат можно получить с помощью метода Select. dt = dsEmployeeInfo.Tables("Departments") ' Внести изменения для каждого типа состояния. daDepartments(Nothing, Nothing, _ dt.GetChanges(DataViewRowState.Added)) daDepartments.Update(Nothing, Nothing, _ dt.GetChanges(DataViewRowState.ModifiedCurrent)) daDepartments.Update(Nothing, Nothing, _ dt.GetChanges(DataViewRowState.Deleted)) Преимущество использования метода Select вместо GetChanges заключается в том, что он может выполнять фильтрацию и сортировку. Здесь следует напомнить о разнице между операциями удаления записи с помощью методов Remove и о которой сообщалось в главе 5, "ADO.NET: объект DataSet". При использовании метода Remove запись навсегда удаляется из коллекции, а при удалении с помощью метода Delete — только отмечается как удаленная. При обновлении источника данных данными из объекта DataTable с помощью объекта DataAdapter нужно использовать метод Delete, а не Remove. Когда объект DataAdapter встречает помеченную для удаления запись, он автоматически выполняет команду DeleteCommand для базы данных, чтобы осуществить синхронизацию с объектом DataTable. Если вместо метода Delete использовать Remove, то объект DataAdapter не обнаружит уже удаленную запись из объекта DataTable и не сможет удалить ее из источника данных. Указание команд обновленияОбъект DataAdapter не создает автоматически команды INSERT, UPDATE и DELETE для обновления источника данных в соответствии с изменениями данных в объекте DataSet. Если при вызове команды метода Update не была указана команда INSERT, UPDATE или DELETE, то генерируется исключительная ситуация. Эти команды можно указать одним их следующих способов: • использовать объект CommandBuilder для автоматической генерации команд во время выполнения приложения; • явно запрограммировать эти команды; • использовать компонент DataAdapter DesignTime Component и мастер конфигурирования объекта DataAdapter Configuration Wizard. Использование объекта CommandBuilderЭто самый простой способ, но он связан с существенными ограничениями. Он аналогичен применению метода BatchUpdate в модели ADO 2.X. Когда объект CommandBuilder связывается с соответствующим ему объектом DataAdapter, он автоматически может задавать значения свойств InsertCommand, UpdateCommand и DeleteCommand для данного объекта DataAdapter. А если эти свойства имеют ненулевые значения (т.е. для них не задано значение Nothing), то объект-команда уже существует и объект CommandBuilder не переопределяет ее. НА ЗАМЕТКУ Для автоматической генерации команд нужно указать объект SelectCommand адаптера данных DataAdapter. Объект CommandBuilder использует схему таблицы, полученную с помощью метода Select объекта SelectCommand для генерации соответствующей команды вставки, обновления или удаления. Учтите, что поля, возвращаемые объектом SelectCommand, должны содержать первичный ключ или поле с уникальными значениями. Изменение текста команды SELECT после автоматической генерации команд обновления может привести к возникновению исключительных ситуаций при одновременном выполнении этой команды обновления. Если в исходной команде SELECT, на основании которой автоматически генерируются команды обновления данных, содержались поля, которых уже нет в генерированной команде, то при выполнении одной из команд обновления с помощью метода Update объекта DataAdapter может произойти попытка обращения к уже несуществующим полям и возникновение исключительной ситуации. Для исключения этой проблемы следует вызвать метод RefreshSchema объекта CommandBuilder после изменения свойства SelectCommand объекта DataAdapter или после изменения текста (объект CommandText) данной команды. НА ЗАМЕТКУ Хотя конструктор команд CommandBuilder прост в применении, нужно учитывать его ограничения. Основное ограничение связано с тем, что нельзя проконтролировать выполняемые им действия, т.е. задать его конфигурацию. Он просто генерирует команды обновления на основе заданной команды SELECT без каких-либо параметров. Кроме того, он предназначен для выполнения команд по отношению к одной таблице базы данных. Иначе говоря, объект DataTable с результатами объединения таблиц не может использоваться объектом CommandBuilder. Более того, команды генерируются для одной таблицы без учета ее возможных связей с другими таблицами базы данных, что может привести к нарушению целостности данных при наличии между таблицами родительско-дочерних отношений. Еще одно ограничение объекта CommandBuilder связано с тем, что он не способен генерировать команды обновления, если имя таблицы или поля содержит специальные символы, например пробел, точку или другие не буквенно-цифровые символы, даже если это имя заключено в кавычки. Однако он может работать с полностью квалифицированными именами таблиц в формате база_данных.владелец.таблица. Чтобы извлекать и сохранять данные в базе данных с помощью объекта DataAdapter, создадим в проекте DataSetCode еще одну форму. Для этого выполните перечисленные ниже действия. 1. Создайте новую кнопку непосредственно под кнопкой DataAdapter Fill, перетаскивая ее из панели элементов управления. В окне свойств Properties укажите значение btnDataAdapterUpdate для свойства (Name) и значение DataAdapter Updates для свойства Text. 2. Создайте в проекте DataSetCode новую форму. 3. В окне свойств Properties укажите значение frmUpdates для свойства (Name) и значение DataAdapter Updates для свойства Text. 4. Увеличьте размер формы frmUpdates. 5. Создайте в правой части формы сетку данных DataGrid, перетаскивая ее из панели элементов управления. 6. В окне Properties укажите значение grdDataGrid для свойства (Name) сетки данных. 7. Увеличьте размер сетки данных, чтобы она занимала до 80% всей площади формы. 8. В верхнем левом углу формы создайте кнопку, перетаскивая ее из панели элементов управления. 9. В окне Properties укажите значение btnLoad для свойства (Name) и значение Load для свойства Text этой кнопки. 10. Создайте новую кнопку непосредственно под кнопкой btnLoad, перетаскивая ее из панели элементов управления. 11. В окне Properties укажите значение btnLoad для свойства (Name), значение Load для свойства Text и значение False для свойства Enabled этой кнопки. 12. В верхней части файла с кодом формы frmUpdates введите следующий код: Imports System Imports System.Data Imports System.Data.SqlClient 13. Затем в код формы frmUpdates введите код из листинга 6.3. Листинг 6.3. Применение объекта SqlCommandBuilder для автоматической генерации команд обновленияPrivate dsEmployeeInfo As DataSet Private daEmployees As SqlDataAdapter Private conn As New SqlConnection( _ "server=localhost;uid=sa;pwd=;database=novelty") ' Применение объекта SqlCommandBuilder ' для автоматической генерации команд обновления. Private cbEmployees As SqlCommandBuilder Private Sub btnLoad_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnLoad.Click dsEmployeeInfo = New DataSet() LoadCommandBuilder() ' Конфигурирование объекта-сетки DataGrid. Me.grdDataGrid.PreferredColumnWidth = 110 Me.grdDataGrid.AllowSorting = True ' Вставка данных в объект DataSet. daEmployees.Fill(dsEmployeeInfo, "Employees") ' Присвоение объекта DataSet объекту DataGrid. Me.grdDataGrid.DataSource = _ dsEmployeeInfo.Tables("Employees") Me.btnUpdate.Enabled = True End Sub Private Sub btnUpdate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnUpdate.Click daEmployees.Update(dsEmployeeInfo, "Employees") End Sub Private Sub LoadCommandBuilder() Dim param As SqlParameter If conn.State = ConnectionState.Closed Then conn.Open() End If ' Создание нового объекта DataAdapter. Dim SQL As String SQL = "Select FirstName, LastName, Department ID, Salary, _ ID from tblEmployee" daEmployees = New SqlDataAdapter(SQL, conn) ' Создание команды с помощью SqlCommandBuilder. cbEmployees = New SqlCommandBuilder(daEmployees) End Sub Основная подпрограмма LoadCommandBuilder() вызывается после щелчка на кнопке Load. Она демонстрирует способ явного открытия подключения, возможность избежать возникновения исключительных ситуаций при нескольких последовательных щелчках на кнопке Load и конфигурирование адаптера данных SqlDataAdapter (объект daEmployees) и конструктора команд SqlCommandBuilder (объект cbEmployees). Эти два объекта создаются и инициализируются с помощью их конструкторов, которые принимают значения своих свойств в качестве параметров. Конструктор объекта daEmployees получает строку с командой SELECT и объект подключения, а объект cbEmployees — адаптер данных daEmployees. НА ЗАМЕТКУ Далее конфигурируется объект-сетка, вызывается метод Fill для загрузки таблицы Employees в объект daEmployees, задается таблица Employees для свойства DataSource объекта daEmployees и автоматически отображаются данные в сетке. Обработчик события щелчка на кнопке Update содержит только одну строку кода, которая просто вызывает метод Update для объекта daEmployees. Скомпонуйте проект DataSetCode и щелкните на кнопке DataAdapter Updates в форме frmDataSets. После отображения на экране формы frmUpdates щелкните на кнопке Load. Это приведет к считыванию данных из таблицы Employees, загрузке данных в объект tblEmployees объекта dsEmployeeInfo и отображению их в сетке, как показано на рис. 6.2. Теперь можно протестировать работу этой формы, внося в данные произвольные изменения. Например, добавьте в сетку новые записи, прокручивая ее вниз и вводя новые данные в последней пустой строке сетки. Удалите несколько строк, выделяя их и нажимая клавишу <Delete>. Измените значения некоторых данных в уже имеющихся записях. Учтите, что все эти изменения не будут отражены в базе данных до тех пор, пока пользователь не щелкнет на кнопке Update. Внесенные в базу данных изменения можно проверить, используя любые инструменты просмотра таблиц базы данных или щелкая на кнопке Load для повторной загрузки данных из таблиц базы данных в объект grdDataGrid формы frmUpdates. РИС. 6.2. Отображение данных из таблицы tblEmployees в сетке grdDataGrid НА ЗАМЕТКУ Явное указание команд обновленияВ отличие от довольно простого способа генерации нужных команд обновления с помощью объекта CommandBuilder, явное указание команд обновления в коде представляет собой хотя и более гибкий, но весьма трудоемкий способ. При его использовании каждая из четырех команд управления данными (Select, Insert, Update и Delete) должна быть создана отдельно. Чаще всего они кодируются в виде хранимой процедуры для каждой команды SQL. В листинге 6.4 представлен сценарий SQL Server для генерации четырех хранимых процедур. Хранимая процедура SelectEmployees просто выбирает все поля таблицы tblEmployee. Хранимая процедура InsertEmployee принимает четыре параметра, т.е. по одному для каждого обновляемого поля. Хранимая процедура UpdateEmployee принимает те же четыре параметра для обновляемых полей и еще один параметр для исходного значения идентификационного поля, которое используется в предложении WHERE для выбора обновляемой записи (на основании первичного ключа). Хранимая процедура DeleteEmployee принимает исходное значение идентификационного поля для удаления указанной записи. Листинг 6.4. Сценарий SQL Server для создания хранимых процедур для таблицы tblEmployeeIF EXISTS (SELECT * FROM sysobjects WHERE name = 'SelectEmployees' AND user__ name(uid) = 'dbo' DROP PROCEDURE [dbo].[SelectEmployees] GO CREATE PROCEDURE [dbo].[SelectEmployees] AS SET NOCOUNT ON; SELECT FirstName, LastName, DepartmentID, Salary, ID FROM tblEmployee GO IF EXISTS (SELECT * FROM sysobjects WHERE name = 'InsertEmployee' AND user_name(uid) = 'dbo' DROP PROCEDURE [dbo].[InsertEmployee] GO CREATE PROCEDURE [dbo].[InsertEmployee] ( @FirstName varchar(50), @LastName varchar(70), @DepartmentID int, @Salary money ) AS SET NOCOUNT ON; SELECT INTO tblEmployee(FirstName, LastName, DepartmentID, Salary) VALUES (@FirstName, @LastName, @DepartmentID, @Salary) GO IF EXISTS (SELECT * FROM sysobjects WHERE name = 'UpdateEmployee' and user_name(uid) = 'dbo' DROP PROCEDURE [dbo].[UpdateEmployee] GO CREATE PROCEDURE [dbo].[UpdateEmployee] ( @FirstName varchar(50), @LastName varchar(70), @DepartmentID int, @Salary money, @Original_ID int ) AS SET NOCOUNT ON; UPDATE tblEmployee SET FirstName = @FirstName, LastName = @LastName, DepartmentID = @DepartmentID, Salary = @Salary WHERE (ID = @Original_ID) GO IF EXISTS (SELECT * FROM sysobjects WHERE name 'DeleteEmployee' and user_name DROP PROCEDURE [dbo].[DeleteEmployee] GO create procedure [dbo].[DeleteEmployee] (@Original_ID int) AS SET NOCOUNT ON; DELETE FROM tblEmployee WHERE (ID = @Original_ID) GO Вернемся к коду приложения. Во-первых, изменим первую строку кода в подпрограмме btnLoad_Click, т.е. вместо вызова подпрограммы LoadCommandBuilder вставим вызов подпрограммы LoadExplicitCode. Кроме того, для отладки явно заданного кода для команды обновления нужно добавить блок Try-Catch в подпрограмму btnUpdate_Click, которая теперь будет иметь приведенный ниже вид. Private Sub btnUpdate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnUpdate.Click Try daEmployees.Update(dsEmployeeInfo, "Employees") Catch es As SqlException MessageBox.Show(es.Message) End Try End Sub Наконец, код подпрограммы LoadExplicitCode будет выглядеть так, как показано в листинге 6.5. Листинг 6.5. Подпрограмма LoadExplicitCode для четырех специализированных команд SQL для адаптера данных daEmployeesPrivate Sub LoadExplicitCode() Dim param As SqlParameter If conn.State = ConnectionState.Closed Then conn.Open() End If ' Создание нового объекта DataAdapter. daEmployees = New SqlDataAdapter() ' Создание специализированной ' хранимой процедуры для команды Select. daEmployees.SelectCommand = New SqlCommand() With daEmployees.SelectCommand .Connection = conn .CommandType = CommandType.StoredProcedure .CommandText = "SelectEmployees" End With ' Создание специализированной ' хранимой процедуры для команды Insert. daEmployees.InsertCommand = New SqlCommand() With daEmployees.InsertCommand .Connection = conn .CommandType = CommandType.StoredProcedure.CommandText = "InsertEmployee" End With param = daEmployees.InsertCommand.Parameters.Add(_ New SqlParameter(@FirstName", SqlDbType.VarChar, 50)) param.Direction = ParameterDirection.Input param.SourceColumn = "FirstName" param.SourceVersion = DataRowVersion.Current param = daEmployees.InsertCommand.Parameters.Add( _ New SqlParameter("@LastName", SqlDbType.VarChar, 70)) param.Direction = ParameterDirection.Input param.SourceColumn = "LastName" param.SourceVersion = DataRowVersion.Current param = daEmployees.InsertCommand.Parameters.Add(_ New SqlParameter("@DepartmentID, SqlDbType.Int)) param.Direction = ParameterDirection.Input param.SourceColumn = "DepartmentID" param.SourceVersion = DataRowVersion.Current param = daEmployees.InsertCommand.Parameters.Add( _ New SqlParameter("@Salary", SqlDbType.Money)) param.Direction = ParameterDirection.Input param.SourceColumn = "Salary" param.SourceVersion = DataRowVersion.Current ' Создание специализированной ' хранимой процедуры для команды Update. daEmployees.UpdateCommand = New SqlCommand() With daEmployees.UpdateCommand .Connection = conn .CommandType = CommandType.StoredProcedure .CommandText = "UpdateEmployee" End With param = daEmployees.UpdateCommand.Parameters.Add( _ New SqlParameter("@FirstName@, SqlDbType.VarChar, 50)) param.Direction = ParameterDirection.Input param.SourceColumn = "FirstName" param.SourceVersion = DataRowVersion.Current param = daEmployees.UpdateCommand.Parameters.Add( _ New qlParameter("@LastName", SqlDbType.VarChar, 70)) param.Direction = ParameterDirection.Input param.SourceColumn = "LastName" param.SourceVersion = DataRowVersion.Current param = daEmployees.UpdateCommand.Parameters.Add( _ New SqlParameter("@DepartmentID, SqlDbType.Int)) param.Direction = ParameterDirection.Input param.SourceColumn = "DepartmentID" param.SourceVersion = DataRowVersion.Current param = daEmployees.UpdateCommand.Parameters.Add( _ New SqlParameter("@Salary, SqlDbType.Money)) param.Direction = ParameterDirection.Input param.SourceColumn = "Salary" param.SourceVersion = DataRowVersion.Current param = daEmployees.UpdateCommand.Parameters.Add( _ New SqlParameter("@Original_ID, SqlDbType.Int)) param.Direction = ParameterDirection.Input param.SourceColumn = "ID" param.SourceVersion = DataRowVersion.Original ' Создание специализированной ' хранимой процедуры для команды Delete. daEmployees.DeleteCommand = New SqlCommand() With daEmployees.DeleteCommand .Connection = conn .CommandType = CommandType.StoredProcedure .CommandText = "DeleteEmployee" End With param = daEmployees.DeleteCommand.Parameters.Add(_ New SqlParameter("@Original_ID", SqlDbType.Int)) param.Direction = ParameterDirection.Input param.SourceColumn = "ID" param.SourceVersion = DataRowVersion.Original End Sub НА ЗАМЕТКУ Невзирая на большой размер, код подпрограммы LoadExplicitCode имеет очень простую и понятную структуру, если определен интерфейс (параметры и типы) для хранимых процедур. Для всех свойств объекта Command создается новый экземпляр объекта SQLCommand. Ему присваивается общий объект Connection и задаются значения свойств CommandType и CommandText. Затем нужно создать и конфигурировать все параметры каждой команды. Скомпонуем проект DataSetCode и снова проверим работоспособность полученного приложения. Оно будет работать как и прежде, но теперь в подпрограмме LoadExplicitCode используются специализированные команды для обновления базы данных. Этот подход требует больших усилий по созданию кода, но является более гибким, предлагает более высокую производительность и централизованное управление хранимыми процедурами. Вставка бизнес-логики в команды обновленияПрежде уже описывалось, как хранимые процедуры для специализированных команд обновления можно использовать для вставки бизнес-логики в хранимые процедуры, которые вызываются автоматически. По сравнению с прежними версиями модели ADO и другими моделями доступа к данным новизна заключается не во вставке логики в хранимые процедуры, поскольку эта функциональная возможность существовала и раньше. Дело в том, что эти хранимые процедуры вызываются автоматически при выполнении "пакетных" обновлений вместо явной организации вызовов в коде программы. Для демонстрации этого подхода попробуем изменить хранимую процедуру. Предположим, что бизнес-логика определяется следующим образом: если при вставке новой записи о сотруднике значение зарплаты в поле Salary не определено или равно 0, то для него автоматически задается значение, определяемое функцией отдела. Для реализации этой бизнес-логики применим очень простой механизм: автоматически присвоенное значение будет равно произведению номера отдела и значения 10000. (Конечно, в реальных условиях компания может использовать более удачный алгоритм для указания этого значения!) Измененная хранимая процедура теперь будет выглядеть так, как показано ниже. CREATE PROCEDURE dbo.SelectEmployees ( @FirstName varchar(50), @LastName varchar(70), @DepartmentID int, @Salary money) AS SET NOCOUNT ON; if (@Salary = 0 or @Salary is null) begin -– Вычисления set @Salary = @DepartmentID * 1000 end INSERT INTO tblEmployee (FirstName, LastName, DepartmentID, Salary) VALUES (@FirstName, @LastName, @DepartmentID, @Salary) GO НА ЗАМЕТКУ Теперь можно вновь запустить приложение проекта DataSetCode без внесения каких-либо изменений в его код. Попробуйте вставить в форму несколько новых записей с пустыми значениями поля Salary и проверьте работоспособность новой хранимой процедуры, т.е. корректного автоматического присвоения значений для этого поля. Использование компонента DataAdapter во время создания приложенияПосле описания в предыдущем разделе способа создания кода вручную продемонстрируем метод автоматического создания кода с помощью программы-мастера Data Adapter Configuration Wizard. Программа-мастер Data Adapter Configuration Wizard предлагает целый набор параметров для конфигурирования объекта DataAdapter во время создания приложения. Она обладает более широкими функциональными возможностями, чем другие компоненты времени создания приложения. Она содержит графический интерфейс для управления многими внутренними объектами и свойствами компонента, а также инструменты автоматического создания кода. 1. Откройте форму frmUpdates в режиме конструктора формы. 2. Из вкладки Data выберите и перетащите в форму компонент SqlDataAdapter. Этот компонент невидим во время выполнения, поэтому он появится под формой среди других невизуальных компонентов. При этом на экране откроется первое диалоговое окно программы-мастера Data Adapter Configuration Wizard. Щелкните на кнопке Next. 3. В следующем диалоговом окне, Choose Your Data Connection (Выберите подключение к базе данных), выберите подключение к базе данных Novelty. Если оно не указано, то его следует создать, щелкнув на кнопке New Connection (Новое подключение) и указав характеристики подключения в стандартном диалоговом окне Data Link Properties (Свойства соединения с данными). После выбора подключения щелкните на кнопке Next. 4. В следующем диалоговом окне, Choose a Query Type (Выбрать тип запроса), можно выбрать тип запроса: Use SQL statements (Использовать команды SQL), Create new stored procedures (Создать новые хранимые процедуры) или Use existing stored procedures (Использовать существующие хранимые процедуры). В данном примере выберите предлагаемый по умолчанию тип запроса Use SQL Statements, хотя на практике бывает полезно использовать созданные программой-мастером хранимые процедуры. 5. В следующем диалоговом окне, Generate the SQL Statements (Создать SQL-команду), введите в текстовую область приведенную ниже команду SQL, которая будет использоваться объектом DataAdapter в качестве основы для других команд обновления. SELECT FirstName, LastName, DepartmentID, Salary, ID FROM tblEmployee В этом диалоговом окне есть еще две кнопки. После щелчка на кнопке Advanced Options (Дополнительные возможности) отображается диалоговое окно Advanced SQL Generation Options (Дополнительные возможности для генерации команд SQL) с тремя параметрами. Первый параметр, Generate Insert, Update and Delete Statements (Создать команды вставки, обновления и удаления), указывает на необходимость создания команд вставки, обновления и удаления на основе указанной команды SQL (или нужно использовать объект DataAdapter только для наполнения данными набора данных DataSet). Второй параметр, Use Optimistic concurrency (Использовать оптимистическую обработку параллельно выполняемых команд), означает включение в команду SQL предложения WHERE для всех значений полей в обновленной записи. Это позволяет обнаружить изменения данной записи, внесенные другими пользователями в базе данных, так как в этом случае такая команда SQL будет отвергнута. Третий параметр, Refresh the Dataset (Обновить набор данных Dataset), позволяет добавить еще одну команду SELECT к создаваемым командам вставки и обновления данных для возвращения значений идентификационных полей, предлагаемых по умолчанию значений полей, а также вычисленных сервером значений. После щелчка на кнопке Query Builder (Конструктор запроса) отображается одноименное стандартное диалоговое окно для графического создания команды SELECT вместо непосредственного ввода ее в текстовой области диалогового окна Generate the SQL statements. 6. Щелкните на кнопке Next для отображения диалогового окна View Wizard Results (Просмотр результатов создания команды SQL с помощью программы-мастера). 7. Щелкните на кнопке Finish для применения указанных параметров объекта DataAdapter. НА ЗАМЕТКУ Теперь нужно связать объект DataAdapter, которому автоматически присвоено имя SqlDataAdpater1, с созданным кодом. Для этого нужно явно открыть созданное подключение SqlConnection1. Подпрограмму btnLoad_Click нужно изменить таким образом, чтобы она вызывала подпрограмму LoadWizardCode вместо подпрограммы LoadExplicitCode. Далее нужно вызвать метод Fill вновь созданного объекта SqlDataAdpater1. Кроме того, для использования нового объекта SqlDataAdpater1 потребуется изменить код подпрограммы btnLoad_Click. Наконец, нужно создать код подпрограммы LoadWizardCode, которая предназначена для открытия нового подключения. Эти три подпрограммы показаны в листинге 6.6. Листинг 6.6. Измененные и новые подпрограммы для использования нового объекта SqlDataAdapter1 в уже существующем приложенииPrivate Sub btnLoad_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnLoad.Click dsEmployeeInfo = New DataSet() LoadWizardCode() ' Конфигурирование объекта-сетки DataGrid. Me.grdDataGrid.PreferredColumnWidth = 110 Me.grdDataGrid.AllowSorting = True ' Вставка данных в объект DataSet. daEmployees.Fill(dsEmployeeInfo, "Employees") ' Присвоение объекта DataSet объекту DataGrid. Me.grdDataGrid.DataSource = _ dsEmployeeInfo.Tables("Employees") Me.btnUpdate.Enabled = True End Sub Private Sub btnUpdate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnUpdate.Click Try ' daEmployees.Update(dsEmployeeInfo, "Employees") SqlDataAdapter1.Update(dsEmployeeInfo, "Employees") Catch es As SqlException MessageBox.Show(es.Message) End Try End Sub Private Sub LoadWizardCode() If SqlConnection1.State = ConnectionState.Closed Then SqlConnection1.Open() End If End Sub Для просмотра кода, автоматически созданного программой-мастером, нужно открыть раздел Windows Form Designer generated code (Код, сгенерированный конструктором Windows Form). Он напоминает код, созданный вручную в листинге 6.5. НА ЗАМЕТКУ Скомпонуйте проект DataSetCode и запустите полученное приложение для проверки его работоспособности. Как видите, программа-мастер Data Adapter Configuration Wizard позволяет с удивительной легкостью и быстротой создавать полностью функциональный код. Это усовершенствование смогут по достоинству оценить те программисты, которым раньше приходилось вручную создавать код для хранимых процедур. Во время создания приложения можно воспользоваться еще одной функциональной возможностью компонента DataAdapter — инструментом предварительного просмотра данных. Для этого щелкните правой кнопкой мыши на объекте DataAdapter и выберите в контекстном меню команду Preview Data (Предварительный просмотр данных) или выберите объект DataAdapter и щелкните на ссылке Preview Data в той части окна свойств Properties, которая находится между списком свойств и их описанием. В списке Data adapters (Объекты – адаптеры данных) выберите нужный объект-адаптер и щелкните на кнопке Fill Dataset (Вставить данные в набор данных). На рис. 6.3 показан результат выполнения этих действий для объекта SqlDataAdapter в форме frmUpdates. РИС. 6.З. Данные, вставленные в набор данных из таблицы tblEmployee с помощью диалогового окна предварительного просмотра данных Data Adapter Preview Бизнес-ситуация 6.1: комбинация нескольких связанных таблицКак уже отмечалось, ни один из перечисленных выше методов указания команд обновления не позволяет обновлять данные сразу в нескольких таблицах, особенно если они связаны родительско-дочерним отношением. Значит ли это, что в модели ADO.NET не поддерживается обработка такой ситуации? Нет, это не так. В данной бизнес-ситуации для доказательства этого утверждения демонстрируются функциональные возможности модели ADO.NET, в частности применение пакета команд SQL для вставки данных из двух таблиц за счет одного обращения к серверу. Итак, программист компании Jones Novelties, Inc. создает форму для отображения и обновления данных о клиентах и их заказах. Для создания такой формы выполните перечисленные ниже действия. РИС. 6.4. Расположение элементов управления в форме frmCustomersOrders 1. Запустите среду разработки Visual Studio .NET. 2. Создайте новый проект Visual Basic Windows Application. 3. Назовите проект BusinessCase6. 4. Укажите путь к файлам проекта. 5. Увеличьте размер формы Form1 и в окне Properties укажите значение frmCustomersOrders для свойства (Name) и значение Customers and Orders для свойства Text формы Form1. 6. Перетащите в форму кнопку и в окне Properties укажите значение bntFill для ее свойства (Name) и Fill для свойства Text; перетащите в форму кнопку и в окне Properties укажите значение bntUpdate для ее свойства (Name) и значение Update для свойства Text; перетащите в форму сетку данных и в окне Properties укажите значение grdCustomersOrders для ее свойства (Name). 7. Расположите все элементы управления, как показано на рис. 6.4. В верхней части файла с исходным кодом вставьте следующие строки кода для импорта пространств имен System. Data и System.Data.SqlClient: Imports System.Data Imports System.Data.SqlClient В тело определения класса для формы frmCustomersOrders включите код из листинга 6.7. Листинг 6.7. Код загрузки и обновления данных сразу в нескольких связанных таблицахPrivate ds As DataSet Private en As New SqlConnection( _ "server=localhost;uid=sa;database=Novelty") Private Sub btnFill_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnFill.Click Dim da As New SqlDataAdapter() grdCustomersOrders.DataSource = Nothing ds = New DataSet() ' Создание команды SELECT. da.SelectCommand = New SqlCommand() da.SelectCommand.Connection = cn da.SelectCommand.CommandType = CommandType.Text da.SelectCommand.CommandText = _ "select * from tblCustomer; select * from tblOrder" ' Указание информативных имен для таблиц. da.TableMappings.Add("Table", "Customers") da.TableMappings.Add("Table1", "Orders") ' Загрузка данных, da.Fill(ds) ' Создание отношения. ds.Relations.Add("Customer_Orders", _ ds.Tables("Customers").Columns("ID"), _ ds.Tables("Orders").Columns("CustomerID")) ' Отображение данных. grdCustomersOrders.DataSource = ds End Sub Private Sub btnUpdate_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnUpdate.Click ' Создание адаптеров данных. Dim daCustomers As New SqlDataAdapter( _ "select * from tblCustomer", en) Dim daOrders As New SqlDataAdapter( _ "select * from tblOrder", en) Dim cbCustomers As New SqlCommandBuilder(daCustomers) Dim cbOrders As New SqlCommandBuilder(daOrders) Try ' Внесение изменений в таблицы в "правильном" ' порядке (см. далее в тексте). Dim ChangedTable As New DataTable() ' Удаление записей в дочерней таблице. ChangedTable = _ ds.Tables("Orders").GetChanges(DataRowState.Deleted) If Not ChangedTable Is Nothing Then daOrders.Update(ChangedTable) End If ' Все измененные записи в родительской таблице. ChangedTable = ds.Tables("Customers").GetChanges If Not ChangedTable Is Nothing Then daCustomers.Update(ChangedTable) End If ' Новые или измененные записи в дочерней таблице. ChangedTable = _ ds.Tables("Orders").GetChanges(DataRowState.Added _ Or DataRowState.Modified) If Not ChangedTable Is Nothing Then daOrders.Update(ChangedTable) End If Catch ex As Exception MessageBox.Show(ex.Message) End Try End Sub Первая подпрограмма btnFill_Click считывает обе таблицы из базы данных посредством одного обращения к базе данных благодаря выполнению пакета команд SQL. В объекте CommandText отдельные команды пакета отделяются точкой с запятой. Обратите внимание, что предлагаемые по умолчанию имена таблиц Table и Table1 в приведенных ниже строках кода отображаются на более информативные имена Customers и Orders. ' Указание информативных имен для таблиц. da.TableMappings.Add("Table", "Customers") da. TableMappings.Add("Table1", "Orders") НА ЗАМЕТКУ После вставки данных в набор данных ds между таблицами Customers и Orders создается отношение DataRelation, где Customers является родительской таблицей, a Orders — дочерней. Последняя строка кода в этой подпрограмме связывает набор данных DataSet с сеткой для отображения данных. Вторая подпрограмма, btnUpdate_Click, вносит в базу данных изменения данных в объектах-таблицах с учетом родительско-дочерних связей между ними. К сожалению, ссылочная целостность данных не поддерживается автоматически, а потому ее нужно организовать вручную. Для этого разработчику необходимо сгруппировать типы изменений, а затем выполнить их в правильном порядке. Для двух таблиц, между которыми существуют родительско-дочерние связи, изменения следует вносить в приведенном ниже порядке. 1. Сначала удалить записи в дочерней таблице. 2. Вставить, обновить и удалить записи в родительской таблице. 3. Вставить и обновить записи в дочерней таблице. Для получения соответствующих изменений подпрограмма должна вызвать для данной таблицы метод GetChanges с фильтром состояния записи. Каждый вызов метода GetChanges возвращает объект DataTable только с измененными записями и заданным состоянием. Если таких записей нет, то возвращается значение Nothing. Если есть хотя бы одна измененная строка с заданным состоянием, то для фактического обновления базы данных вызывается метод Update объекта DataAdapter. Код этой подпрограммы окружен блоком операторов Try-Catch для обработки исключительных ситуаций, которые могут возникнуть в процессе обновления базы данных. Скомпонуйте проект BusinessCase6 и проверьте полученное приложение, выполнив перечисленные ниже действия. 1. Запустите полученное приложение BusinessCase6 и щелкните на кнопке Fill. Это приведет к вставке данных в объект DataSet из базы данных Novelty. Однако строка кода grdCustomersOrders.DataSource = ds связывает с сеткой весь объект DataSet, а не какую-то одну таблицу DataTable. Поэтому сетка содержит раскрывающийся список таблиц возле кнопки с изображением знака "плюс", как показано на рис. 6.5. РИС. 6.5. Исходный вид формы frmCustomersOrders после вставки данных в объект DataSet 2. Щелкните на пиктограмме с изображением знака "плюс", раскроется список ссылок на две таблицы объекта DataSet. 3. Щелкните на ссылке Customers, и в сетке будут отображены данные из таблицы tblCustomers. Обратите внимание, что каждая строка в таблице tblCustomers имеет кнопки с изображением знака "плюс" с левой стороны, что означает связь этой таблицы с другими таблицами. После щелчка на такой кнопке раскрывается список объектов DataRelations для данной таблицы. В нашем примере имеется только одна ссылка для отношения Customer_Orders, созданного в подпрограмме btnFillClick (рис. 6.6). РИС. 6.6. Ссылка Customer_Orders для первой записи из таблицы Customers 4. Щелкните на ссылке Customer_Orders в первой записи. На основании определения отношения Customer_Orders будут вставлены и отображены записи из таблицы Orders, которые относятся к текущей записи из таблицы Customers. НА ЗАМЕТКУ Теперь с помощью этой формы пользователи могут просматривать имеющиеся и вводить новые данные. При вводе новой дочерней записи значение 1 в поле указывается автоматически, потому что сетка способна определить его в связанной записи из родительской таблицы. Продолжим и добавим значения для полей OrderDate и Amount. При этом не нужно задавать значение для поля ID, потому что это идентификационное поле, которому значение присваивается автоматически. 5. Щелкните на кнопке Update для выполнения подпрограммы btnUpdate_Click из листинга 6.7, которая вносит указанные изменения данных в базу данных. 6. Чтобы проверить корректность внесенных изменений, щелкните на кнопке Fill для повторной загрузки информации из базы данных в объект DataSet и сетку. Откройте первую запись таблицы Customers и найдите ее дочерние записи. Убедитесь в том, что среди них находится введенная вами дочерняя запись. Попробуйте внести дополнительные изменения в базу данных вставляя, удаляя и изменяя записи в обеих таблицах и проверяя выполнение обновлений. НА ЗАМЕТКУ РезюмеВ этой главе рассмотрен объект DataAdapter, который является одним из основных объектов модели ADO.NET. Он играет роль моста между неподключенными объектами DataSet (и связанными с ними объектами) и подключенными объектами DataProvider, которые фактически связаны и подключены к физическому источнику данных. Объект DataAdapter используется для вставки данных в объект DataSet из источника данных с помощью явно заданных команд или хранимых процедур. Объект DataAdapter также автоматически обновляет источник данных теми изменениями, которые произошли в объекте DataSet, что позволяет полностью настроить команды вставки, обновления и удаления для источника данных. Вопросы и ответыИногда требуется программно создать и вставить в набор несколько записей, а затем внести эти обновления в базу данных. В модели ADO 2.X можно создать набор записей, но нельзя обновить базу данных. А как обстоит дело в модели ADO.NET? В модели ADO.NET это требование реализуется очень просто. Как отмечалось в главе 5, "ADO.NET: объект DataSet", объект DataSet является контейнером данных, который не зависит от фактически используемого источника данных. Для обновления базы данных изменениями из объекта DataSet достаточно только подключиться к уже сконфигурированному объекту DataAdapter и вызвать его метод Update, который фактически обновит базу данных. Это верно и для программно созданных объектов DataSet, а не только для созданных с помощью команды Select объектов DataAdapter. В момент готовности к внесению изменений в физический источник данных достаточно подключиться к сконфигурированному объекту DataAdapter и вызвать его метод Update. |
|
||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||
|