Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции_CSharp_4.docx
Скачиваний:
37
Добавлен:
02.11.2018
Размер:
237.61 Кб
Скачать

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

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

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

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

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

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

var con = new SqlConnection { ConnectionString = " . . . " };

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

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

con.Open();

var r = cmd.ExecuteReader();

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

while (r.Read()) { }

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

r.Close();

con.Close();

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

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

while (r.Read())

{

Console.WriteLine((int)r["id"]);

}

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

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

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

while (r.Read())

{

Console.WriteLine((int)r[0]);

}

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

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

var 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 имеет ряд методов вида GetТипДанных() (например, GetInt32()). Эти методы получают в качестве параметра индекс столбца, а возвращают значение в столбце, приведенное к соответствующему типу:

var r = cmd.ExecuteReader();

var 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 Albums; SELECT * FROM Songs";

con.Open();

var r = cmd.ExecuteReader();

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

do

{

while (r.Read())

{

Console.WriteLine(r[0]);

}

}

while (r.NextResult());

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

var r = cmd.ExecuteReader();

var data = new object[r.FieldCount];

while (r.Read())

{

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

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

r.GetValues(data);

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

}

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

var reader = cmd.ExecuteReader();

DataTable table = reader.GetSchemaTable();

foreach (DataRow row in table.Rows)

{

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

(SqlDbType)row["ProviderType"]);

}

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

Таблица 5

Значения перечисления 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 аналогичным образом отбрасываются наборы результатов.