Смекни!
smekni.com

MS SQL Server 9 Yukon. Интеграция с .NET (стр. 5 из 6)

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

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

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

Неконстантные методы

Конечно же, 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, мы уже рассмотрели. Второе – 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-формата (поскольку в этом случае у сервера нет способа оценить размеры буфера, выделяемого для сохранения). Значение этого свойства не может превышать SqlUserDefinedTypeAttribute.MaxByteSizeValue.

IsFixedLength

// using System.Data.Sql bool SqlUserDefinedTypeAttribute.IsFixedLength {get; set}

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

IsByteOrdered

// using System.Data.Sql bool SqlUserDefinedTypeAttribute.IsByteOrdered {get; set}

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

Триггеры

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

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

CREATE TRIGGER trigger_name ON { table | view } [ WITH ENCRYPTION ] { { { FOR | AFTER | INSTEAD OF } { [ INSERT ] [ , ] [ UPDATE ] [ , ] [ DELETE ] } [ WITH APPEND ] [ NOT FOR REPLICATION ] AS { sql_statement [ ...n ] | 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.

ПРИМЕЧАНИЕ Все три свойства этого атрибута – только для чтения. Их можно установить, воспользовавшись одним из двух перегруженных конструкторов: 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} created&bsol;n", 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>, которое вытаскивается банальным регулярным выражением (не сомневаюсь, что более искушенные разработчики не упустят случая применить XPath и XSLT). В этом примере выполнялся следующий T-SQL-скрипт:

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.