Теперь нужно подключиться к этому процессу для отладки. Меню Debug->Attach to Process… покажет диалог подключения к процессу. Выберите нужный процесс, и смело жмите Attach.
Все. Теперь вы можете ставить точки останова в исходных текстах классов, загруженных в сервер. Кроме того, по умолчанию отладчик будет перехватывать все исключения .NET.
Не забудьте сделать Debug->Detach All перед тем, как перекомпилировать проект. Отладчик Visual Studio блокирует файлы с отладочной информацией, что мешает компилятору произвести Build.
В новой версии MS SQL Server синтаксис оператора CREATE PROCEDURE был расширен. Вот фрагмент из SQL Server Books Online:
CREATE PROC [ EDURE ] [schema_name.] procedure_name [ ; number ] [ { @parameter data_type } [ VARYING ] [ = default ] [ [ OUT [ PUT ] ] [ ,...n ] [ WITH < procedure_option > [ ,...n ] [ FOR REPLICATION ] AS { < sql_statement > [ ...n ] | <.NET_Framework_reference> } -- <.NET_Framework_reference> ::= EXTERNAL NAME assembly_name:class_name[::method_name] |
Как видно из этого фрагмента, теперь вместо указания тела процедуры на T-SQL можно указать метод класса из загруженной ранее сборки. К этому методу предъявляются следующие требования:
Это должен быть статический метод (не конструктор и не деструктор класса)
Число параметров должно совпадать с числом параметров в описании хранимой процедуры, а их типы должны быть совместимы с типами данных соответствующих параметров. Если параметр процедуры объявлен как OUTPUT, то соответствующий параметр метода должен передаваться по ссылке.
Метод должен либо не иметь возвращаемого значения, либо возвращать значение одного из следующих типов: SQLInt32, SQLInt16, System.Int32, System.Int16
Для успешного создания такой хранимой процедуры необходимо быть владельцем соответствующей сборки или иметь для нее права REFERENCES.
Давайте перейдем от слов к делу и попробуем создать хранимую процедуру.
Минимальный код хранимой процедуры на C# выглядит вот таким образом:
using System; using System.Data; using System.Data.Sql; using System.Data.SqlServer; using System.Data.SqlTypes; public class StoredProcedure { [SqlProcedure] public static void MyProcedure() { } }; |
Очевидно, он не очень функционален. Тем не менее, метод StoredProcedure.MyProcedure уже можно зарегистрировать в базе данных качестве хранимой процедуры, вызвать (например, из Query Analyzer), и убедиться, что он успешно выполняется (то есть ничего не делает).
Обратите внимание на атрибут SqlProcedure (System.Data.Sql.SqlProcedureAttribute). Этот атрибут не несет никакой информации для MS SQL Server. Он используется MS Visual Studio Whidbey при развертывании проекта – для методов, помеченных таким атрибутом, автоматически будут вызваны соответствующие операторы CREATE PROCEDURE. По умолчанию будет предпринята попытка назначить хранимой процедуре такое же имя, как и у метода. Это поведение можно изменить, воспользовавшись единственным свойством атрибута – Name. Если заменить девятую строку примера выше на [SqlProcedure("MyProcName")], то хранимая процедура будет называться MyProcName.
Здравствуй, мир
Останавливаться на том, каким образом хранимая процедура обрабатывает данные, смысла нет – это обычный 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(). Это тоже табличная функция.
Все эти варианты подробно рассмотрены далее.