Лекция 13 Команды манипулирования данными часто вызываются с параметрами, при этом некоторые элементы команды задаются только в период выполнения. Рассмотрим приложение для учета товаров в книжном магазине. В нем предусмотрен поиск книг по названию, который можно реализовать запросом к БД на основе следующего оператора SQL: SELECT * FROM Books WHERE (Title LIKE <value>) Требуемое название вводится пользователем в период выполнения. Поскольку это значение заранее неизвестно, необходим механизм передачи введенного значения оператору SQL во время выполнения программы. Каждый параметр представляется экземпляром класса SqlParameter, OleDbParameter и так далее, в зависимости от типа провайдера. Параметры хранятся в свойстве-наборе Parameters объекта Command. В период выполнения значения параметров считываются из этого свойства и подставляются в оператор SQL либо передаются хранимой процедуре. Набор Parameters состоит из объектов Parameter соответствующего типа. Рассмотрим некоторые свойства класса Parameter. DbType (не отображается в дизайнере) OleDbType (только для объекта OleDbParameter) SQLType (только для SQLParameter) Direction ParameterName Precision Scale Size Value Свойства DbType и OleDbType объекта OleDbParameter взаимосвязаны. Первое представляет тип параметра в соответствии с общей системой типов (CTS) платформы .NET, а второе – как он представлен в БД. Это необходимо, 2 поскольку не все БД совместимы с CTS. Объект Parameter выполняет преобразование параметров из типа, используемого в приложении, в тип, используемый в БД. Поскольку эти свойства взаимосвязаны, при изменении значения одного из них значение другого автоматически меняется и преобразуется в соответствующий поддерживаемый тип. Аналогичо связаны свойства DbType и SqlType объектов SqlParameter: свойство SqlType указывает тип БД SQL, представленный параметром. Свойство Direction объекта параметра определяет, является ли этот параметр входным или выходным. Возможные значения этого свойства – Input, Output, InputOutput или ReturnValue. На элементы набора Parameters можно ссылаться по индексу либо по имени, заданному свойством ParameterName. Ниже показаны два способа установки значения первого по счету параметра с именем «myParameter». oleDbCommand.Parameters[0].Value = “Hello, World !”; oleDbCommand.Parameters[“myParameter”].Value = “Goodbye for now”; Свойства Precision, Scale и Size определяют длину и точность значения параметров. Precision и Scale применяются с числовыми десятичными параметрами. Они определяют разрядность и длину дробной части значения свойства Value соответственно. Size применяется с двоичными и строковыми параметрами и представляет максимальную длину такого поля. Свойство Value содержит значение параметра. Если свойство CommandType объекта Command установлено в Text, необходимо предусмотреть поля подстановки для всех параметров оператора SQL. В случае объекта OleDbCommand поле подстановки обозначается символом “?”, например: SELECT EmpId, Title, FirstName, LastName FROM Employees WHERE (FirstName = ?) AND (LastName = ?) Порядок заполнения полей определяется порядком элементов набора Parameters. 3 С объектом SqlCommand можно применять именованные параметры. Чтобы создать поле подстановки для именованного параметра, необходимо указать имя параметра (как оно задано свойством ParameterName), предварив его символом «@». Например: SELECT Empld, Title, FirstName, LastName FROM Employees WHERE (Title = @Title) Выполнить с помощью объекта Command команду, не возвращающую значений или возвращающую единственное значение, можно, например, так: object O = myCommand.ExecuteScalar (); 6.2.3. Динамическое формирование SQL-запросов Иногда необходимая конструкция запроса SQL становится известной только в период выполнения программы. Например, запрос может содержать введенную пользователем строку для поиска или возвращать столбцы и таблицы, определенные программно в период выполнения. Для решения таких задач команды создаются и настраиваются в период выполнения. Первый шаг в этом направлении – формирование строки команды. Можно предварительно подготовить заготовку команды и при необходимости в период выполнения заменять некоторые значения строковыми переменными. Для объединения строк можно использовать оператор конкатенации. Строковые значения, передаваемые базе данных в конструкции WHERE, необходимо заключать в апострофы. Если апостроф содержится внутри такого значения, следует его удвоить. У каждого провайдера данных есть конструктор класса Command, позволяющий устанавливать свойства CommandText и Connection при создании экземпляра Command. После установки этих свойств остается открыть соединение и выполнить команду. Например: public void DeleteRecord (string aString) { 4 string Cmd; Cmd = “DELETE FROM Employees WHERE Name = '” + aString + “'”; OleDbCommand myCommand = new OleDbCommand (Cmd, myConnection); myConnection.Ореn (); myCommand.ExecuteNonQuery (); myConnection.Close (); } 6.2.4. Применение объекта DataReader Для использования объекта Command с запросами, возвращающими несколько значений, можно применить метод ExecuteReader. Он возвращает объект DataReader, который обеспечивает быстрое и эффективное последовательное однонаправленное чтение данных. Объект DataReader ориентирован на использование постоянного соединения и, пока он существует, требует монопольного доступа к активному соединению. Объект DataReader нельзя создать напрямую, это делается путем вызова метода ExecuteReader объекта Command. Подобно другим членам классов провайдеров данных, у каждого класса DataProvider есть собственный класс DataReader. Объект OleDbCommand возвращает OleDbDataReader, а объект SqlCommand – SqlDataReader, например: System.Data.OleDb.OleDbDataReader myOleDbReader; System.Data.SqlClient.SqlDataReader mySqlReader; myOleDbReader = myOleDbCommand.ExecuteReader (); mySqlReader = mySqlCommand.ExecuteReader (); Получив ссылку на объект DataReader, можно просматривать записи, загружая нужные данные в память. У нового объекта DataReader указатель чтения устанавливается на первую запись результирующего набора. Чтобы сделать ее доступной, следует вызвать метод Read. Если запись доступна, 5 метод Read переводит указатель объекта DataReader к следующей записи и возвращает true, в противном случае – false. При чтении записи с помощью объекта DataReader значения отдельных полей доступны через индексатор по индексу либо по имени поля, например: while ( myDataReader.Read () ) { object myObject = myDataReader[3]; object myOtherObject = myDataReader[“CustomerID”]; } При таком способе доступа DataReader предоставляет все значения в виде объектов. Однако этот класс имеет собственные методы для извлечения типизированных данных. Имена этих методов образуются из префикса Get и имени типа извлекаемых данных. Например, метод извлечения значения типа Boolean называется GetBoolean: bool myBool = myDataReader.GetBoolean (3); При использовании такого способа извлечения данных необходимо указывать порядковый номер, а не имя поля. Если известно только имя поля, можно определить его порядковый номер методом GetOrdinal, например: int iCustomerID = myDataReader.GetOrdinal (“CustomerID”); string Customer = myDataReader.GetString (iCustomerID); Прочитав данные с помощью DataReader, необходимо вызвать метод Close, иначе объект DataReader будет удерживать монопольный доступ к активному соединению, сделав его недоступным другим объектам: myDataReader.Close (); Если перед вызовом метода ExecuteReader установить свойство CommandBehavior в СloseConnection, то по окончании чтения метод Close явно вызывать не требуется. Следующий пример демонстрирует перебор записей результирующего набора для вывода содержимого одного из столбцов в окне Console. Этот пример предполагает наличие объекта OleDbCommand с именем 6 “myOleDbCommand”, свойство Connection которого использует соединение с именем “myConnection”. myConnection.Ореn (); System.Data.OleDb.OleDbDataReader myReader = myOleDbCommand.ExecuteReader (); while ( myReader.Read () ) { Console.WriteLine ( myReader[“Customers”].ToString () ); } myReader.Close (); myConnection.Close (); Если свойство CommandType объекта Соmmand установлено в Text, то можно одной командой получить несколько результирующих наборов. Для этого нужно в свойство CommandText поместить несколько SQL-операторов, разделенных точкой с запятой, например: SELECT * FROM Accounts; SELECT * FROM Creditors Операторы выполняются последовательно. В этом случае объект DataReader вернет несколько наборов. Первый результирующий набор DataReader возвращает автоматически. Чтобы получить доступ к другим результирующим наборам, необходимо вызывать метод NextResult. Он возвращает false при попытке обращения к несуществующему результирующему набору, при этом устанавливает указатель объекта DataReader на первый результирующий набор. Рассмотрим пример. do { while ( myReader.Read () ) { …………………………….. } } while ( myReader.NextResult () ); 7 6.2.5. Создание и использование объекта DataAdapter Объекты класса DataAdapter обеспечивают связь между источником данных и объектом DataSet, управляя обменом данных и контролируя их передачу. Объект DataAdapter способен извлекать данные, заполнять объекты DataSet и обновлять содержимое источника данных. В Visual Studio .NET доступны несколько видов DataAdapter, включая SqlDataAdapter, оптимизированный для взаимодействия с БД MS SQL Server, и OleDbDataAdapter, предоставляющий доступ к любому источнику данных, поддерживаемому OleDbProvider. Как правило, обменом данными между объектом DataTable и таблицей БД управляет отдельный объект DataAdapter. В DataSet может быть несколько таблиц, поэтому для каждой из них необходимо создавать собственный объект DataAdapter. Проще всего создать DataAdapter в окне Server Explorer. Узел Data Connections содержит все доступные соединения с данными. Раскрыв узел, можно получить дополнительные сведения о подключенной БД, в том числе список имеющихся таблиц, просмотров и хранимых процедур. Для создания объекта DataAdapter, представляющего таблицу, можно перетащить таблицу из окна Server Explorer в окно дизайнера. Таким образом будет создан объект DataAdapter соответствующего типа (SqlDataAdapter, OleDbDataAdapter, … ) с корректными значениями свойств SelectCommand, UpdateCommand, InsertCommand и DeleteCommand. Кроме того, предусмотрена настройка объекта DataAdapter для работы с подмножеством столбцов таблицы. Для этого достаточно раскрыть узел таблицы и выбрать нужные столбцы, удерживая нажатой клавишу Ctrl. Затем перетащить выбранные столбцы в окно дизайнера – получится объект DataAdapter, сконфигурированный для доступа к выбранным столбцам. 8 Объект DataAdapter можно также создать, перетащив соответствующий объект DataAdapter с панели Toolbox в окно дизайнера, после чего автоматически будет вызван мастер Data Adapter Configuration. Объект DataAdapter инкапсулирует функциональность, необходимую для заполнения DataSet данными и обновления БД. Чтобы заполнить DataSet данными, следует вызвать метод Fill объекта DataAdapter. Этот метод выполняет команды, заданные свойством SelectCommand, на соединении, указанном свойством Connection. Методу Fill необходимо передать целевой объект, которым может быть DataSet либо DataTable, например: DataSet myDataSet = new DataSet (); myDataAdapter.Fill (myDataSet); При выполнении команды, вызванной методом Fill, соединение с БД открывается только на время извлечения данных, после чего сразу же закрывается. Таким образом, после извлечения данные становятся отсоединенными и ими можно манипулировать в коде независимо от БД, а при необходимости ее можно обновлять. Обычно для каждой таблицы данных создается отдельный объект DataAdapter. Если нужно загрузить содержимое нескольких таблиц в один объект DataSet, следует задействовать несколько объектов DataAdapter. Один и тот же объект DataSet может быть передан методу Fill каждого объекта DataAdapter, при этом всякий раз создается новый объект DataTable, заполняется данными и добавляется к объекту DataSet. 6.2.6. Типизированные объекты DataSet Стандартные объекты DataSet не являются строго типизированными. Каждый элемент данных доступен в форме объекта. ADO .NET поддерживает также типизированные объекты DataSet. На таблицы и поля такого объекта DataSet можно ссылаться по именам, соответствующим реальным именам таблиц и столбцов; их значения доступны в виде значений 9 соответствующих типов, а не объектов. Это дает ряд преимуществ. Вопервых, код программы становится более понятным и его удобнее сопровождать. Во-вторых, ошибки из-за несоответствия типов обнаруживаются в период компиляции, а не в период выполнения – это экономит время, необходимое для тестирования. Наконец, полные имена членов наборов можно заменять их дружественными именами, при этом в период разработки имена типизированных членов данных отображаются в окнах среды разработки. Ниже приводятся эквивалентные примеры, использующие соответственно нетипизированный и типизированный объекты DataSet. string myOrder = (string) dsOrders.Tables[“Orders”].Rows[0][“OrderID”]; string myOrder = dsOrders.Orders[0].OrderID; Типизированный DataSet – это экземпляр другого класса, производного от DataSet. Структура этого класса определяется файлом схемы XML (XSDфайлом), в котором описаны особенности объекта DataSet, включая имена таблиц и столбцов. Для создания типизированного объекта DataSet требуется файл схемы. Его можно создать только для заранее известной структуры данных. Типизированный класс DataSet можно сгенерировать автоматически, выбрав в VS в меню Data команду Generate DataSet. Резюме Объект Connection реализует подключение к БД. Объект Command представляет команду SQL или ссылку на хранимую процедуру. Объекты OleDbCommand и SqlCommand содержат три метода для выполнения команд: ExecuteNonQuery, ExecuteScalar и ExecuteReader. Параметры – это значения, необходимые для выполнения команд, представленных объектами Command. При использовании объекта OleDbCommand поля для подстановки значений параметров в операторах SQL обозначаются знаком вопроса. Объект SqlCommand допускает применение только именованных параметров. 10 Объекты DataReader предоставляют доступ к подсоединенным данным. Данные, полученные через объект DataReader, доступны только для последовательного однонаправленного чтения. DataReader требует монопольного доступа к соединению с источником данных. Объекты DataReader имеют методы для извлечения строго типизированных данных. Объекты DataAdapter осуществляют взаимодействие между БД и объектом DataSet, управляя выполнением команд для заполнения DataSet данными из БД и обновления БД содержимым объекта DataSet. Типизированные объекты DataSet – это экземпляры классов, производных от DataSet. Эти объекты основаны на схеме XML. На таблицы и поля таких объектов DataSet можно ссылаться по их дружественным именам.