Для проверки этих рассуждений добавим в наш класс вот такой метод:
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\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)
</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.