[... n] |
<. NET_Framework_reference>) p>
- p>
<. NET_Framework_reference> ::= p>
EXTERNAL NAME
assembly_name: class_name [:: method_name] p>
Як видно з цього фрагмента, тепер замість вказівки
тіла процедури на T-SQL можна вказати метод класу з завантаженої раніше складання.
До цього методу ставляться такі вимоги: p>
Це повинен бути статичний метод (не конструктор і не
деструктор класу) p>
Число параметрів має збігатися із числом параметрів
в описі збереженої процедури, а їх типи повинні бути сумісні з типами даних
відповідних параметрів. Якщо параметр процедури оголошений як OUTPUT, то
відповідний параметр методу повинен передаватися по посиланню. p>
Метод має або не мати значення, що повертається,
або повертати значення одного з таких типів: SQLInt32, SQLInt16,
System.Int32, System.Int16 p>
Для успішного створення такої збереженої процедури
необхідно бути власником відповідної складання або мати для неї права
REFERENCES. p>
Давайте перейдемо від слів до справи і спробуємо створити
збережену процедуру. p>
Мінімальний код збереженої процедури на C # виглядає ось
таким чином: p>
using System; p>
using System.Data; p>
using System.Data.Sql; p>
using System.Data.SqlServer; p>
using System.Data.SqlTypes; p>
public class StoredProcedure p>
( p>
[SqlProcedure] p>
public static void
MyProcedure () p>
( p>
) p>
); p>
Очевидно, він не дуже функціональний. Тим не менше,
метод StoredProcedure.MyProcedure вже можна зареєструвати в базі даних
як збереженої процедури, викликати (наприклад, з Query Analyzer), і
переконатися, що він успішно виконується (тобто нічого не робить). p>
Зверніть увагу на атрибут SqlProcedure
(System.Data.Sql.SqlProcedureAttribute). Цей атрибут не несе ніякої
інформації для MS SQL Server. Він використовується MS Visual Studio Whidbey при
розгортанні проекту - для методів, позначених таким атрибутом, автоматично
будуть викликані відповідні оператори CREATE PROCEDURE. За замовчуванням буде
зроблена спроба призначити збереженої процедури таке ж ім'я, як і у методу.
Це поведінку можна змінити, скориставшись єдиною властивістю атрибута
- Name. Якщо замінити дев'ятий рядок прикладу вище на
[SqlProcedure ( "MyProcName")], то збережена процедура буде називатися
MyProcName. P>
Привіт, світ p>
Зупинятися на те, яким чином збережена
процедура обробляє дані, сенсу немає - це звичайний C #, і його особливості
добре відомі. Давайте навчимо її спілкуватися із зовнішнім світом. Для початку доведемо
її до рівня Керніган та Рітчі: p>
using System; p>
using System.Data; p>
using System.Data.Sql; p>
using System.Data.SqlServer; p>
using System.Data.SqlTypes; p>
public class StoredProcedure p>
( p>
[SqlProcedure ( "HelloWorld ")] p>
public static void
MyProcedure () p>
( p>
SqlContext.GetPipe (). Send ( "Hello, Yukon !"); p>
) p>
); p>
Ця процедура демонструє ще один важливий компонент,
зв'язує. NET з MS SQL Server: клас System.Data.SqlServer.SqlContext. Цей
клас містить кілька статичних методів, що забезпечують доступ до
контексту, в якому виконується код. У даному випадку ми отримуємо доступ до
об'єкту класу System.Data.SqlServer.SqlPipe, який представляє серверну
сторону з'єднання з клієнтом. Саме в цю «трубу» SQL Server відправляє
результати виконання запитів. Якщо зберігається процедура повинна повертати
якісь дані в клієнтську програму, то без SqlPipe не обійтися. p>
У цьому прикладі ми використовуємо метод SqlPipe.Send (String
msg), призначений для відправлення текстових повідомлень. Його функціональність
аналогічна команді print в T-SQL. Решта методів SqlPipe призначені для
відправки табличних даних: p>
Метод або властивість p>
Опис p>
public void Execute (System.Data.SqlServer.SqlCommand command) public
void Execute p>
(System.Data.SqlServer.SqlExecutionContext request) p>
Виконує зазначену команду
або запит і повертає результат клієнтові. Аналог виконання оператора SELECT
... FROM ... в збереженої процедури на T-SQL. P>
public void Send (System.Data.SqlServer.SqlError se) p>
Повертає клієнту
зазначену помилку. p>
public void Send (System.Data.Sql.ISqlReader reader) p>
Відправляє клієнтові всі
записи із заданого набору. p>
public void SendResultsStart (System.Data.Sql.ISqlRecord record, bool
sendRow) p>
Посилає клієнту першого
запис в наборі записів. Встановлює властивість SendingResults в true. P>
public System.Boolean SendingResults (get;) p>
Вказує, що процес
відправки набору записів не закінчений. p>
public void SendResultsRow (System.Data.Sql.ISqlRecord record) public
void Send (System.Data.Sql.ISqlRecord record) p>
Посилає клієнту чергову
запис у наборі. Вимагає SendingResults == true. P>
public void SendResultsEnd
() P>
Сигналізує про закінчення
набору записів і встановлює властивість SendingResults в false. p>
Таблиця 2. p>
Таким чином, крім передачі клієнтові набору даних,
отриманого від сервера, можна формувати результати вручну. З точки зору
клієнта це буде виглядати як звичайний набір записів. p>
Повертаємо випадкових символів
p>
Поки що документація досить скупо висвітлює цей
питання, але після декількох експериментів мені вдалося створити ось таку
процедуру: p>
[SqlProcedure ()] p>
public static void CurrencyCourse ( p>
[SqlMapping (typeof (SqlDateTime))] DateTime start, p>
[SqlMapping (typeof (SqlDateTime))] DateTime end) p>
( p>
using (SqlCommand cmd =
SqlContext.GetCommand ()) p>
( p>
cmd.CommandText = @ " p>
select changeDate, course
from Course p>
where changeDate between
@ start and @ end "; p>
cmd.Parameters.AddWithValue ( "@ start",
start); p>
cmd.Parameters.AddWithValue ( "@ end", end); p>
DateTime current = start; p>
SqlDecimal course =
SqlDecimal.Null;// спочатку курс відсутня; p>
SqlMetaData [] recstruct =
new SqlMetaData [2]; p>
recstruct [0] = new
SqlMetaData ( "D", SqlDbType.DateTime); p>
recstruct [1] = new
SqlMetaData ( "course", SqlDbType.Decimal, 10, 4); p>
SqlDataRecord rec = new
SqlDataRecord (recstruct); p>
SqlPipe pipe =
SqlContext.GetPipe (); p>
pipe.SendResultsStart (rec,
false); p>
using (SqlDataReader r =
cmd.ExecuteReader ()) p>
( p>
while (r.Read ()) p>
( p>
rec.SetSqlDecimal (1,
course); p>
while (current <
r.GetDateTime (0)) p>
?? ( P>
rec.SetDateTime (0,
current); p>
pipe.SendResultsRow (rec); p>
current =
current.AddDays (1); p>
) p>
course =
r.GetDecimal (1); p>
) p>
) p>
rec.SetSqlDecimal (1,
course); p>
while (current <= end) p>
( p>
rec.SetDateTime (0,
current); p>
pipe.SendResultsRow (rec); p>
current = current.AddDays (1); p>
) p>
pipe.SendResultsEnd (); p>
) p>
) p>
Ця процедура перетворює дані в таблиці зміни
курсів певної валюти (Course) в таблицю щоденних значень курсу, повторюючи
попереднє значення для тих днів, що змін не відбувалося. p>
На цей раз у процедури є параметри. Щоб допомогти
інструментів автоматичного розгортання (наприклад, той же MS VS Whidbey)
визначити SQL-типи параметрів збереженої процедури, для параметрів методу можна
вказати атрибут SqlMapping (System.Data.Sql.SqlMappingAttribute). Його
єдиний параметр і задає тип для параметра процедури. У даному випадку
цей атрибут є надмірним - параметри типу DateTime автоматично
відображаються в тип SQL datetime (якому відповідає тип CLR
System.Data.SqlTypes.SqlDateTime), але в більш складних випадках їм доведеться
користуватися для усунення неоднозначності. p>
Щоб виконати запит до даних сервера, ми
скористаємося ще одним статичним методом класу SqlContext --
SqlContext.GetCommand (). p>
Щоб повернути дані клієнта, потрібен примірник
класу, що реалізовує інтерфейс System.Data.Sql.ISqlRecord. У даному випадку
використаний System.Data.Sql.SqlDataRecord. Його конструктор вимагає вказати
бажану структуру запису. Ця структура описується масивом об'єктів класу System.Data.Sql.SqlMetaData.
У кожному об'єкті задається ім'я і тип відповідної колонки. Ми описуємо
структуру, яка відповідає в термінах SQL ось такий «таблиці»: p>
( p>
D datetime, p>
course decimal (10, 4) p>
) p>
Створивши профіль, ми ініціюємо процес відправки за допомогою
виклику: p>
pipe.SendResultsStart (rec, false); p>
Другий параметр говорить про те, що саму запис
відправляти клієнтові не потрібно, а замість цього метадані запису використовуються для
ініціалізації відправляється набору записів. p>
Далі все просто - ми читаємо чергову запис з
SqlDataReader, отриманого в результаті виконання команди, заповнюємо поля в
SqlDataRecord, і відправляємо її клієнтові. Додатковий цикл в кінці досилає
запису для дат між останньою зміною і кінцем запитаного інтервалу. p>
Відправивши все, що хотілося, ми сигналізуючи клієнту
про закінчення набору за допомогою дзвінка p>
pipe.SendResultsEnd (); p>
Варто зазначити, що результати повертаються безпосередньо
клієнту, тобто код, який викликав процедуру, яка не має над цим процесом
ніякого контролю. Повторне використання такого коду в серверної частини
програми малоймовірно. У наступному розділі ми дізнаємося про те, як можна обійти
це обмеження. p>
Опції
p>
У рамках T-SQL функції поділяються на два види: скалярні
і табличні. p>
ПРИМІТКА p>
Є ще агрегатні функції, але їх
реалізація істотно відрізняється від «звичайних», і тому ми
розглянемо їх у наступному розділі. p>
З точки зору. NET, ці два типи функцій влаштовані
майже однаково. Як і процедури, що зберігаються, вони реалізуються за допомогою
статичних методів класу. Відмінність полягає в тому, як вони повертають
значення. Є три варіанти: p>
Повертаємо значення довільного типу. Це скалярна
функція. p>
Повертаємо System.Data.Sql.ISqlReader. Структура
даних у ньому повинна збігатися з декларованої структурою результату функції.
Це таблична функція. p>
Повертаємо void. Усередині функції вручну формуємо
що повертаються дані через SqlContext.GetReturnResultSet (). Це теж таблична
функція. p>
Всі ці варіанти докладно розглянуті далі. p>
ПРИМІТКА p>
На відміну від вбудованих функцій,
звертатися до «саморобним» потрібно з повагою - випереджаючи ім'я функції ім'ям
схеми (яке за замовчуванням збігається з іменем її власника). Наприклад, я
викликав функцію з наступного підрозділу приблизно ось так: p>
select
dbo.RevertString ( "Beavis rulez") p>
Скалярні функції
p>
Це найпростіша різновид функцій. В якості
прикладу напишемо свій варіант вбудованої функції reverse: p>
[SqlFunc ()] p>
[SqlFunction ( p>
DataAccess =
DataAccessKind.None, p>
SystemDataAccess =
SystemDataAccessKind.None, p>
IsDeterministic = true, p>
IsPrecise = true)] p>
public static SqlString RevertString (SqlString str) p>
( p>
if (str.IsNull) p>
return SqlString.Null; p>
System.Text.StringBuilder sb =
new p>
System.Text.StringBuilder (str.Value.Length); p>
for (int i = str.Value.Length-1;
i> = 0; i -) p>
sb.Append (str.Value [i ]); p>
return new
SqlString (sb.ToString ()); p>
) p>
Оскільки реалізація самої функції примітивна,
зупинимося на тому, що її оточує. p>
По-перше, до методу застосований атрибут SqlFunc. Як і
SqlProcedure, він дозволяє вказати засобам автоматичного розгортання
інформацію, необхідну для правильної побудови команди CREATE FUNCTION. У
даному випадку ніяких параметрів не використано - атрибут просто вказує,
що даний метод треба буде зареєструвати як функцію. Більш докладно ми
розглянемо можливості цього атрибуту трохи пізніше. p>
А от наступний атрибут - SQLFunction - вже
використовується «всередині» MS SQL Server для визначення того, як можна цю функцію
використовувати. У таблиці 3 наведено опис параметрів цього атрибуту: p>
Ім'я параметру p>
Опис p>
DataAccess p>
Який доступ здійснює
функція до користувача даних в базі: DataAccessKind.None --
нікакого.DataAccessKind.Read - читає дані. p>
SystemDataAccess p>
Який доступ здійснює
функція до системних даними в базі: SystemDataAccessKind.None --
нікакого.SystemDataAccessKind.Read - читає дані. p>
IsDeterministic p>
Чи є функція
детерміністичних, тобто чи залежить її повертається значення тільки від
переданих параметрів. p>
IsPrecise p>
Чи виконує функція
округлення в процесі роботи. p>
Таблиця 3. p>
У нашому випадку ні до яких даними доступу не
відбувається, що повертає значення залежить тільки від переданого параметра, і
значення є точним, а не наближеним. p>
ПРИМІТКА p>
Це дозволяє використовувати цю функцію
в максимально широкому контексті - наприклад, можна створити обчислювані колонку
на її основі, і навіть індекс по цій колонці. Це може бути корисно для
сортування, наприклад, списку одержувачів e-mail. Сортування за зверненому
адресою поставить поруч адреса в одному домені, і можна буде оптимізувати
розсилання листів. p>
Повертаємо ISqlReader
p>
У багатьох випадках таблична функція виконує роль
параметризрвані view - дані беруться з таблиць, і, після застосування
операторів SQL до вихідних даних і параметрів, результат повертається в
викликає код. Створимо функцію, яка буде повертати список змін
курсу валют, проізшедшіх в заданому діапазоні дат: p>
[SqlFunc (TableDefinition = "D datetime, course decimal (10,
4 )")] p>
[SqlFunction (DataAccess = DataAccessKind.Read, p>
SystemDataAccess =
SystemDataAccessKind.None, p>
IsDeterministic = false,
IsPrecise = true)] p>
public static ISqlReader GetCourseChanges (DateTime start, DateTime
end) p>
( p>
SqlCommand cmd =
SqlContext.GetCommand (); p>
cmd.CommandText = @ " p>
select changeDate, course
from Course p>
where changeDate between
@ start and @ end "; p>
cmd.Parameters.AddWithValue ( "@ start", start); p>
cmd.Parameters.AddWithValue ( "@ end", end); p>
return cmd.ExecuteReader (); p>
) p>
ПОПЕРЕДЖЕННЯ p>
На жаль, поки що мені не
вдалося змусити цей приклад працювати. Сервер неухильно повертає помилку
«Reader is closed». Яким чином уникнути закриття Reader після повернення
його серверу, я поки не зрозумів. p>
Працюємо з SqlResultSet
p>
Для тих випадків, коли необхідно сформувати
повертається набір даних вручну, передбачений доступ до нього через метод
контексту SqlContext.GetReturnResultSet (). Об'єкт, що повертається цим методом,
вже ініціалізованим першим відповідно до декларованої структурою функції. У
нього слід додати необхідні записи. У принципі, можна як додавати, так і
видаляти/змінювати записи, якщо це здається необхідним. Відтворимо поведінка
збереженої процедури CurrencyCourse, створеної в кінці попереднього розділу: p>
[SqlFunc (TableDefinition = "D datetime, course decimal (10, 4)
NULL ")] p>
[SqlFunction (DataAccess = DataAccessKind.Read, p>
SystemDataAccess =
SystemDataAccessKind.None, p>
IsDeterministic = false,
IsPrecise = true)] p>
public static void GetCourseTable (DateTime start, DateTime end) p>
( p>
using (SqlCommand cmd =
SqlContext.GetCommand ()) p>
( p>
cmd.CommandText = @ " p>
select changeDate, course
from Course p>
where changeDate between
@ start and @ end "; p>
cmd.Parameters.AddWithValue ( "@ start", start); p>
cmd.Parameters.AddWithValue ( "@ end",
end); p>
DateTime current = start; p>
SqlDecimal course =
SqlDecimal.Null; p>
SqlResultSet source =
cmd.ExecuteResultSet (ResultSetOptions.None); p>
SqlResultSet dest =
SqlContext.GetReturnResultSet (); p>
SqlDataRecord rec; p>
while (source.Read ()) p>
( p>
while (current <
source.GetDateTime (0)) p>
( p>
rec =
dest.CreateRecord (); p>
rec.SetSqlDecimal (1,
course); p>
rec.SetDateTime (0,
current); p>
dest.Insert (rec); p>
current =
current.AddDays (1); p>
) p>
course = source.GetDecimal (1); p>
) p>
while (current <= end) p>
( p>
rec = dest.CreateRecord (); p>
rec.SetDateTime (0,
current); p>
rec.SetSqlDecimal (1,
course); p>
dest.Insert (rec); p>
current =
current.AddDays (1); p>
) p>
) p>
) p>
Зверніть увагу, що тепер в атрибуті SqlFunction
міститься значення властивості DataAccess = DataAccessKind.Read, вказуючи на те,
що функція читає дані з бази. p>
ПОПЕРЕДЖЕННЯ p>
Зверніть увагу також на те, що на
Цього разу для доступу до даних ми використовуємо SqlResultSet замість
SqlDataReader. Справа в тому, що одночасно читати з бази і працювати з
що повертається набором записів можна - виникає виключення з повідомленням про
те, що дане з'єднання вже використовується. Можливо, ця особливість
поведінки буде змінена при випуску фінальної версії. Але поки що єдиним
способом написати подібну функцію є читання даних цілком до початку
формування вихідного набору даних. p>
агрегуються функції
p>
Більшості розробників для побудови своїх додатків
цілком вистачає стандартного набору агрегуються функцій. Однак тепер настав
свято і для рідкісних любителів зробити щось незвичайне - у новому MS SQL
Server можна реалізувати свій спосіб вийти за межі SUM, AVG і СOUNT. P>
Створюються вони за допомогою оператора CREATE AGGREGATE: p>
CREATE AGGREGATE [schema_name. ] Aggregate_name p>
(@ param_name <
input_sqltype>) p>
RETURNS p>
EXTERNAL NAME assembly_name [: class_name] p>
::= p>
system_scalar_type | ([
udt_schema_name. ] Udt_type_name) p>
::= p>
system_scalar_type | ([
udt_schema_name. ] Udt_type_name) p>
Цього разу написання одного методу недостатньо.
Замість цього для підрахунку агрегатів використовуються об'єкти. Ідея проста - у міру
перегляду вихідних даних ми накопичуємо те, що потрібно накопичувати, а зетем
виводимо накопичене у вихідний набір. Відповідно для кожного з цих
дій потрібно реалізувати за методом: p>
Назва методу p>
Опис p>
public void Init () public void Init (input_type value) p>
ініціалізує об'єкт.
Викликається один раз на групу агрегіруемих значень. Якщо реалізована версія
методу з одним параметром, то SQL Server може використовувати її для передачі
перше значення в групі. Тип параметра value (input_type) повинен бути
сумісним з тим типом, яка зазначена як input_sqltype в операторові CREATE
AGGREGATE. P>
public void Accumulate (input_type value) p>
Після ініціалізації
об'єкта, сервер викликає цей метод по одному разу для кожного агрегіруемого
значення. (На список що подаються на вхід значень, крім складу полів у
операторі GROUP BY, впливає також і наявність ключового слова
distinct перед агрегіруемим виразом. Як і для вбудованих функцій, це
ключове слово призведе до того, що в список для кожної групи потраплять тільки
різні значення агрегіруемого вирази). Тип параметра value повинен бути
сумісним з тим типом, яка зазначена як input_sqltype в операторові CREATE
AGGREGATE. P>
public return_type
Terminate () p>
Незважаючи на страшне
назву, цей метод всього лише повинен повернути те саме агреговане
значення, що було обчислено для групи вхідних значень. Тип результату
повинен бути сумісним з тим типом, яка зазначена як return_sqltype в
операторі CREATE AGGREGATE. p>
public void Merge (udagg_type group) p>
Цей метод призначений для
випадків, коли SQL Server створює більше одного агрегує об'єкта на одну
групу вхідних значень. Наприклад, при виконанні запиту на
багатопроцесорної машині, вхідні дані можуть бути розділені на кілька
потоків для одночасної обробки. Перед висновком даних необхідно
виконати злиття розрахованих агрегатних значень. Саме це і робить цей
метод. Він приймає єдиний параметр того ж класу, в якому оголошено. P>
Таблиця 4. p>
Крім цих методів, у класу повинен бути визначений
конструктор без аргументів (інакше SQL Server не зможе створювати об'єкти цього
класу). Крім того, повинна бути забезпечена можливість сериализации об'єктів --
для випадків, коли серверу потрібно зберегти проміжний результат на диск. Ми
відкладемо опис подробиць сериализации до наступного розділу, а поки що
спробуємо зробити свою функцію для обчислення середнього геометричного. p>
Тим, хто погано пам'ятає шкільний курс, нагадаю, що
середнє геометричне з N чисел - це корінь N-ною мірою через їхні твори.
(А середнє арифметичне N чисел, що звичайно і мається на увазі під
терміном «середнє значення» - це сума цих чисел, поділена на N). p>
ПРИМІТКА p>
На жаль, розрахунок середнього
геометричного за визначенням дуже швидко призводить до переповнення навіть на
дуже невеликих наборах вхідних даних - твір росте дуже швидко.
Тому ми схитрував і скористаємося тим математичним фактом, що
твір N чисел одно експоненті від суми їх логарифмів. Замість
витягання кореня ступеня N (а це те ж саме, що і зведення в ступінь
1/N) ми поділимо на N суму логарифмів перед застосуванням функції Exp (). p>
[Serializable] p>
[SqlUserDefinedAggregate (Format.Native, IsInvariantToDuplicates =
false, IsInvariantToNulls = true, IsInvariantToOrder = true, IsNullIfEmpty =
true)] p>
[StructLayout (LayoutKind.Sequential)] p>
public class AvgGeom: INullable p>
( p>
private double _agg; p>
private int _count; p>
private bool _isNull = true; p>
# region User-Defined Attribute
Required Methods p>
public void Init () p>
( p>
_agg = 0; p>
_count = 0; p>
_isNull = true; p>
) p>
public void
Accumulate (SqlDouble Value) p>
( p>
if (! Value.IsNull) p>
( p>
_agg + =
System.Math.Log (Value.Value); p>
_count ++; p>
_isNull = false; p>
) p>
) p>
public void Merge (AvgGeom
Group) p>
( p>
if (! Group.IsNull) p>
( p>
_agg + = Group._agg; p>
_count + = Group._count; p>
_isNull = false; p>
) p>
) p>
public SqlDouble Terminate () p>
( p>
if (IsNull) p>
return SqlDouble.Null; p>
else p>
return new
SqlDouble (System.Math.Exp (_agg/_count )); p>
) p>
# endregion p>
# region INullable Members p>
public bool IsNull p>
( p>
get p>
( p>
return _isNull; p>
) p>
) p>
# endregion p>
) p>
В першу чергу звернемо увагу на атрибут
SqlUserDefinedAggregate, що передує опис нашого класу. У ньому
визначено кілька параметрів (таблиця 5). p>
Ім'я параметру p>
Опис p>
Format p>
Формат сериализации
об'єктів цього класу. Подробиці - в наступному розділі. P>
MaxByteSize p>
Максимальний розмір
серіалізованного об'єкта. Подробиці - в наступному розділі. P>
IsInvariantToDuplicates p>
Чи залежить агреговане
значення від наявності дублікатів у вхідних даних (за замовчуванням - так). Наприклад,
для функції MIN () зовсім неважливо, скільки разів повторюються вхідні
значення, а для функції SUM () - важливо. Оптимізатор запитів SQL Server може
використовувати цю інформацію для мінімізації кількості викликів методу
Accumulate. P>
IsInvariantToNulls p>
Чи впливає наявність
NULL-значень у вхідних даних на агреговане значення. Для більшості
вбудованих агрегуються функцій (крім COUNT ()) це так. p>
IsNullIfEmpty p>
означає, що агрегуються
функція повертає NULL для порожніх вхідних наборів. Наприклад, функція MIN при
виконання на порожньому наборі повертає якраз NULL, а функція COUNT () - 0. p>
IsInvariantToOrder p>
Цей параметр поки не
документований; судячи з назви, він повинен визначати, чи впливає на
результат порядок надання значень в метод Accumulate (). Див примітку після
таблиці p>
Таблиця 5. p>
ПОПЕРЕДЖЕННЯ p>
Всі вбудовані агрегуються функції (а
також наш приклад) є комутативність, що дозволяє серверу вибирати
порядок сканування вхідних даних на свій розсуд. Однак, наприклад,
результат функцій типу First () або Last (), (доторие повинні повертати
відповідно перше або останнє значення в наборі), очевидним чином
залежить від порядку вхідних значень. Тим не менш, поки незрозуміло, як можна
використовувати подібні функції - справа в тому, що синтаксис SQL не дозволяє
визначати порядок агрегування записів. Оператор ORDER BY застосовується лише для
вихідного набору записів, і використовувати в ньому можна тільки ті поля, за
яким виконується угрупування. У звичайних вкладених запити (за результатами
яких можна будувати запити з угрупованням) застосування ORDER BY заборонено.
Швидше за все (це тільки моє припущення!) Розробники MS SQL Server Yukon
передбачають використовувати властивість
SqlUserDefinedAggregateAttribute.IsInvariantToOrder для тих випадків, коли
програміст будь-яким чином все ж таки може гарантувати певний
упорядкування вхідних даних - це властивість має переконати сервер
утриматися від переупорядочіванія записів перед агрегування. Поки що мені
не вдалося виявити будь-якого впливу цієї властивості на поведінку
сервера. p>
Для того, щоб наш об'єкт міг приймати значення
NULL, необхідно реалізувати інтерфейс INullable. Цей інтерфейс визначає
єдине read-only властивість bool IsNull. Всі класи з System.Data.SqlTypes
реалізують цей інтерфейс. У нашому прикладі об'єкт приймає значення NULL при
ініціалізації, і перестає бути Null відразу, як тільки йому буде передано
не-NULL значення в метод Accumulate або Merge. p>
Користувацькі типи даних
p>
Систему типів SQL Server можна розширити за допомогою
користувацьких типів даних (User-defined Types, UDT). Користувацькі типи
реалізуються як керований клас на будь-якому з CLR-мов і реєструються в SQL
Server. Такий тип можна використовувати для визначення типу колонки в таблиці,
або як змінну (параметр процедури) у виразі Т-SQL. При цьому методи
об'єктів можна викликати прямо з T-SQL. p>
Створення користувацького типу даних
p>
В T-SQL призначений для користувача тип даних реєструється при
допомогою оператора CREATE TYPE: p>
CREATE
TYPE [type_schema_name. ] Type_name p>
([
FROM base_type [(precision [, scale]) | ( 'urn: schema-namespace')] p>
[NULL | NOT NULL]] p>
| [EXTERNAL NAME [assembly_schema_name. ]
assembly_name [: class_name]] p>
) p>
У операторі вказується ім'я класу із заздалегідь
завантаженої в базу збірки. p>
Альтернативою прямого використання T-SQL, як і в
інших випадках, служить автоматичне розгортання проектів MS Visual Studio
. Net Whidbey. Класи, помічені атрибутом SqlUserDefinedType (ми докладно
розглянемо його трохи пізніше - під час обговорення сериализации) автоматично
реєструються як користувацьких типів при розгортанні проектів
типу SQL Server Project. p>
Для того, щоб клас. NET можна було використовувати в
як для користувача типу даних SQL Server, він повинен виконувати
деякі обов'язки: p>
Мати конструктор без параметрів. Як правило, він
повертає екземпляр, що відповідає значенню NULL (про це далі). p>
Підтримувати NULL-значення. Клас повинен реалізовувати
інтерфейс INullable, який описаний у попередньому розділі. Також необхідна
реалізація в класі статичної властивості Null, Котор