46378 (665526), страница 2
Текст из файла (страница 2)
Здравствуй, мир
Останавливаться на том, каким образом хранимая процедура обрабатывает данные, смысла нет – это обычный C#, и его особенности хорошо известны. Давайте научим ее общаться с внешним миром. Для начала доведем ее до уровня Кернигана и Ритчи:
using System; using System.Data; using System.Data.Sql; using System.Data.SqlServer; using System.Data.SqlTypes; public class StoredProcedure { [SqlProcedure("HelloWorld")] public static void MyProcedure() { SqlContext.GetPipe().Send("Hello, Yukon!"); } }; |
Эта процедура демонстрирует еще один важный компонент, связывающий .NET с MS SQL Server: класс System.Data.SqlServer.SqlContext. Этот класс содержит несколько статических методов, обеспечивающих доступ к контексту, в котором выполняется код. В данном случае мы получаем доступ к объекту класса System.Data.SqlServer.SqlPipe, который представляет серверную сторону соединения с клиентом. Именно в эту «трубу» SQL Server отправляет результаты выполнения запросов. Если хранимая процедура должна возвращать какие-то данные в клиентское приложение, то без SqlPipe не обойтись.
В этом примере мы используем метод SqlPipe.Send(String msg), предназначенный для отправки текстовых сообщений. Его функциональность аналогична команде print в T-SQL. Остальные методы SqlPipe предназначены для отправки табличных данных:
Метод или свойство | Описание |
public void Execute (System.Data.SqlServer.SqlCommand command )public void Execute (System.Data.SqlServer.SqlExecutionContext request ) | Выполняет указанную команду или запрос и возвращает результат клиенту. Аналог выполнения оператора SELECT … FROM … в хранимой процедуре на T-SQL. |
public void Send (System.Data.SqlServer.SqlError se) | Возвращает клиенту указанную ошибку. |
public void Send (System.Data.Sql.ISqlReader reader) | Отправляет клиенту все записи из указанного набора. |
public void SendResultsStart (System.Data.Sql.ISqlRecord record , bool sendRow) | Посылает клиенту первую запись в наборе записей. Устанавливает свойство SendingResults в true. |
public System.Boolean SendingResults { get; } | Указывает, что процесс отправки набора записей не окончен. |
public void SendResultsRow (System.Data.Sql.ISqlRecord record)public void Send (System.Data.Sql.ISqlRecord record ) | Посылает клиенту очередную запись в наборе. Требует SendingResults == true. |
public void SendResultsEnd ( ) | Сигнализирует об окончании набора записей и устанавливает свойство SendingResults в false. |
Таблица 2.
Таким образом, помимо передачи клиенту набора данных, полученного от сервера, можно формировать результаты вручную. С точки зрения клиента это будет выглядеть как обычный набор записей.
Возвращаем произвольные данные
Пока что документация весьма скупо освещает этот вопрос, но после нескольких экспериментов мне удалось создать вот такую процедуру:
[SqlProcedure()] public static void CurrencyCourse( [SqlMapping(typeof(SqlDateTime))] DateTime start, [SqlMapping(typeof(SqlDateTime))] DateTime end) { using (SqlCommand cmd = SqlContext.GetCommand()) { cmd.CommandText = @" select changeDate, course from Course where changeDate between @start and @end"; cmd.Parameters.AddWithValue("@start", start); cmd.Parameters.AddWithValue("@end", end); DateTime current = start; SqlDecimal course = SqlDecimal.Null; // сначала курс отсутствует; SqlMetaData[] recstruct = new SqlMetaData[2]; recstruct[0] = new SqlMetaData("D", SqlDbType.DateTime); recstruct[1] = new SqlMetaData("course", SqlDbType.Decimal, 10, 4); SqlDataRecord rec = new SqlDataRecord(recstruct); SqlPipe pipe = SqlContext.GetPipe(); pipe.SendResultsStart(rec, false); using (SqlDataReader r = cmd.ExecuteReader()) { while (r.Read()) { rec.SetSqlDecimal(1, course); while(current < r.GetDateTime(0)) { rec.SetDateTime(0, current); pipe.SendResultsRow(rec); current = current.AddDays(1); } course = r.GetDecimal(1); } } rec.SetSqlDecimal(1, course); while (current <= end) { rec.SetDateTime(0, current); pipe.SendResultsRow(rec); current = current.AddDays(1); } pipe.SendResultsEnd(); } } |
Эта процедура превращает данные в таблице изменения курсов некой валюты (Course) в таблицу ежедневных значений курса, повторяя предыдущее значение для тех дней, в которые изменений не происходило.
На этот раз у процедуры есть параметры. Чтобы помочь инструментам автоматического развертывания (например, той же MS VS Whidbey) определить SQL-типы параметров хранимой процедуры, для параметров метода можно указать атрибут SqlMapping (System.Data.Sql.SqlMappingAttribute). Его единственный параметр и задает тип для параметра процедуры. В данном случае этот атрибут является избыточным – параметры типа DateTime автоматически отображаются в тип SQL datetime (которому соответствует тип CLR System.Data.SqlTypes.SqlDateTime), но в более сложных случаях им придется пользоваться для устранения неоднозначности.
Чтобы выполнить запрос к данным сервера, мы воспользуемся еще одним статическим методом класса SqlContext – SqlContext.GetCommand().
Чтобы возвратить данные клиенту, нужен экземпляр класса, реализующего интерфейс System.Data.Sql.ISqlRecord. В данном случае использован System.Data.Sql.SqlDataRecord. Его конструктор требует указать желаемую структуру записи. Эта структура описывается массивом объектов класса System.Data.Sql.SqlMetaData. В каждом объекте задается имя и тип соответствующей колонки. Мы описываем структуру, соответствующую в терминах SQL вот такой «таблице»:
( D datetime, course decimal(10, 4) ) |
Создав запись, мы инициируем процесс отправки при помощи вызова:
pipe.SendResultsStart(rec, false); |
Второй параметр говорит о том, что саму запись отправлять клиенту не нужно; вместо этого метаданные записи используются для инициализации отправляемого набора записей.
Дальше все просто – мы читаем очередную запись из SqlDataReader, полученного в результате исполнения команды, заполняем поля в SqlDataRecord, и отправляем ее клиенту. Дополнительный цикл в конце досылает записи для дат между последним изменением и концом запрошенного интервала.
Отправив все, что хотелось, мы сигнализируем клиенту об окончании набора при помощи вызова
pipe.SendResultsEnd(); |
Стоит отметить, что результаты возвращаются напрямую клиенту, т.е. код, который вызвал процедуру, не имеет над этим процессом никакого контроля. Повторное использование такого кода в серверной части приложения маловероятно. В следующем разделе мы узнаем о том, как можно обойти это ограничение.
Функции
В рамках T-SQL функции делятся на два вида: скалярные и табличные.
ПРИМЕЧАНИЕ Есть еще агрегатные функции, но их реализация существенным образом отличается от «обычных», и поэтому мы рассмотрим их в следующем разделе. |
С точки зрения .NET, эти два типа функций устроены почти одинаково. Как и хранимые процедуры, они реализуются при помощи статических методов класса. Отличие заключается в том, как они возвращают значения. Есть три варианта:
Возвращаем значение произвольного типа. Это скалярная функция.
Возвращаем System.Data.Sql.ISqlReader. Структура данных в нем должна совпадать с декларированной структурой результата функции. Это табличная функция.
Возвращаем void. Внутри функции вручную формируем возвращаемые данные через SqlContext.GetReturnResultSet(). Это тоже табличная функция.
Все эти варианты подробно рассмотрены далее.
ПРИМЕЧАНИЕ В отличие от встроенных функций, обращаться к «самодельным» нужно с уважением – предваряя имя функции именем схемы (которое по умолчанию совпадает с именем ее владельца). Например, я вызывал функцию из следующего подраздела примерно вот так: select dbo.RevertString(“Beavis rulez”) |
Скалярные функции
Это самая простая разновидность функций. В качестве примера напишем свой вариант встроенной функции reverse:
[SqlFunc()] [SqlFunction( DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = true, IsPrecise = true)] public static SqlString RevertString(SqlString str) { if (str.IsNull) return SqlString.Null; System.Text.StringBuilder sb = new System.Text.StringBuilder(str.Value.Length); for (int i=str.Value.Length-1; i>=0; i--) sb.Append(str.Value[i]); return new SqlString(sb.ToString()); } |
Поскольку реализация самой функции примитивна, остановимся на том, что ее окружает.
Во-первых, к методу применен атрибут SqlFunc. Как и SqlProcedure, он позволяет указать средствам автоматического развертывания информацию, необходимую для правильного построения команды CREATE FUNCTION. В данном случае никаких параметров не использовано – атрибут просто указывает, что данный метод надо будет зарегистрировать как функцию. Более подробно мы рассмотрим возможности этого атрибута чуть позже.
А вот следующий атрибут – SQLFunction – уже используется «внутри» MS SQL Server для определения того, как можно эту функцию использовать. В таблице 3 приведено описание параметров этого атрибута:
Имя параметра | Описание |
DataAccess | Какой доступ осуществляет функция к пользовательским данным в базе:DataAccessKind.None – никакого.DataAccessKind.Read – читает данные. |
SystemDataAccess | Какой доступ осуществляет функция к системным данным в базе:SystemDataAccessKind.None – никакого.SystemDataAccessKind.Read – читает данные. |
IsDeterministic | Является ли функция детерминистической, т.е. зависит ли ее возвращаемое значение только от переданных параметров. |
IsPrecise | Выполняет ли функция округления в процессе работы. |
Таблица 3.
В нашем случае ни к каким данным доступа не происходит, возвращаемое значение зависит только от переданного параметра, и значение является точным, а не приближенным.
ПРИМЕЧАНИЕ Это позволяет использовать эту функцию в максимально широком контексте – например, можно создать вычисляемую колонку на ее основе, и даже индекс по этой колонке. Это может быть полезно для сортировки, например, списка получателей e-mail. Сортировка по обращенному адресу поставит рядом адреса в одном домене, и можно будет оптимизировать рассылку писем. |
Возвращаем ISqlReader
Во многих случаях табличная функция выполняет роль параметризованного view – данные берутся из таблиц, и, после применения операторов SQL к исходным данным и параметрам, результат возвращается в вызывающий код. Создадим функцию, которая будет возвращать список изменений курса валют, произшедших в заданном диапазоне дат:
[SqlFunc(TableDefinition = "D datetime, course decimal(10, 4)")] [SqlFunction(DataAccess = DataAccessKind.Read, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = false, IsPrecise = true)] public static ISqlReader GetCourseChanges(DateTime start, DateTime end) { SqlCommand cmd = SqlContext.GetCommand(); cmd.CommandText = @" select changeDate, course from Course where changeDate between @start and @end"; cmd.Parameters.AddWithValue("@start", start); cmd.Parameters.AddWithValue("@end", end); return cmd.ExecuteReader(); } |
ПРЕДУПРЕЖДЕНИЕ Увы, пока что мне не удалось заставить этот пример работать. Сервер неуклонно возвращает ошибку «Reader is closed». Каким образом избежать закрытия Reader после возвращения его серверу, я пока не понял. |
Работаем с SqlResultSet
Для тех случаев, когда необходимо сформировать возвращаемый набор данных вручную, предусмотрен доступ к нему через метод контекста SqlContext.GetReturnResultSet(). Объект, возвращаемый этим методом, уже проинициализирован в соответствии с декларированной структурой функции. В него нужно добавить требуемые записи. В принципе, можно как добавлять, так и удалять/изменять записи, если это кажется необходимым. Воспроизведем поведение хранимой процедуры CurrencyCourse, созданной в конце предыдущего раздела:
[SqlFunc(TableDefinition = "D datetime, course decimal(10, 4) NULL")] [SqlFunction(DataAccess = DataAccessKind.Read, SystemDataAccess = SystemDataAccessKind.None, IsDeterministic = false, IsPrecise = true)] public static void GetCourseTable(DateTime start, DateTime end) { using (SqlCommand cmd = SqlContext.GetCommand()) { cmd.CommandText = @" select changeDate, course from Course where changeDate between @start and @end"; cmd.Parameters.AddWithValue("@start", start); cmd.Parameters.AddWithValue("@end", end); DateTime current = start; SqlDecimal course = SqlDecimal.Null; SqlResultSet source = cmd.ExecuteResultSet(ResultSetOptions.None); SqlResultSet dest = SqlContext.GetReturnResultSet(); SqlDataRecord rec; while (source.Read()) { while (current < source.GetDateTime(0)) { rec = dest.CreateRecord(); rec.SetSqlDecimal(1, course); rec.SetDateTime(0, current); dest.Insert(rec); current = current.AddDays(1); } course = source.GetDecimal(1); } while (current <= end) { rec = dest.CreateRecord(); rec.SetDateTime(0, current); rec.SetSqlDecimal(1, course); dest.Insert(rec); current = current.AddDays(1); } } } |
Обратите внимание, что теперь в атрибуте SqlFunction содержится значение свойства DataAccess = DataAccessKind.Read, указывая на то, что функция читает данные из базы.
ПРЕДУПРЕЖДЕНИЕ Обратите внимание также на то, что на этот раз для доступа к данным мы используем SqlResultSet вместо SqlDataReader. Дело в том, что одновременно читать из базы и работать с возвращаемым набором записей нельзя – возникает исключение с сообщением о том, что данное соединение уже используется. Возможно, данная особенность поведения будет изменена при выпуске финальной версии. Но пока единственным способом написать подобную функцию является чтение данных целиком до начала формирования выходного набора данных. |
Агрегирующие функции