Главная » Microsoft SQL Server, Базы данных » Создание пользовательских типов интеграции CLR – ЧАСТЬ 2

0

?               Использование СОМ. Это можно заявить только с небольшой натяжкой, но в случае, когда пользовательский тип внедряет некоторый старый программный код, важный для организации, с помощью interop-сборки, необходима тщательная проверка типа, созданного как класс или структура. Например, когда неспособная к корректному преобразованию переменная размещается interop-сборкой, в документации предупреждается о возможных проблемах. Среди таких типов — строки, массивы, объекты, классы и типы значений. Это не обходит стороной и множество других встроенных типов, поэтому, из соображений безопасности, если в типе планируется использование СОМ, то этот тип разумно создавать как класс.

Остальные важные требования будет лучше обсудить в контексте шаблона пользовательского типа программы Visual Studio 2005.

Программирование пользовательских типов CLR в Visual Studio

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

Создаваемый в качестве примера тип будет использоваться для хранения IP-адресов. Это самая распространенная схема адресации, используемой в настоящее время в Интернете. IP-адрес традиционно представляется четырьмя целыми числами, каждое из которых находится в диапазоне от нуля до 255, разделенными точками. В настоящее время большинство сетей назначают каждому узлу один или несколько IP-адресов для поддержки транспортного и сетевого уровней модели OSI. В Интернете используются символьные имена DNS, присвоенные IP-адресам. На практике довольно часто возникает потребность представлять 1Р-адреса в базе данных, однако в предыдущих версиях SQL Server было довольно проблематично обеспечивать доменную целостность этих значений. Обычно IP-адреса хранились как строки, а проверка выполнялась уже на уровне приложения, которому нужны были эти данные. С помощью пользовательских типов в SQL Server можно организовать проверку IP-адресов уже на уровне базы данных с помощью обычных выражений .NET. К тому же теперь стало возможным ссылаться на пространство имен System.Net из членов пользовательского типа.

Полное решение Visual Studio 2005 для пользовательского типа, обсуждаемого В в настоящей главе, можно загрузить с Web-сайта книги. В этом решении со- ^^ХСети держатся рабочие примеры типа IP-адреса, реализованного тремя способами:

•                       как структуры в своей простейшей форме;

•                       как структуры с пользовательским форматом;

•                       как классы.

Давайте посмотрим, как можно создать пользовательский тип для IP-адреса. В простой структуре пользовательского типа можно увидеть его элементы. В первую очередь обратите внимание на системные сборки, которые должны быть включены во все пользовательские типы: Imports System Imports System.Data Imports System.Data.Sql Imports System.Data.SqlTypes Imports Microsoft.SqlServer.Server Imports System.Text.RegularExpressions

1 Если столбец с пользовательским типом будет индексироваться,

‘ необходимо установить свойство IsByteOrdered в значение true <Serializable()> _

<Microsoft.SqlServer.Server.SqlUserDefinedType(Format.Native, IsByteOrdered:=True)> _

Public Structure IPTypel Implements INullable

Private Shared Readonly _parser As _New _

Regex("\A((2[0-4]\d|25 [0-5] | [01] ?\d\d?)\.){3}(2[0-4]\d|25 [0-5] |

[01]?\d\d?)")

Private Const _NULL As String = "NULL_IP"

Private m_Null As Boolean Private m_OctetA As Byte Private m_OctetB As Byte Private m_OctetC As Byte Private m_OctetD As Byte

Public Overrides Function ToString() As String If Me.IsNull Then Return _NULL

‘ можно также использовать ‘ Return Nothing Else

Return Me .m_OctetA. ToString () & " .11 & _

Me.m_OctetB.ToString() &            & _

Me.m_OctetC.ToString() & "." & _

Me.m_OctetD.ToString()

End If End Function

Public Readonly Property IsNull() As Boolean Implements _

INullable.IsNull Get

Return m_Null End Get End Property

Public Shared Readonly Property NullO As IPTypel Get

Dim h As IPTypel 1= New IPTypel h.m_Null = True Return h End Get End Property

Public Shared Function Parse(ByVal s As SqlString) As IPTypel If s.IsNull Or s.Value = _NULL Then

‘ база данных определяет допустимость в столбце пустых значений

Return Null End If

‘ конструктор не обязателен для структуры 1 Допустимо, но не обязательно, чтобы

‘ каждый член имел значение по умолчанию, чтобы каждый октет ‘ IP-адреса был инициализирован нулем Dim u As IPTypel ‘= New IPTypel

‘строка .NET должна быть разобрана на составляющие Dim str As String = Convert.ToString(s)

Dim m As Match = _parser.Match(str)

If m.Success Then

Dim arr() As String = str.Split(CType, Char))

u.OctetA = CType(arr(0), Byte)

u.OctetB = CType(arr(1), Byte)

u.OctetC = CType(arr(2), Byte)

u.OctetD = CType(arr(3), Byte)

Return u Else

Throw New ArgumentException("Invalid IP v4 Address")

‘ Return Nothing End If End Function

‘results in 0.0.0.0 = null Public Property OctetAO As Byte Get

OctetA = m_OctetA End Get

Set(ByVal value As Byte) m_OctetA = value End Set End Property

Public Property OctetBO As Byte Get

OctetB = m_OctetB End Get

Set(ByVal value As Byte) m_OctetB = value End Set End Property

Public Property OctetCO As Byte Get

OctetC = m_OctetC End Get

Set(ByVal value As Byte) m_OctetC = value End Set End Property

Public Property OctetDO As Byte Get

OctetD = mjDctetD End Get

Set(ByVal value As Byte) m__OctetD = value End Set

End Property

<SqlMethod(IsDeterministic:=True, IsPrecise:=True)> _

Public Function GetCSubNetO As String

GetCSubNet = m_OctetC.ToString +      + m_OctetD.ToString

End Function End Structure

Если нужно заложить в тип другие функции, то в проект интеграции CLR нужно включить и другие пространства имен с помощью команды Imports и ссылки на соответствующую сборку. Например, к обычным выражениям, инструментам WMI (Windows Management Instrumentation) и множеству сетевых служб можно получить доступ из типов интеграции CLR с помощью предлагаемых системой пространств имен .NET. Для использования регулярных выражений добавьте следующий оператор:

Imports System.Text.RegularExpressions

Далее объявляется класс или структура. Некоторые важные аспекты объявления могут варьироваться в зависимости от того, на чем основан тип — на структуре или классе. В первую очередь следует определить обязательные атрибуты. Затем указывается область определения класса и его имя. Если класс является производным от другого, то далее указывается родительский класс. Заметим, что класс может наследовать только от одного класса, а структура не может наследовать вообще. Если класс реализует какой-либо интерфейс, реализации этого интерфейса перечисляются после родительского объекта. Закрывается класс или структура оператором End Class или End Structure.

Теперь рассмотрим объявления элементов структуры или класса более подробно.

Атрибут Serializable обеспечивает поддержку метаданных для представления состояния данных пользовательского типа в потоке байтов при их транспортировке и использовании, а также для упаковки потока с целью хранения в типе данных. Атрибут Serializable не обязателен в пользовательском типе, однако в большинстве ситуаций может оказаться полезным. Конечно, сериализация помогает определить, как поток байтов будет разделяться на данные членов пользовательского типа. В общем случае сериализация занимается объединением всех полей пользовательского типа в двоичный поток или поток XML. Атрибут Sertializable не имеет параметров — он просто должен быть определен, после чего в теле структуры или класса должны быть определены интерфейсы и методы сериализации.

Дополнительная Дискуссию об использовании пользовательских атрибутов типов интеграции информация CLR см. в главе 27.

Атрибут SqlUserDef inedType является специализированным и используется только для пользовательских типов. Как отмечалось в главе 27, компилятор использует этот атрибут при компиляции сборки в код MSIL. Он также оказывает влияние на манифест сборки и используется SQL Server во время загрузки сборки в сервер. Этот атрибут довольно емкий; он содержит четыре параметра. Во-первых, следует четко определить формат хранения типа; по умолчанию в шаблоне предусмотрен формат Fonmat .Native. Этот формат способен обеспечить наибольшую совместимость и производительность при минимальном объеме дополнительного программирования. Если столбцы, создаваемые с данным типом, будут участвовать в индексации, также необходимо установить параметр IsByteOrdered. Среди остальных значений атрибута очень полезный Format .UserDef ined и относительно безынтересный Format.Unknown.

Если пользовательский тип определяется как класс, использующий “родной” формат, то для обеспечения совместимости с СОМ должен быть установлен атрибут Struct Layout. Это выполняется с помощью добавления пространства имен InteropServices и присвоения свойству LayoutKind значения StructLayout. Возможными значениями атрибута также являются Auto, Explicit и Sequential, и в большинстве пользовательских типов интеграции CLR обычно используют значение Sequential. В общем случае значение Auto указывает системе самой принять решение относительно компоновки полей пользовательского типа; значение Explicit позволяет программисту явным образом определить компоновку, а значение Sequential инструктирует программу располагать поля в порядке их отправки. По умолчанию компилятор Visual Studio использует раскладку Sequential.

Импортируется пространство имен:

Imports System.Runtime.InteropServices

Параметр LayoutKind. Sequential включается с остальными пользовательскими атрибутами:

<System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)> _

Для определения способа доступа к типу используется область определения структуры. В VB.NET уровни доступа могут быть установлены с помощью следующих модификаторов: Public, Private, Friend, Protected и ProtectedFriend. В других языках программирования семейства .NET существуют аналогичные модификаторы. Чтобы пользовательский тип был доступен вне домена приложения (как вы помните из главы 27, домен приложения является аналогом схемы базы данных), уровень доступа должен быть установлен в Public: <Serializable()> _

<Microsoft.SqlServer.Server.SqlUserDef inedType(Format.Native, IsByteOrdered:=True)> _

Public Structure IPTypel

Implements INullable

В отличие от класса структура не может наследовать от другой структуры. В то же время, подобно классу, структура может оттенять (или скрывать, или полностью замещать) другой программный элемент, имеющий точна такое же имя, с помощью модификатора Shadows.

Дополнительная Дополнительные ресурсы, из которых можно почерпнуть информацию о работе информация, со средой .NET Framework, см. в главе 27.

Класс может наследовать от другого класса. Это значит, что в дополнение к модификаторам уровня доступа и Shadows доступны и два дополнительных для базового класса типа, определяющего область наследования класса.

?               Модификатор Must Inherit требует, чтобы класс мог использоваться только для наследования, и запрещает создание экземпляров данного класса.

?               Модификатор Not Inheritable запрещает использование данного класса другими для наследования.

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

Если тип объявляется как класс, то он может наследовать только от одного базового класса. Для указания базового класса в языке VB.NET предусмотрено ключевое слово Inherits (сопровождаемое именем класса), которое вставляется в объявление непосредственно после имени типа. Необходимо также включить ссылку на сборку, где можно найти базовый класс (если он находится в другой сборке), а также пространство имен в разделе Imports в верхней части файла. Несмотря на то что тип может управляться из базового класса, SQL Server

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

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

<Serializable()> _

<Microsoft.SqlServer.Server.SqlUserDefinedType(Format.Native, IsByteOrdered:=True)> _

<Systern.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)> _ Public Class IPType3 Implements INullable

Последнее, что нужно вставить в объявление класса, — это имена интерфейсов, которые будут использоваться типом. Класс может использовать несколько интерфейсов. В пользовательский тип всегда нужно внедрять интерфейс INullable из пространства имен Sys- tem. Data. SQLTypes. Этот интерфейс содержит свойство IsNull, позволяющее среде .NET Framework функционировать в базе данных, допускающей пустые значения в каком- либо из типов данных.

Среди других интерфейсов, которые реализуются встроенными типами данных SQL Server, можно упомянуть IComparable и IXMLSerializable из пространства имен System. Интерфейс IComparable служит для поддержки сравнений в среде .NET. и позволяет разработчику определять, как экземпляр пользовательского типа может быть вычислен в выражении. IXMLSerializable является интерфейсом из пространства имен System.XML, обеспечивающим поддержку сериализации и десериализации между структурой хранилища и потоками XML путем подмены методов ReadXML и WriteXML. Еще один интерфейс, интересный с точки зрения пользовательских типов, используется в удаленных и внутренних организационных ситуациях. Это интерфейс IBinarySerialize из пространства имен Microsoft. SQLServer. Server. Его методы Read и Write должны быть реализованы при использовании формата сериализации UserDef ined.

<Serializable()> _

<Microsoft.SqlServer.Server.SqlUserDefinedType(Format.UserDefined, _

IsByteOrdered:=True, _

IsFixedLength:=True, _

MaxByteSize:=4, _

Name : =111 Pv411) > _

Public Structure IPType2

Implements INullable, IComparable, IBinarySerialize,_

IXMLSerializable

Напомню, что формат сериализации в атрибуте пользовательского типа SQLUserDef inedType обычно определен как Native. Формат сериализации UserDef ined может пригодиться в случаях, когда структура или класс пользовательского типа содержит свойства, не соответствующие встроенным в SQL Server числовым и временным типам. К тому же, когда формат определен как UserDef ined, идентификатор свойства IsByteOrdered является обязательным, а свойство MaxByteSize пользовательского типа должно находиться в диапазоне от 1 до 8 ООО. Эти ограничения размера выдвигает SQL Server. Лимит в 8 Кбайт проясняет причину, по которой бизнес-объекты пока нельзя считать хорошими кандидатами на реализацию с помощью пользовательских типов.

Методы Read и Write, необходимые для сериализации UserDefined, выглядят несколько туманно, поскольку в документации отсутствует их четкое описание. К сожалению, это может привести многих разработчиков (в частности, тех, кто пытается найти свой путь из Т-SQL в .NET) к решению замкнуться в сериализации Native, избегая того, чего они не понимают. Может, это и стало бы хорошим решением для редко используемых пользовательских типов, предназначенных для реализации правил бизнес-логики, однако сериализация Native может стать сдерживающим фактором внедрения пользовательских типов, когда на первое место выходят вопросы производительности. Для примера представим, насколько полезно было бы сортировать географические координаты по таким атрибутам, как континент, страна или область. Аналогично, представьте, насколько удобнее сортировать ЕР-адреса по октетам, а не как единую строку. Сериализация Native совершенно не предназначена для потребностей подобных сортировок. В то же время сериализация UserDefined полностью развязывает программисту руки в вопросах перемещения и хранения данных.

В документации программы Visual Studio содержится гораздо лучшая дискуссия по вопросам сериализации, чем в документации по интеграции CLR в SQL Server. К сожалению, даже документация Visual Studio требует от читателя концептуального подхода к сериализации в среде .NET Framework. Когда она применяется в сложных бизнес-объектах, реализованных с помощью специализированных пользовательских типов SQL Server, которым необходимы только методы Read и Write, сложность таких объектов может охладить программиста. Сериализация является ничем иным, как кодированием значений всех полей членов экземпляра структуры или класса в позиционный поток битов в операциях транспортировки и копирования. Несколько странно узнать, что все поля, общедоступные и частные, должны быть загружены и направлены в поток, хотя для разработчика было бы достаточно явно сериализовать только поля членов, чтобы позволить состоянию экземпляра быть точно воссозданным на другом конце операции транспортировки или копирования.

В примерах с IP-адресом, описываемых в настоящей главе, один из типов использует сериализацию UserDefined. Четырехбайтовые частные поля должны всегда находиться в одной и той же последовательности, чтобы гарантировать представление одного и того же IP-адреса. Следствия нарушения порядка октетов IP или их неполноты при транспортировке очевидны для любого читателя, хотя бы немного знакомого с сетевым стеком TCP/IP. Сериализация гарантирует порядок и качество данных при передаче из базы данных клиенту. В методе Read необходимо только загрузить поля в объект BinaryReader. После этого в методе Write нужно извлечь поля из объекта Binary Writer точно в том же порядке, который использовался в методе Read.

Public Sub Read(ByVal r As System.10.BinaryReader) _

Implements IBinarySerialize.Read m_OctetA = r.ReadByteO m_OctetB = r.ReadByteO m_OctetC = r.ReadByteO m_OctetD = r.ReadByteO End Sub

Public Sub Write(ByVal w As System.10.BinaryWriter) _

Implements IBinarySerialize.Write w.Write(m_OctetA) w.Write(m_OctetB) w.Write(m_OctetC) w.Write(m_OctetD)

End Sub

В приведенном выше фрагменте программного кода октеты IP были определены в порядке от А до D. Это не так технически важно — главное, чтобы порядок полей в методах Read и Write в точности совпадал. Эта гибкость продемонстрирована в загружаемом коде, равно как и реализация интерфейса I Comparable.

Тестирование и отладка пользовательского типа

Перед тем как пользовательский тип сможет быть адекватно использован, успешно скомпилированная сборка должна быть развернута в SQL Server. К тому же наиболее вероятные сценарии функционирования должны включать создание таблицы и выполнение солидного пакета инструкций DML, чтобы проверить правильность работы нового пользовательского типа в контексте базы данных.

Visual Studio в этом отношении предлагает для этого этапа разработки полезный компонент Test Scripts уровня проекта. Существуют три существенных преимущества использования этого проекта над развертыванием пользовательского типа на тестовом сервере (даже если Developer Edition SQL Server развернут в той же среде, что и Visual Studio) и тестированием с помощью Management Studio или утилиты командной строки SQLCMD. Первое из них заключается в том, что внимание разработчика может быть эксклюзивно сфокусировано на тестировании функциональности. Когда тестовый сценарий используется в проекте, пользовательский тип может быть развернут и переразвернут в ходе разработки с помощью пункта меню Debug^Visual Studio Deploy. Когда же используется Management Studio или какая- либо другая утилита, внешняя по отношению к Visual Studio, нужно постоянно вручную проверять наличие в ней ссылки на версию пользовательского типа, скомпилированную последней. Вторым преимуществом является то, что пошаговое прохождение из сценария Т-SQL по коду CLR полностью поддерживается только в интерфейсе Visual Studio. Третье преимущество заключается в том, что версии тестовых сценариев можно поддерживать с помощью SourceSafe вместе с другими компонентами проекта.

Для создания и удаления любых таблиц и представлений, на которые содержатся ссылки в тестовом сценарии, полезно создать специальные инструкции DDL. Это сэкономит время, которое пришлось бы затратить на поиск таблиц и представлений для удаления в них объектов с изменяемым в процессе разработки типом.

Вопросы производительности

Оптимизация программного кода CLR требует не столько усилий, как настройка производительности кода Т-SQL. В то время как на практике доводилось слышать множество историй о преобразовании 30-часовой хранимой процедуры в 10-минутный запрос, пользовательские типы интеграции CLR вряд ли окупят героические усилия, вложенные в их оптимизацию. Если пользовательские типы создавались более-менее корректно, то максимум, чего можно ожидать, — это устранения из кода нескольких циклов работы процессора. Если реализация типа была некорректной, то в производительности могут остаться некоторые узкие места, и аккуратный разработчик может их избежать. Например, если в пользовательском типе реализован метод, требующий уровня защиты EXTERNAL_ACCESS, и результат этого метода остался в наборе данных, когда значение типа было добавлено в таблицу, пользователь должен осознавать, что не существует способа управления тем, что может произойти в процессе извлечения внешнего значения. Очень вероятно, что низкая производительность операций ввода-вывода связана с задержками в сети или невосприимчивыми внешними источниками данных, к которым выполняется обращение из программного кода .NET класса пользовательского типа.

Рассмотрим следующий пример пользовательского типа IP-адреса, который использует пространство имен System.Net для преобразования IP-адреса в имя сервера с помощью класса DNS:

<SqlMethod(IsDeterministic:=False)> _

Public Function GetDNSNameO As String If Not (Me.IsNull) Then GetDNSName = _NULL Else Try

GetDNSName =_

Dns.GetHostEntry(IPAddress.Parse(Me.ToString)).HostName Catch ex As Sockets.SocketException

GetDNSName = "Socket Error: " & ex.SocketErrorCode.ToString End Try End If

End Function

Как правило, следует остерегаться мощи интеграции CLR. Предположим, что Внимание! описанный выше метод используется при каждом добавлении IP-адреса в таблицу. При этом имя компьютера будет сохраняться в таблице как вычисляемый столбец. Некоторые помехи способны затянуть продолжительность транзакции вставки и вывести ее за установленные пределы. А в SQL Server может случиться масса неожиданных и непредсказуемых событий, непосредственно не касающихся выполняемого кода CLR. Узкие места сети и конфликты DNS могут создать недопустимый уровень конкуренции в таблице. Все, что остается в данной ситуации операции вставки, — это смириться с задержками. К тому же в случаях, когда добавляемый IP-адрес не может быть преобразован в имя узла, увеличение нагрузки за счет обработки исключения SocketException для каждого вставляемого значения может также стать причиной замедления работы базы данных. Другими словами, потенциальные возможности интеграции CLR могут запросто привести к нежелательным последствиям, которые следует предвидеть при создании и использовании пользовательских типов.

Как и любой другой столбец данных, столбец, созданный с пользовательским типом, может существенно поднять производительность, если он будет проиндексирован. Столбец с пользовательским типом может индексироваться только в том случае, если он может быть отсортирован в двоичном порядке. Гетерогенные пользовательские типы (т.е. те, которые содержат смесь встроенных типов) могут помочь создать более полезные индексы, если был тщательно продуман порядок десериализации полей членов. Рассмотрим пользовательский тип с интегральными и символьными полями. Если символьное поле более интересно с точки зрения поиска и является наиболее вероятным претендентом на включение в выражение аргумента поиска предложения WHERE, при сериализации разумно вставлять значение этого поля в хранилище первым. Вычисляемые столбцы, управляемые членами класса пользовательского типа, также могут быть проиндексированы, если в атрибуты этих членов включено свойство IsDeterministic.

Еще один вопрос, который уже обсуждался в контексте структурных отличий семантики значений и ссылок, связан с тем, что пользовательские типы, созданные как структуры, имеют тип значений и хранятся в стеке программы, в то время как типы, созданные как класс, имеют тип ссылок и находятся в пространстве кучи объекта. В общем случае влияние этого отличия на производительность невелико. В примере кода для этой главы, содержащемся на Web-сайте книги, выполняется тест производительности, который заключается в следующем. Выполняется одно и то же количество вставок в пользовательские типы, созданные с помощью структур (семантика значений) и классов (семантика ссылок) и встроенной сериализации. Класс пользовательского типа использует сериализацию UserDef ined и столбец со встроенным типом varchar. В результате выполнения этого теста не было замечено значительных отличий в производительности операций вставки. Тем не менее при проектировании пользовательского типа полезно оценить затраты на внедрение и знать о существующих различиях.

Когда технология пользовательских типов интеграции CLR пробьет себе дорогу в приложения, работающие с SQL Server, естественно, возникнут новые вопросы, связанные с производительностью. В этом процессе компания Microsoft, несомненно, будет постепенно совершенствовать модель пользовательских типов с целью повышения их производительности.

Источник: Нильсен, Пол. Microsoft SQL Server 2005. Библия пользователя. : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2008. — 1232 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии