Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции OOP c#.doc
Скачиваний:
46
Добавлен:
22.09.2019
Размер:
3.38 Mб
Скачать

4.5. Чтение данных и объект DataReader

Чтение данных из базы – одна из наиболее часто выполняемых операций. В том случае, когда необходимо прочитать набор данных, используется метод ExecuteReader(), возвращающий объект DataReader (далее для краткости – просто «ридер»). Каждый поставщик имеет собственный класс для ридера, однако любой такой класс реализует интерфейс IDataReader.

Использование ридеров имеет следующие особенности. Во-первых, ридеры не создаются при помощи вызова конструктора. Единственный способ создать ридер – это выполнить метод ExecuteReader(). Во-вторых, ридеры позволяют перемещаться по данным набора строго последовательно и в одном направлении – от начала к концу. Большинство СУБД выполняют подобную «навигацию» максимально быстро. В-третьих, данные, полученные при помощи ридера, доступны только для чтения. И, наконец, на время чтения данных соответствующее соединение с базой блокируется, то есть соединение не может быть использовано другими командами, пока чтение данных не завершено.

Рассмотрим работу с ридерами на примере класса SqlDataReader. Основным методом ридера является метод Read(), который перемещает указатель на следующую запись в наборе данных и возвращает false, если записей в наборе больше нет. После прочтения всех записей у ридера необходимо вызвать метод Close(). Этот метод освобождает соединение, которое было занято ридером.

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

// Стандартные подготовительные действия

SqlConnection con = new SqlConnection();

con.ConnectionString = . . .

SqlCommand cmd = new SqlCommand("SELECT * FROM Artists", con);

// Открываем соединение и получаем ридер, выполняя команду

con.Open();

SqlDataReader r = cmd.ExecuteReader();

// В цикле читаем данные (пока кода в цикле нет!)

while(r.Read()) { . . . }

// Закрываем ридер; если необходимо, закрываем соединение

r.Close();

con.Close();

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

// Изменяем фрагмент кода из предыдущего примера

while(r.Read()) {

int id = (int) r["id"];

Console.WriteLine(id);

}

Поиск столбца по имени регистронезависим1. Если соответствующего столбца в наборе данных нет, то генерируется исключение IndexOutOfRangeException.

Выполнение поиска столбца по имени требует сравнения строк и происходит медленно. Альтернативой является использование в качестве индекса номера столбца. В этом случае производительность повышается:

while(r.Read()) {

int id = (int) r[0];

Console.WriteLine(id);

}

Теоретически, использование числовых индексов вместо имен столбцов означает снижение гибкости приложения. Однако порядок столбцов в наборе данных меняется, только если меняется строка запроса или структура объекта БД (таблицы, хранимой процедуры). В большинстве приложений можно без каких-либо проблем жестко задать порядковые номера всех столбцов. Тем не менее, в некоторых ситуациях известно только имя столбца, но не его порядковый номер. Метод ридера GetOrdinal() принимает строку, представляющую имя столбца, и возвращает целое значение, соответствующее порядковому номеру столбца2. Это позволяет достичь компромисса между гибкостью и производительностью:

// Получаем ридер

SqlDataReader r = cmd.ExecuteReader();

// Один раз находим номер столбца по имени (а не в цикле!)

int id_index = r.GetOrdinal("id");

int name_index = r.GetOrdinal("name");

// В цикле доступ будет быстрее

while(r.Read()) {

Console.WriteLine(r[id_index] + "\t" + r[name_index]);

}

Класс SqlDataReader (как и класс OleDBDataReader) имеет ряд методов вида Get<тип данных> (например, GetInt32()). Эти методы получают в качестве параметра индекс столбца, а возвращают значение в столбце, приведенное к соответствующему типу:

SqlDataReader r = cmd.ExecuteReader();

int name_index = r.GetOrdinal("name");

while(r.Read()) {

Console.WriteLine(r.GetString(name_index));

}

Отдельные поля записей набора данных могут иметь пустые (null) значения, то есть быть незаполненными. При попытке извлечь значения из null-поля (а точнее, при попытке преобразования null-поля в требуемый тип) будет сгенерировано исключение. Ридер имеет метод IsDBNull(), предназначенный для индикации пустых полей:

if (!r.IsDBNull(id_index))

Console.WriteLine(r.GetInt32(id_index);

Некоторые СУБД и соответствующие поставщики данных позволяют выполнить запрос к базе, возвращающий несколько наборов данных. Ридер такого поставщика реализует метод NextResult(), который выполняет переход к следующему набору или возвращает false, если такого набора нет. Рассмотрим пример для SqlDataReader (обратите внимание на место вызова метода NextResult()):

// Отсутствует код создания соединения и команды

// Запрос возвращает два набора данных

cmd.CommandText = "SELECT * FROM Disks;" +

"SELECT * FROM Artists";

con.Open();

SqlDataReader r = cmd.ExecuteReader();

// Вложенные циклы, внешний – по наборам данных

do {

while(r.Read())

Console.WriteLine(r[1]);

} while(r.NextResult());

r.Close();

con.Close();

Упомянем некоторые свойства и методы класса SqlDataReader. Свойство FieldCount возвращает целое число, соответствующее числу столбцов в наборе результатов. Свойство IsClosed возвращает логическое значение, указывающее, закрыт ли ридер. Свойство RecordsAffected позволяет определить число записей, измененных запросом1.

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

SqlDataReader r = cmd.ExecuteReader();

object[] data = new object[r.FieldCount];

while(r.Read()) {

// на самом деле GetValues() – функция,

// которая возвращает количество полей прочитанной записи

r.GetValues(data);

Console.WriteLine(data[1].ToString());

}

Для исследования структуры возвращаемого набора данных можно применить метод GetSchemaTable(). Этот метод создает объект DataTable, строки которого описывают столбцы полученного набора данных. Колонки таблицы соответствуют атрибутам этих столбцов1. Следующий пример кода выводит для каждого столбца его имя и тип:

SqlDataReader r = cmd.ExecuteReader();

DataTable tbl = r.GetSchemaTable();

foreach (DataRow row in tbl.Rows)

Console.WriteLine(row["ColumnName"] + " - " +

((SqlDbType)row["ProviderType"]).ToString());

Для таблицы Artists код выводит на консоль следующее:

id - Int

name - VarChar

Вернемся к методу ExecuteReader(). Этот метод перегружен и может принимать значения из перечисления CommandBehavior , которые перечислены в таблице 25 (допустимо использовать побитовую комбинацию значений).

Таблица 25

Значения перечисления CommandBehavior

Имя

Значение

Описание

CloseConnection

32

При закрытии ридера закрывается и соединение

KeyInfo

4

Ридер получает сведения первичного ключа для столбцов, входящих в набор результатов

SchemaOnly

2

Ридер содержит только информацию о столбцах, запрос фактически не выполняется

SequentialAccess

16

Значения столбцов доступны только в последовательном порядке

SingleResult

1

Ридер содержит результаты только первого запроса, возвращающего записи

SingleRow

8

Ридер содержит только первую запись, возвращенную запросом

Если при вызове метода ExecuteReader() передать ему константу CloseConnection, то при вызове метода Close() ридера последний вызовет метод Close() связанного с ним объекта Connection.

При использовании в качестве параметра метода ExecuteReader() константы SequentialAccess строки считываются последовательно на уровне столбцов. Например, просмотрев содержимое третьего столбца, просмотреть содержимое первого и второго столбцов уже нельзя. Данное поведение оправдано при наличии в строках длинных двоичных данных. Если не применять константу SequentialAccess, то последние будут считаны в ридер вне зависимости от того, будут они использованы или нет. Таким образом, параметр SequentialAccess помогает более эффективно работать с запросами большого количества двоичных данных, когда реально работа ведется только с их частью.

Выше было описано, как с помощью метода ридера GetSchemaTable() можно получить метаданные о столбцах набора данных. Вызвав ExecuteReader() и указав в качестве параметра константу SchemaOnly, мы фактически получим информацию схемы о столбцах, не выполняя запроса. Если указать в параметре константу KeyInfo, ридер выберет из источника данных дополнительную информацию для схемы, чтобы показать, являются ли столбцы набора ключевыми столбцами таблиц источника данных. При использовании константы SchemaOnly дополнительно указывать константу KeyInfo не требуется.

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

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