46378 (665526), страница 4
Текст из файла (страница 4)
Свойства
Свойства ведут себя очень похоже на переменные с такими «странными» именами. В соответствии с наличием в декларации свойства 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 |
Программисту на традиционных объектно-ориентированных языках естественно ожидать получения различных результатов во втором и третьем запросах – ведь вызов 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 } } } ::= 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("(?.*)", 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. Вот так выглядит типичное значение, попадающее в наш триггер:
2004-01-15T04:13:59.600 56 CREATE_TABLE Northwind dbo testtrigger TABLE QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" /> create table testtrigger(id int identity)
|
Из всех этих подробностей нас пока интересует только содержимое элемента , которое вытаскивается банальным регулярным выражением (не сомневаюсь, что более искушенные разработчики не упустят случая применить 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 (одноименного типа) предоставляет более удобный доступ к типу действия, вызвавшего срабатывание триггера, чем элемент , содержащийся в EventData.
Для DML-триггеров доступно также свойство bool[] ColumnsUpdated – массив флагов, определяющих, какие из колонок подверглись изменению. Аналогичная функциональность в триггерах T-SQL достигается при помощи функции UPDATE().
Прямого доступа к псевдотаблицам inserted и deleted нет; но их можно прочитать используя SqlCommand, полученную уже знакомым нам методом GetCommand() класса SqlContext.