Учебная работа. Реферат: MS SQL Server 9 Yukon. Интеграция с .NET

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (5 оценок, среднее: 4,80 из 5)
Загрузка...
Контрольные рефераты

Учебная работа. Реферат: MS SQL Server 9 Yukon. Интеграция с .NET

MS SQL Server 9 “Yukon”. Интеграция с .NET

Антон Злыгостев (Sinclair)

ЮКОН

— река в Канаде и США

Со всей ответственностью заявляю – рекламные материалы, публикуемые по данному продукту, не отражают и малой толики новейших способностей. Из 1-го перечисления инноваций можно было бы создать журнальную статью, потому исследователи разделили обязанности меж собой. В данной статье рассматривается разработка интеграции .NET и MS SQL Server.

Общая информация

Предыстория

Современные коммерческие РСУБД не могут дозволить для себя ограничиться ролью пассивного хранилища данных, поддерживая лишь SQL. Нужна поддержка способностей процедурного программирования. До недавнешнего времени обычным решением данной для нас задачи было специфическое для производителя расширение обычного SQL для написания триггеров и хранимых процедур (PL/SQL, T-SQL). На тот не настолько уж редчайший вариант, когда этого расширения не хватало для ублажения потребностей разрабов, предлагались не наименее специфичные методы употреблять «наружный» по отношению к серверу код (пользовательские функции в Interbase, расширенные хранимые процедуры в MS SQL и т.д.).

Главным недочетом первого решения является откровенная узость SQL, хотя бы и расширенного. «Дотянуть» SQL до настоящего языка программирования общего предназначения – нереально. К тому же, он по необходимости является интерпретируемым языком, что ограничивает его быстродействие при выходе за границы табличных операций. к примеру, написать хранимую функцию для шифрования PGP полностью можно и на T-SQL (благо там не необходимо ничего, не считая математики). Но скорость ее работы будет, мягко говоря, недостаточной.

При использовании наружного кода возникает иная неувязка – почти всегда ему тяжело получить доступ к контексту РСУБД, использующей его. Как правило, все взаимодействие происходит через «замочную скважину» точки входа в DLL. Да, для вычисления хеша MD5 таковая разработка полностью подступает, но способности взаимодействия подобного кода с ядром СУБД очень ограничены.

несколько годов назад Oracle предложил употреблять Java в качестве языка программирования для собственной РСУБД. Эта практика не прошла незамеченной в Редмонде, и вот сейчас Microsoft готовит ответный удар.

Последующие объекты MS SQL Server могут быть сделаны с внедрением .NET:

Хранимые процедуры.

Триггеры.

Функции (скалярные и табличные).

Агрегирующие функции.

Пользовательские типы данных.

Для написания кода этих объектов будет нужно .NET Framework версии 1.2 либо выше.

загрузка кода в базу данных

В отличие от расширенных хранимых процедур, код которых находится во наружных динамических библиотеках, код .NET хранится снутри соответственной базы данных. Это обеспечивает доп удобство при администрировании – восстановление базы из запасной копии либо перенос на иной (detach/attach) не нарушит целостности приложения.

Есть два главных метода выполнить загрузку сборки в базу данных:

вручную, с помощью операторов T-SQL и хоть какого клиентского приложения (пойдет даже старенькый Query Analyzer. Он хоть и не так прекрасен в строю, как новейший MS SQL Server Workbench, зато у него нет привычки падать и отжиматься в самые неподходящие моменты).

С помощью средств автоматического развертывания, интегрированных в MS Visual Studio .NET codename Whidbey.

Как сборка загружена в базу данных, ее код можно употреблять для сотворения разных объектов. Индивидуальности этих 2-ух методов описаны в последующих 2-ух подразделах.

загрузка кода с помощью T-SQL

Загрузка кода делается с помощью оператора CREATE ASSEMBLY:

CREATE ASSEMBLY assembly_name

[ AUTHORIZATION owner_name ]

FROM < assembly_bits > [,…n]

[ WITH PERMISSION_SET = EXTERNAL_ACCESS ]

< client_assembly_specifier > :: =

‘[machine_name]share_name[Path]manifest_file_name’

< assembly_bits > :: =

varbinary_expression




Код будет загружен в текущую базу данных соединения. Чтоб поменять базу, за ранее сделайте команду USE database_name.

имя параметра


Описание



assembly_name


Имя сборки .NET (assembly). Оно обязано быть неповторимым в границах текущей базы данных, удовлетворять требованиям MS SQL Server, предъявляемых к идентификаторам, и при всем этом совпадать с именованием файла сборки.



AUTHORIZATION owner_name


Показывает имя роли, которая будет считаться обладателем данной сборки. Можно указывать лишь те роли, в которые заходит текущий юзер, либо те, на которые у него есть права IMPERSONATE. По дефлоту обладателем станет сам текущий юзер.



<client_assembly_specifier>


Строковое выражение, которое показывает локальный либо сетевой путь к файлу сборки. Можно загружать лишь однофайловые сборки. Все сборки, от которых зависит обозначенная, также будут автоматом загружены в базу данных. Если они уже находятся в базе, то у выполняющего операцию юзера должны быть права REFERENCES для их. Если сборки не найдены ни в базе, ни в том же каталоге, где и основная, либо на их нет соответстующих прав, оператор не будет выполнен.



assembly_bits


Бинарное разработка дозволяет миновать фазу записи на диск при динамической генерации кода и избежать заморочек с доступом к файловой системе. Конкретно таковым образом происходит развертывание сборок из-под MS Visual Studio 8.0 (Whidbey).



PERMISSION_SET UNSAFE


Уровень прав, которые MS SQL Server предоставит коду сборки при его выполнении. По дефлоту употребляется SAFE.SAFE – самый ограниченный уровень. Код в таковой сборке не может получить доступ к наружному миру (файловой системе, сети, переменным окружения либо реестру).EXTERNAL_ACCESS дозволяет выходить за границы MS SQL Server, но с определенными ограничениями. Полный перечень ограничений на данный момент недоступен, но по последней мере выполнение не-менеджед кода запрещено.UNSAFE предоставляет неограниченный доступ к ресурсам, как наружным, так и внутренним. Такие сборки могут загрузить в базу данных лишь члены группы sysadmin.Почти всегда обязано хватать уровня SAFE. При всем этом есть гарантия, что злостный разраб не предоставит бинарный код, делающий какие-либо мерзости от имени сервиса SQL Server либо текущего юзера. Все примеры в данной для нас статье работают с уровнем SAFE.




Таблица 1. характеристики оператора CREATE ASSEMBLY.

загрузка с помощью Visual Studio

Загружать код вручную не очень комфортно. В особенности это сказывается во время разработки приложения, когда отлаживаемый код то и дело нужно исправлять. Новенькая версия Visual Studio дозволяет значительно облегчить этот процесс. Довольно сделать в студии проект типа SQL Server Project (не путать с Database Project!), и в меню Build покажутся команды Deploy, которые (кроме компиляции начального кода) сделают всю нужную работу по размещению ваших сборок в базе данных. При открытии проекта Visual Studio попросит указать и базу данных, в которую будет производиться развертывание.

естественно, «снутри» Visual Studio употребляет те же самые команды T-SQL. Как правило, характеристики этих установок определяются автоматом. Для управления действием развертывания употребляются (естественно же!) атрибуты. Классы этих атрибутов реализованы в сборке Microsoft.VisualStudio.DataTools.SqlAttributes.

СОВЕТ

Если у вас возникнет желание создать свои средства автоматического развертывания проектов SQL Server, обратите внимание на эти атрибуты. Поддержка их поможет употреблять код, написанный иными разрабами, без доп усилий




На уровне сборки для управления действием развертывания определен один класс атрибута System.Data.Sql.SqlAssemblyAttribute. При разработке проекта SQL Server Project в файл AssemblyInfo.cs автоматом будет добавлен этот атрибут. Его внедрение смотрится вот так:

[assembly: SqlAssembly(<name>, Authorization = <authorization>)]




Строковый параметр name соответствует параметру assembly_name оператора CREATE ASSEMBLY, параметр Authorization – параметру owner_name (см. предшествующий раздел).

Для того, чтоб Visual Studio могла корректно зарегистрировать ваши типы, процедуры, функции либо триггеры, их код тоже нужно будет снабдить надлежащими атрибутами. Подробности приведены дальше.

Отладка кода

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

Но если вы, как и я, временами испытываете взрыв чувств при виде сообщения о ошибке, которое обрезано ровно перед тем местом, где должен быть номер строчки и название файла, то вас непременно обрадует тот факт, что код .NET, лежащий в базе данных, можно отлаживать с удобством и уютом. Лично я воспользовался для отладки все той же Visual Studio Whidbey, и смотрелось это приблизительно так:

До этого всего, необходимо выполнить развертывание проекта (меню Build->Deploy). Настоятельно рекомендую выбирать отладочную конфигурацию проекта.

сейчас выясните идентификатор процесса (PID) MS SQL Server. процесс именуется “sqlservr.exe”. Те, у кого запущен лишь один экземпляр SQL Server, могут сходу перебегать к пт 3. У меня Yukon стоит с MSDE, потому таковых действий нашлось два. Чтоб избежать неоднозначности, можно просто приостановить излишние серверы, а можно подключиться к подходящему и выполнить команду SELECT ServerProperty(‘ProcessID’)

сейчас необходимо подключиться к этому процессу для отладки. Меню 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 <.NET_Framework_reference>

<.NET_Framework_reference> ::=

EXTERNAL NAME assembly_name:class_name[::method_name]




Как видно из этого фрагмента, сейчас заместо указания тела процедуры на T-SQL можно указать способ класса из загруженной ранее сборки. К этому способу предъявляются последующие требования:

Это должен быть статический способ (не конструктор и не деструктор класса)

Число характеристик обязано совпадать с числом характеристик в описании хранимой процедуры, а их типы должны быть совместимы с типами данных соответственных характеристик. Если параметр процедуры объявлен как OUTPUT, то соответственный параметр способа должен передаваться по ссылке.

Способ должен или не иметь возвращаемого значения, или возвращать значение 1-го из последующих типов: 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);




2-ой параметр гласит о том, что саму запись отправлять клиенту не необходимо; заместо этого метаданные записи употребляются для инициализации отправляемого набора записей.

Далее все просто – мы читаем еще одну запись из 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());

}




Так как реализация самой функции примитивна, остановимся на том, что ее окружает.

Во-1-х, к способу использован атрибут 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(). объект, возвращаемый сиим способом, уже проинициализирован в согласовании с декларированной структурой функции. В него необходимо добавить требуемые записи. В принципе, можно как добавлять, так и удалять/изменять записи, если это кажется нужным. Воспроизведем внимание, что сейчас в атрибуте SqlFunction содержится

ПРЕДУПРЕЖДЕНИЕ

Обратите внимание также на то, что сейчас для доступа к данным мы используем SqlResultSet заместо SqlDataReader. Дело в том, что сразу читать из базы и работать с возвращаемым набором записей недозволено – возникает исключение с сообщением о том, что данное соединение уже употребляется. Может быть, данная изюминка поведения будет изменена при выпуске финишной версии. Но пока единственным методом написать схожую функцию является чтение данных полностью до начала формирования выходного набора данных.




Агрегирующие функции

Большинству разрабов для построения собственных приложений полностью хватает обычного набора агрегирующих функций. Но сейчас настал праздничек и для редчайших любителей создать что-то необыкновенное – в новеньком MS SQL Server можно воплотить собственный метод выйти за границы SUM, AVG и СOUNT.

Создаются они с помощью оператора CREATE AGGREGATE:

CREATE AGGREGATE [ schema_name. ] aggregate_name

( @param_name < input_sqltype > )

RETURNS < return_sqltype >

EXTERNAL NAME assembly_name [ :class_name ]

< input_sqltype > ::=

system_scalar_type | { [ udt_schema_name. ] udt_type_name }

< return_sqltype > ::=

system_scalar_type | { [ udt_schema_name. ] udt_type_name }




сейчас написания 1-го способа недостаточно. Заместо этого для подсчета агрегатов употребляются объекты. Мысль ординарна – по мере просмотра начальных данных мы накапливаем то, что необходимо копить, а зетем выводим накопленное в выходной набор. Соответственно для всякого из этих действий необходимо воплотить по способу:

имя способа


Описание



public void Init()public void Init(input_type value)


Инициализирует объект. Вызывается один раз на группу агрегируемых значений. Если реализована версия способа с одним параметром, то SQL Server может употреблять ее для передачи первого значения в группе. Тип параметра value (input_type) должен быть совместимым с тем типом, который указан как input_sqltype в операторе CREATE AGGREGATE.



public void Accumulate(input_type value)


Опосля инициализации объекта, вызывает этот способ по одному разу для всякого агрегируемого значения. (На перечень подаваемых на вход значений, кроме состава полей в операторе GROUP BY, оказывает воздействие также и наличие главного слова distinct перед агрегируемым выражением. Как и для интегрированных функций, это ключевое слово приведет к тому, что в перечень для каждой группы попадут лишь разные значения агрегируемого выражения). Тип параметра value должен быть совместимым с тем типом, который указан как input_sqltype в операторе CREATE AGGREGATE.



public return_type Terminate()


Невзирая на ужасное заглавие, этот способ всего только должен возвратить то самое агрегированное должен быть совместимым с тем типом, который указан как return_sqltype в операторе CREATE AGGREGATE.



public void Merge(udagg_type group)


Этот способ предназначен для случаев, когда SQL Server делает больше 1-го агрегирующего объекта на одну группу входных значений. к примеру, при выполнении запроса на многопроцессорной машине, входные данные могут быть разбиты на несколько потоков для одновременной обработки. Перед выводом данных нужно выполнить слияние рассчитанных агрегатных значений. Конкретно это и делает этот способ. Он воспринимает единственный параметр такого же класса, в каком объявлен.




Таблица 4.

Кроме этих способов, у класса должен быть определен конструктор без аргументов (по другому SQL Server не сумеет создавать объекты этого класса). Не считая того, обязана быть обеспечена возможность сериализации объектов – для случаев, когда серверу необходимо сохранить промежный итог на диске. Мы отложим описание подробностей сериализации до последующего раздела, а пока что попробуем создать свою функцию для вычисления среднего геометрического.

Тем, кто плохо помнит школьный курс, напомню, что среднее геометрическое из N чисел – это корень N-ной степени из их произведения. (А среднее арифметическое N чисел, которое обычно и предполагается под термином «среднее значение» — это сумма этих чисел, деленная на N).

ПРИМЕЧАНИЕ

К огорчению, расчет среднего геометрического по определению чрезвычайно стремительно приводит к переполнению даже на чрезвычайно маленьких наборах входных данных – произведение вырастает очень стремительно. Потому мы схитрим и воспользуемся тем математическим фактом, что произведение N чисел равно экспоненте от суммы их логарифмов. Заместо извлечения корня степени N (а это то же самое, что и возведение в степень 1/N) мы поделим на N сумму логарифмов перед применением функции Exp().




[Serializable]

[SqlUserDefinedAggregate(Format.Native, IsInvariantToDuplicates = false, IsInvariantToNulls = true, IsInvariantToOrder = true, IsNullIfEmpty = true)]

[StructLayout(LayoutKind.Sequential)]

public class AvgGeom: INullable

{

private double _agg;

private int _count;

private bool _isNull = true;

#region User-Defined Attribute Required Methods

public void Init()

{

_agg = 0;

_count = 0;

_isNull = true;

}

public void Accumulate(SqlDouble Value)

{

if (!Value.IsNull)

{

_agg += System.Math.Log(Value.Value);

_count++;

_isNull = false;

}

}

public void Merge(AvgGeom Group)

{

if (!Group.IsNull)

{

_agg += Group._agg;

_count += Group._count;

_isNull = false;

}

}

public SqlDouble Terminate()

{

if (IsNull)

return SqlDouble.Null;

else

return new SqlDouble(System.Math.Exp(_agg / _count));

}

#endregion

#region INullable Members

public bool IsNull

{

get

{

return _isNull;

}

}

#endregion

}




Сначала обратим внимание на атрибут SqlUserDefinedAggregate, который предваряет описание нашего класса. В нем определено несколько характеристик (таблица 5).

Имя параметра


Описание



Format


формат сериализации объектов этого класса. Подробности – в последующем разделе.



MaxByteSize


Наибольший размер сериализованного объекта. Подробности – в последующем разделе.



IsInvariantToDuplicates


Зависит ли агрегированное к примеру, для функции MIN() совсем непринципиально, сколько раз повторяются входные значения, а для функции SUM() – принципиально. Оптимизатор запросов SQL Server может употреблять эту информацию для минимизации количества вызовов способа Accumulate.



IsInvariantToNulls


Влияет ли наличие NULL-значений во входных данных на агрегированное

IsNullIfEmpty


значит, что агрегирующая функция возвращает NULL для пустых входных наборов. к примеру, функция MIN при выполнении на пустом наборе возвращает как раз NULL , а функция COUNT() – 0.



IsInvariantToOrder


Данный параметр пока не документирован; судя по наименованию, он должен определять, влияет ли на итог порядок подачи значений в способ Accumulate(). См. примечание опосля таблицы




Таблица 5.

ПРЕДУПРЕЖДЕНИЕ

Все интегрированные агрегирующие функции (также наш пример) являются коммутативными, что дозволяет серверу выбирать порядок сканирования входных данных по собственному усмотрению. Но, к примеру, итог функций типа First() либо Last(), (которые должны возвращать соответственно 1-ое либо крайнее порядок агрегирования записей. Оператор ORDER BY применим лишь к выходному набору записей, и употреблять в нем можно лишь те поля, по которым производится группировка. В обыденных вложенных запросах (по результатам которых можно строить запросы с группировкой) применение ORDER BY запрещено. Быстрее всего (это лишь мое предположение!) создатели MS SQL Server Yukon подразумевают употреблять свойство SqlUserDefinedAggregateAttribute.IsInvariantToOrder для тех случаев, когда программер любым методом все таки может гарантировать определенное упорядочивание входных данных – это свойство обязано уверить воздержаться от переупорядочивания записей перед агрегированием. Пока что мне не удалось найти какого-нибудь воздействия этого характеристики на объект мог принимать значение NULL, нужно воплотить интерфейс INullable. Этот интерфейс описывает единственное read-only свойство bool IsNull. Все классы из System.Data.SqlTypes реализуют этот интерфейс. В нашем примере объект воспринимает значение NULL при инициализации, и перестает быть Null сходу, как ему будет передано не-NULL

Пользовательские типы данных

Систему типов SQL Server можно расширить при помощи пользовательских типов данных (User-defined Types, UDT). Пользовательские типы реализуются как управляемый класс на любом из CLR-языков и регистрируются в SQL Server. Таковой тип можно употреблять для определения типа колонки в таблице, либо как переменную (параметр процедуры) в выражении Т-SQL. При всем этом способы объектов можно вызывать прямо из T-SQL.

Создание пользовательского типа данных

В T-SQL пользовательский тип данных регится с помощью оператора CREATE TYPE:

CREATE TYPE [ type_schema_name. ] type_name

NOT NULL ] ]




В операторе указывается имя класса из за ранее загруженной в базу сборки.

Кандидатурой прямому использованию T-SQL, как и в остальных вариантах, служит автоматическое развертывание проектов MS Visual Studio .Net Whidbey. Классы, помеченные атрибутом SqlUserDefinedType (мы тщательно разглядим его чуток позднее – при обсуждении сериализации) автоматом регистрируются в качестве пользовательских типов при развертывании проектов типа SQL Server Project.

Для того, чтоб класс .NET можно было употреблять в качестве пользовательского типа данных SQL Server, он должен делать некие обязанности:

Иметь конструктор без характеристик. Как правило, он возвращает экземпляр, соответственный значению NULL (о этом дальше).

Поддерживать NULL-значения. Класс должен реализовывать интерфейс INullable, который описан в прошлом разделе. Также нужна реализация в классе статического характеристики Null, которое возвращает NULL-объект этого класса, т.е. обязано быть MyClass.Null.IsNull == true. Все способы должны корректно обрабатывать передачу в параметрах как экземпляра Null, так и значения null.

Поддерживать конверсию в строчку и назад: в классе должен быть определен способ Parse(SqlString s) и подабающим образом перекрыт способ ToString().

Поддерживать сериализацию. Так как объекты этого класса будут храниться на диске, нужно обеспечить преобразование в «тонкий» формат (также восстановление).

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

В качестве упражнения я воплотил тип «точка на плоскости». Он умеет представлять свои координаты как в декартовых, так и в полярных координатах.

using System;

using System.Data.Sql;

using System.Data.SqlTypes;

using System.Text.RegularExpressions;

using System.Runtime.InteropServices;

[Serializable]

[SqlUserDefinedType(Format.Native)]

[StructLayout(LayoutKind.Sequential)]

public class SqlPoint: INullable

{

#region NULLability

private bool _isNull = true;

public bool IsNull

{

get

{

return _isNull;

}

}

public static SqlPoint Null

{

get

{

return new SqlPoint();

}

}

#endregion

#region Конверсия в строчку и назад

public override string ToString()

{

return IsNull? «null» : String.Format(«X: {0}; Y: {1}», x, y);

}

public static SqlPoint Parse(SqlString s)

s.Value.ToLower() == «null»)

return Null;

SqlPoint p = new SqlPoint();

Regex t = new Regex(@»x:(?<x>d*(.d+)?)s*W*y:(?<y>d*(.d+)?)»,

RegexOptions.IgnoreCase);

Match match = t.Match(s.Value);

p.x = SqlDouble.Parse(match.Groups[«x»].Value);

p.y = SqlDouble.Parse(match.Groups[«y»].Value);

return p;

#endregion

#region Наши данные

private double _x=0;

private double _y=0;

public SqlDouble x

{

get

{

return IsNull ? SqlDouble.Null : new SqlDouble(_x);

}

set

{

if (!value.IsNull)

{

_x = value.Value;

_isNull = false;

}

}

}

public SqlDouble y

{

get { return IsNull? SqlDouble.Null: new SqlDouble(_y); }

set

{

if (!value.IsNull)

{

_y = value.Value;

_isNull = false;

}

}

}

public SqlDouble R // А эти характеристики мы будем вычислять.

{

get

{

return IsNull

? SqlDouble.Null

: new SqlDouble(System.Math.Sqrt(_y*_y +_x*_x));

}

set

{

if (value.IsNull)

{

_isNull = true;

return;

}

double alpha = Alpha.IsNull? 0 : Alpha.Value;

_x = System.Math.Cos(alpha) * value.Value;

_y = System.Math.Sin(alpha) * value.Value;

}

}

public SqlDouble Alpha

{

get

{

return (IsNull)

? SqlDouble.Null

: new SqlDouble(System.Math.Atan2(_y, _x));

}

set

{

if (value.IsNull)

{

_isNull = true;

return;

}

double r = R.IsNull ? 0 : R.Value;

_x = System.Math.Cos(value.Value) * r;

_y = System.Math.Sin(value.Value) * r;

}

}

#endregion

}




Опосля регистрации данного класса как пользовательского типа выполнение вот таковых запросов:

declare @p SqlPoint

set @p::x = 3

set @p::y = 4

select @p::x, @p::y, @p::R, @p::ToString()

set @p::R = 10

select @p::x, @p::y, @p::R, @p::ToString()

set @p::Alpha = 0

select @p::x, @p::y, @p::R, @p::ToString()

set @p = convert(SqlPoint, ‘ x:6; y:5.00’)

select @p::x, @p::y, @p::R, @p::ToString()




Приводит к результатам, приведенным в таблице 6:

@p::x


@p::y


@p::R


@p::ToString()



3.0


4.0


5.0


X: 3; Y: 4



6.0000000000000009


7.9999999999999991


10.0


X: 6; Y: 8



10.0


0.0


10.0


X: 10; Y: 0



6.0


5.0


7.810249675906654


X: 6; Y: 5




Таблица 6

В приведенном выше примере продемонстрирована работа с членами объектов пользовательских типов. Синтаксис довольно прост – оператором выбора члена объекта выступает двойное двоеточие.

характеристики

Характеристики ведут себя чрезвычайно похоже на переменные с таковыми «необычными» именами. В согласовании с наличием в декларации характеристики get и set его можно употреблять в присваиваниях как справа, так и слева. Все характеристики нашего класса поддерживают и чтение, и запись.

ПРЕДУПРЕЖДЕНИЕ

Изменять состояние объекта в геттере характеристики очень не рекомендуется! Дело в том, что SQL Server подразумевает, что чтение характеристики не приведет к изменению состояния. Если вы прочитаете состояние объекта доживет ровно до окончания выполнения геттера, а потом он будет возвращен в начальное состояние.




Способы

Все общественные способы класса будут доступны в T-SQL. Реализация способов предоставляет существенно огромную упругость, чем реализация параметров, но также накладывает на разраба некую ответственность.

Постоянные способы

По дефлоту считается, что все способы являются постоянными, т.е. не изменяют состояния объекта. Это дозволяет употреблять их в операторах select – Идеология SQL воспрещает какие-либо конфигурации данных в читающих запросах. По другому бы нереально было обеспечить нужные характеристики изоляции транзакций. При всем этом так же, как и для геттеров параметров, принудительно обеспечивает константность – конфигурации, произведенные в «обыкновенном» способе будут отменены сходу опосля выполнения способа, даже для переменных T-SQL.

Для проверки этих рассуждений добавим в наш класс вот таковой способ:

public SqlDouble ResetR(SqlDouble newR)

{

SqlDouble Result = R;

R = newR;

return Result;

}




Попробуем пользоваться «дырой» и неявно поменять состояние объекта:

declare @p SqlPoint

set @p::x=3

set @p::y=4

select @p::R

select @p::ResetR(10)

select @p::R




Программеру на обычных объектно-ориентированных языках естественно ждать получения разных результатов во 2-м и 3-ем запросах – ведь вызов ResetR видоизменит приватное поле объекта. Как досадно бы это не звучало, во всех 3-х вариантах возвратится одно и то же

Неконстантные способы

естественно же, SQL Server дозволяет объектам иметь и неконстантные способы. Такие способы необходимо помечать атрибутом SqlMethod со свойством IsMutator, установленным в true. При всем этом неконстантным способам запрещено возвращать какие-либо значения. Для иллюстрации реализуем «правильную» версию способа ResetR в нашем классе:

[SqlMethod(IsMutator=true, OnNullCall=false)]

public void ResetR2(SqlDouble newR)

{

R = newR;

}




Подробнее о атрибуте SqlMethod

Атрибут SqlMethod (System.Data.Sql.SqlMethodAttribute) унаследован от атрибута SqlFunction, рассмотренного ранее при описании функций. У него есть конструктор без характеристик и два новейших характеристики. Одно из их, IsMutator, мы уже разглядели. 2-ое – OnNullCall – пока недокументировано; быстрее всего речь идет о оптимизации выполнения запросов, при которой может игнорировать вызовы способов на NULL-объектах. Тем не наименее пока что мне не удалось достигнуть проявления каких-то эффектов, связанных с сиим свойством.

Сериализация

Так как хоть какой баз данных безпрерывно перемещает данные из памяти на диск и назад, главной задачей является обеспечение действенного механизма преобразования «{живых}» объектов в подходящий для хранения формат и назад. Понятно, что всепригодного решения не существует. Потому создатели SQL Server предоставили программерам широкий выбор способностей по управлению сиим действием.

Базу управления сериализацией закладывают неотклонимые для пользовательских типов атрибуты Serializable и System.Data.Sql.SqlUserDefinedTypeAttribute.

У второго из их есть последующие характеристики:

Format

// using System.Data.Sql

Format SqlUserDefinedTypeAttribute.Format {get; set}




Единственный неотклонимый параметр конструктора атрибута. Он описывает избранный формат сериализации. Может принимать последующие значения:

Native — в этом случае MS SQL Server употребляет обычный метод преобразования объекта в бинарное формат, все поля класса должны быть блиттируемыми. Этот специфичный для .NET термин значит наличие общего представления для управляемой и неуправляемой памяти. К Счастью, интегрированные скалярные типы, также их массивы и структуры, построенные из их, являются блиттируемыми. Как досадно бы это не звучало, тип System.String (как и все ссылочные типы) блиттируемым не является. Не считая ограничения по типам полей, класс должен быть помечен атрибутом [StructLayout(LayoutKind.Sequential)]. Для этого формата недозволено указывать параметр SqlUserDefinedTypeAttribute.MaxByteSize.

SerializeDataWithMetaData – в этом случае совместно с данными всякого объекта хранится также информация о их структуре. Этот формат по дефлоту установлен в пользовательских типах, сделанных по шаблону Visual Studio (Project->Add New Item… User-Defined Type). Он не просит никаких доп действий от разраба, и не накладывает фактически никаких ограничений на содержимое класса. Но его эффективность приметно ниже, чем у Native формата – замеров я не создавал, но длина бинарного представления объекта гласит сама за себя.

SerializeData – этот формат должен быть промежным меж Native и SerializeDataWithMetaData. Мысль в том, чтоб хранить структурную информацию ровно один раз на класс, а в представлениях объектов хранить лишь сами данные. Как досадно бы это не звучало, текущая версия сервера не поддерживает этот формат

UserDefined – для тех, кто предпочитает полный контроль над происходящим. В этом случае параметр MaxByteSize является неотклонимым, а класс должен воплотить интерфейс IBinarySerialize. На теоретическом уровне, дозволяет достигнуть сопоставимой с Native-форматом производительности, при отсутствии ограничений на хранимые данные.

MaxByteSize

// using System.Data.Sql

int SqlUserDefinedTypeAttribute.MaxByteSize {get; set}




Это свойство описывает очень занимаемое бинарным представлением объекта количество б. Его применение непременно лишь в случае UserDefined-формата (так как в этом случае у сервера нет метода оценить размеры буфера, выделяемого для сохранения).

IsFixedLength

// using System.Data.Sql

bool SqlUserDefinedTypeAttribute.IsFixedLength {get; set}




Устанавливайте этот параметр, если все экземпляры класса занимают однообразное количество б при сохранении в бинарном представлении.

IsByteOrdered

// using System.Data.Sql

bool SqlUserDefinedTypeAttribute.IsByteOrdered {get; set}




В тех редчайших вариантах, когда результаты сопоставления всех 2-ух экземпляров пользовательского типа совпадают с плодами словарного сопоставления их бинарных представлений, указание этого параметра дозволит серверу делать операции сортировки и индексации. Его наличие значит, что для сопоставления не необходимо десериализовывать объекты. К огорчению, пока что MS SQL Server не поддерживает использования пользовательских типов, не являющихся двоично упорядоченными, в предикатах сопоставления, операторах order by и group by, также ограничениях ссылочной целостности.

Триггеры

Реализация триггеров в .NET не очень различается от реализации процедур, рассмотренной сначала данной для нас статьи. Телом триггера будет служить статический способ класса, и все произнесенное о параметрах и общении с наружным миром остается справедливым. Можно считать триггер личным случаем хранимой процедуры, которая вызывается по какому-либо событию.

Для создаются триггера на T-SQL существует соответственный вариант оператора CREATE TRIGGER:

CREATE TRIGGER trigger_name

ON view

[ WITH ENCRYPTION ]

{

{ INSTEAD OF { [ INSERT ] [ , ] [ UPDATE ] [ , ] [ DELETE ] }

[ WITH APPEND ]

[ NOT FOR REPLICATION ]

AS

EXTERNAL NAME < method specifier >

}

}

< method_specifier > ::=

assembly_name:class_name[::method_name]




Как в других вариантах, механизм автоматического развертывания проектов в MS Visual Studio Whidbey предоставляет комфортную кандидатуру – атрибут SQLTrigger:

имя параметра


Описание



string Name


Имя триггера, соответствует параметру trigger_name в T-SQL.



string ForClause


Событие, запускающее триггер. к примеру, «INSTEAD OF INSERT”, либо “FOR CREATE_ASSEMBLY” (обратите внимание, что новейшие DDL-триггеры тоже поддерживаются)



string Target


объект, с которым ассоциируется триггер. Для традиционных DML-триггеров это имя таблицы либо view, для DDL-триггеров это или “ALL SERVER” для перехвата всех событий в границах сервера, или “DATABASE”, чтоб ограничиться лишь текущей базой.




Таблица 7.

ПРИМЕЧАНИЕ

Все три характеристики этого атрибута – лишь для чтения. Их можно установить, воспользовавшись одним из 2-ух перегруженных конструкторов: SqlTrigger(name, target, forClause) либо SqlTrigger(target, forClause).




Приведем пример обычного триггера, который будет срабатывать при разработке таблиц:

[SqlTrigger («DATABASE», «AFTER CREATE_TABLE»)]

public static void AttachAnotherTrigger()

{

SqlTriggerContext ctx = SqlContext.GetTriggerContext();

string xml = ctx.EventData.ToSqlString().Value;

Regex p = new Regex(«<object>(?<tablename>.*)</object>»,

RegexOptions.IgnoreCase);

string tableName = p.Match(xml).Groups[«tablename»].Value;

SqlContext.GetPipe().Send(String.Format(«Table {0} createdn», tableName));

using (SqlCommand cmd = SqlContext.GetCommand())

{

cmd.CommandText = String.Format(

@»create trigger {0}_insert on {0} for insert

as external name TestingYukon:CTriggerTest::AnotherTrigger»,

tableName);

cmd.ExecuteNonQuery();

}

}




Этот легкий DDL-триггер с каждой создаваемой в текущей базе таблицей связывает DML триггер на вставку. имя таблицы, на которой вышло срабатывание, извлекается из характеристики SqlChars SqlTriggerContext.EventData. Это пока недокументированное (к огорчению) свойство предоставляет исчерпающую информацию о событии, вызвавшем срабатывание триггера, в формате XML. Вот так смотрится обычное

<EVENT_INSTANCE>

<PostTime>2004-01-15T04:13:59.600</PostTime>

<SPID>56</SPID>

<EventType>CREATE_TABLE</EventType>

<Database>Northwind</Database>

<Schema>dbo</Schema>

<Object>testtrigger</Object>

<ObjectType>TABLE</ObjectType>

<TSQLCommand>

<SetOptions ANSI_NULLS=»ON» ANSI_NULL_DEFAULT=»ON» ANSI_PADDING=»ON»

QUOTED_IDENTIFIER=»ON» ENCRYPTED=»FALSE» />

<CommandText>create table testtrigger(id int identity)&#x0D;</CommandText>

</TSQLCommand>

</EVENT_INSTANCE>




Из всех этих подробностей нас пока интересует лишь содержимое элемента <object>, которое вытаскивается очевидным постоянным выражением (не сомневаюсь, что наиболее вкусившие вкусскрипт:

create table testtrigger(id int identity)

insert into testtrigger default values

drop table testtrigger

—drop trigger AttachAnotherTrigger on database



ПРЕДУПРЕЖДЕНИЕ

Текущая версия MS Visual Studio Whidbey неправильно обрабатывает удаление DML – триггеров при автоматическом развертывании. В отличие от обыденных триггеров при их удалении необходимо указывать не только лишь имя, но также и контекст ( либо база данных) с которым связан триггер. Конкретно с сиим соединено наличие закомментированной строчки в конце скрипта.




Кроме характеристики EventData, у класса SqlTriggerContext еще есть два характеристики.

Свойство TriggerAction (одноименного типа) предоставляет наиболее удачный доступ к типу деяния, вызвавшего срабатывание триггера, чем элемент <EventType>, находящийся в EventData.

Для DML-триггеров доступно также свойство bool[] ColumnsUpdated – массив флагов, определяющих, какие из колонок подверглись изменению. Подобная функциональность в триггерах T-SQL достигается с помощью функции UPDATE().

Прямого доступа к псевдотаблицам inserted и deleted нет; но их можно прочесть используя SqlCommand, полученную уже знакомым нам способом GetCommand() класса SqlContext.

Вот полный текст класса, в каком объявлены оба триггера:

using System;

using System.Data;

using System.Data.Sql;

using System.Data.SqlServer;

using System.Data.SqlTypes;

using System.Text.RegularExpressions;

public class CTriggerTest

{

[SqlTrigger («DATABASE», «AFTER CREATE_TABLE»)]

public static void AttachAnotherTrigger()

{

SqlTriggerContext ctx = SqlContext.GetTriggerContext();

Regex p = new Regex(«<object>(?<tablename>.*)</object>»,

RegexOptions.IgnoreCase);

string xml = ctx.EventData.ToSqlString().Value;

string tableName = p.Match(xml).Groups[«tablename»].Value;

SqlContext.GetPipe().Send(String.Format(«Table {0} createdn», tableName));

using (SqlCommand cmd = SqlContext.GetCommand())

{

cmd.CommandText = String.Format(

@»create trigger {0}_insert on {0} for insert

as external name TestingYukon:CTriggerTest::AnotherTrigger»,

tableName);

cmd.ExecuteNonQuery();

}

}

public static void AnotherTrigger()

{

SqlContext.GetPipe().Send(«Row Insert Interceptedn»);

}

}




Как несложно увидеть, никакого атрибута способу AnotherTrigger не назначено – мы регистрируем его вручную. К слову, еще одним преимуществом триггеров на .NET-языках по сопоставлению с T-SQL является возможность назначить одно и то же тело на разные объекты.

Информация к размышлению

Я включил в этот раздел результаты неких тестов с MS SQL Server Yukon, которые не чрезвычайно отлично вписываются в общую структуру статьи.

Yukon и Generics

Одним из более увлекательных инноваций в .NET 1.2 являются Generic-и. Они разрешают облегчить повторное внедрение кода, совместно с тем повышая надежность приложений.

Так как программирование под Yukon просит конкретно данной для нас версии .NET, разумно поинтересоваться перспективами использования в нем данного новаторства.

Как досадно бы это не звучало, впрямую употреблять Generic-тип в Yukon недозволено. В одном из собственных первых тестов я пробовал сделать обобщенную агрегирующую функцию. Таковой подход кажется полностью логичным – почти все агрегирующие функции естественным образом обобщаются на произвольные типы данных. В примере, который я привел в данной статье, функция AvgGeom воспринимает и возвращает SqlDouble. При ее применении к числам с фиксированной запятой появляются ненадобные затратные расходы на преобразование типов во время выполнения запроса. Писать же по версии данной для нас функции для всякого числового типа – расточительно и просто скучновато.

Но, попытка зарегистрировать generic-класс в качестве агрегирующей функции не удалась. Такие классы Yukon просто «не лицезреет». Это, в общем-то, разумно – как мне понятно, обязанность генерации «окончательного» класса по его generic-прототипу лежит на компиляторе. А его-то как раз в сервере нет! Потому внедрение конструкций вида GenericType<SQLDecimal> в параметре EXTERNAL NAME хоть какого из операторов CREATE лишено смысла.

Последующим шагом стала попытка предоставить серверу обыденный класс, унаследованный от спец generic-реализации. Как досадно бы это не звучало, это тоже не привело к успеху – «не лицезреет» общественные унаследованные способы. Крайним ударом в этот бубен стала таковая попытка:

public new void Init()

{

base.Init();

}




Это был практически жест отчаяния – уже необходимость специализировать класс для всякого типа аргументов убивает всю красоту обобщенного программирования. А уж написание рутинного кода только немногим лучше содержания зоопарка функций-близнецов. Получившийся в итоге класс удалось, в конце концов, «протащить» через регистрацию. К огорчению, полезности от этого было мало – при попытке употреблять функцию в запросе отыскал оба способа и посетовал на неоднозначность.

ПРИМЕЧАНИЕ

В данный момент это смотрится как обычная недоработка. Может быть, это внимание, что нужная функциональность класса агрегирующей функции не выражена в определениях какого-нибудь интерфейса – это соединено конкретно с тем, что типы характеристик могут изменяться. В определениях generic можно было бы выразить этот гипотетичный интерфейс приблизительно так:

public interface IAggregating<input_type, return_type> : INullable

{

void Init();

void Accumulate(input_type value);

return_type Terminate();

void Merge(IAggregating<input_type, return_type> other);

}




Но создатели Yukon проигнорировали эту возможность. В итоге, пригодность класса для реализации агрегирующей функции проверяется лишь на шаге его регистрации в базе данных.

Yukon и ООП

Перспектива внедрения .NET-кода в SQL Server заинтриговала меня сначала способностями ООП, которого так остро иногда не хватает в RDBMS. Но удовлетворенность моя оказалась досрочной – из инкапсуляции, наследования и полиморфизма поддерживается лишь 1-ая парадигма. Наследование и полиморфизм приходится бросить «за дверью», т.е. по ту сторону оператора CREATE ASSEMBLY.

Во-1-х, в Yukon типы совместимы лишь сами с собой и с binary. Это значит, что если колонка в таблице продекларирована как TypeA, то никаких наследников этого типа туда положить недозволено. Как досадно бы это не звучало. При регистрации в базе дела меж классами исчезают.

Во-2-х, полиморфизм в смысле виртуальных способов предполагает поддержку наследования, а ее-то как раз и нету. Другими словами виртуальные способы в Yukon ничем не лучше невиртуальных. Даже самый тоталитарный вариант полиморфизма – перегрузка способов – не работает. В пользовательском типе недозволено употреблять наиболее 1-го общественного члена с данным именованием (другими словами иметь можно, но попытка воззвания к нему приведет к ошибке).

Yukon и индексеры

Применять индексеры в рамках T-SQL недозволено. По последней мере, метода это создать я не отыскал. Как досадно бы это не звучало.

Yukon и метапрограммирование

Поддержка .NET-триггеров для метаданных вместе с возможностью употреблять бинарное содержимое сборки на предмет классов, размеченных надлежащими атрибутами, и делать автоматическую регистрацию соответственных объектов в базе данных.

Yukon и остальные

Не так издавна в форуме RSDN промелькнула ссылка на статью Эндрю Айзенберга и Джима Мелтона, SQL-программы, использующие язык программирования JAVA, размещенную издательством OSP практически 5 лет вспять. В ней упоминается эталон SQL/PSM, предложенный в 1996 году. Как ни удивительно, но синтаксис новейших конструкций T-SQL почти во всем похож на предлагаемый в этом эталоне. Основное отличие касается отсутствия параметра language, которое, быстрее всего, разъясняется неразличимостью языков в .NET. Это можно осознать как намерение Microsoft поддерживать вавилонское столпотворение «снаружи» сервера, нивелируя языковые различия благодаря природе .NET.

Перечень
литературы

Microsoft Development Environment Whidbey (8.0.30703.4),

Microsoft .NET Framework 1.2 (1.2.30703),

Microsoft SQL Server Yukon (9.00.608)

Microsoft Word 2003 (11.5604.5703)

Nescafe Gold (ТУ 9198-330-605473-98)

RSDN Authoring Pack (3.1)

Основная информация по теме статьи находится в MSDN и SQL Server Books Online.

]]>