четверг, 21 мая 2009 г.

Использование CLR-функцийв Microsoft SQL Serverна примере сжатия данных

Использование CLR-функцийв Microsoft SQL Serverна примере сжатия данных

Начиная с версии 2005, Microsoft SQL Server позволяетразрабатывать хранимые процедуры и функции не только на T-SQL, но и на разных языках платформы .NET. В этой статье мы рассмотримсоздание функций для сжатия данных в MS SQL.

Данный подход я использовал в своей системе поиска плагиата “The Crot” (http://siberiasoft.info), чтобы немного обойтиограничение Express Edition на размер базы данных (4 GB).

Примечание

В MS SQL 2008 появились возможности сжатия данных и резервныхкопий, возможно вам подойдет и этот вариант. Подробности как всегда в BOL  http://msdn.microsoft.com/en-us/library/cc280449.aspx.

Для сжатия данных в этом примере мы будем использоватьбесплатную open source библиотеку SharpZipLib, которую можно загрузить по адресу – http://www.icsharpcode.net/OpenSource/SharpZipLib/.

Итак, начнем. Запустите Visual Studio и создайте новый проект Class Library:

 

Далее, переименуйте созданный по умолчанию класс Class1 в CompressionFunctions, и пометьте егомодификатором static.

using System;

using System.Collections.Generic;

using System.Text;

 

namespace CLRFunctions

{

    public static class CompressionFunctions

    {

 

    }

}

Распакуйте библиотеку SharpZipLib, добавьте еев решение (File->Add->Existing Project…), и добавьте ссылку на этот проект в проект CLRFunctions (Project->Add reference…, вкладка Projects).

Теперь создадим в классе Compression методы для упаковки ираспаковки данных. Эти методы будут соответствовать скалярным функциям MS SQL. Для того чтобыпревратить обычный метод в CLR-функцию,надо всего лишь пометить его атрибутом SqlFunctionAttribute. Этот атрибут находится в пространстве имен Microsoft.SqlServer.Server, так что для удобства добавьтеего в список using.

Рассмотрим метод для упаковки данных. Этот метод будетпринимать на вход произвольные двоичные данные, упаковывать их, и возвращатьопять же двоичные данные. На сервере эти данные будут храниться в столбце типа varbinary(max), который в .NET представлентипом SqlBytes (которыйнаходится в пространстве имен System.Data.SqlTypes, так что добавьте и его всписок using). Полнуютаблицу соответствия типов MS SQL типам .NET CLR можно найти в MSDN в статье SQL Server Data Types andTheir .NET Framework Equivalents.

Итак, метод будет выглядеть следующим образом:

[SqlFunctionAttribute()]

public static SqlBinary ZipData(SqlBinarydata)

{

 

}

Теперь само сжатие. SharpZipLib содержит класс DeflaterOutputStream, которыйумеет упаковывать потоки:

[SqlFunctionAttribute()]

public static SqlBinary ZipData(SqlBinarydata)

{

    //Создаем поток впамяти,

    //куда положимданные из параметра data

    MemoryStreamcompress = new MemoryStream();

 

    //Собственнообъект, который инкапсулирует алгоритм сжатия,

    //конструкторпринимает два параметра - уровень сжатия,

    //и добавлять илинет заголовок zip-архива,

    //true - недобавлять, нам он не нужен

    Deflater deflater =new Deflater(Deflater.BEST_COMPRESSION, true);

 

    //Создаем сжимающий поток

    DeflaterOutputStream zos = new DeflaterOutputStream(compress,deflater);

 

    //Записываемв сжимающий поток данные из параметра data

    zos.Write(data.Value,0, data.Value.Length);

    zos.Close();

 

    //Преобразуемсжатый поток в массив байт и возвращаем как SqlBinary

    return new SqlBinary(compress.ToArray());

}

Распаковка также достаточно просто – для этого нампотребуется класс InflaterInputStream:

[SqlFunction()]

public static SqlBinary UnZipData(SqlBinarydata)

{

    //Создаем поток впамяти, содержащий упакованные данные

    //на основании параметра data

    MemoryStream uncompressed = new MemoryStream(data.Value);

 

    //Объект,инкапсулирующий алгоритм распаковки,

    //заголовка у наснет, так передаем true в конструктор

    Inflater inflater =new Inflater(true);

 

    //Создаем распаковывающий поток

    InflaterInputStream zis = newInflaterInputStream(uncompressed, inflater);

 

    //Промежуточныйбуфер для копирования данных из распаковывающего потока

    //врезультирующий поток, содержаший распакованные данные

    byte[] buff = newbyte[4096];

    MemoryStream result = newMemoryStream();

 

    //Копируемданные в результирующий поток

    StreamUtils.Copy(zis,result, buff);

 

    //Преобразуемраспакованный поток в массив байт и возвращаем как SqlBinary

    return new SqlBinary(result.ToArray());

}

На этом этапе откомпилируйте проект, и давайте посмотрим,как зарегистрировать и использовать эти функции в MS SQL. Откройте Microsoft SQL Server Management Studio исоздайте новую БД для экспериментов.

Чтобы можно было использовать приведенные выше функции вбазе данных, сначала необходимо зарегистрировать сборку, полученную врезультате компиляции проекта. Это можно сделать CREATE ASSEMBLY, в моем случае этовыглядит так:

use CLRFunctions

go

alter database CLRFunctions set trustworthy on

go

create assembly CLRFunctions

from 'C:\Work\Tutorials\CLRFunctions\bin\Debug\CLRFunctions.dll'

with permission_set = unsafe

go

В этом сценарии мы сначала изменяем настройки trustworthy базыданных на on, чтобы можно было зарегистрировать «небезопасную» сборку ICSharpCode.SharpZipLib.dll, на которую ссылается наша сборка CLRFunctions. Без этого SQL Server непозволит зарегистрировать сборку.

После этого необходимо создать в БД функции-обертки длянаших CLR-функций,используя следующий сценарий:

create function dbo.unzip_data(@data varbinary(max))

returns varbinary(max)

as

external name CLRFunctions.[CLRFunctions.CompressionFunctions].UnZipData

go

 

create function dbo.zip_data(@data varbinary(max))

returns varbinary(max)

as

external name CLRFunctions.[CLRFunctions.CompressionFunctions].ZipData

go

Внешнее имя функции должно указываться в виде <Имя сборки>.[<Пространствоимен>.<Имя класса>].<Имя метода>.

Давайте протестируем работу функций сначала простымсценарием:

select cast  (

                           dbo.unzip_data(

                                  dbo.zip_data(

                                        cast('Some really stupid test data' as varbinary(max))

                                  )

                           ) as varchar(max))

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

И более замороченным сценарием на случайных данных:

declare @test_compression table (

       data   varbinary(max)

)

 

insert into @test_compression     (

       data

)

select top 1000

             cast(

                    replicate(

                           cast(newid() as varchar(36)), abs(checksum(newid())) % 5000 + 5000

                    ) as varbinary(max)

             )

from master.sys.objects s1,master.sys.objectss2, master.sys.objects s3

 

select              sum(datalength(data)) as uncompressed

             ,      sum(datalength(dbo.zip_data(data))) as compressed

             ,      cast(

                    (sum(datalength(dbo.zip_data(data))) * 1.0 / sum(datalength(data))) * 100

                           as numeric(5, 2)

                    ) as ratio

from @test_compression

go

                Важное примечание

                                Не забывайте перерегистрироватьсборку и функции после изменения сборки. Можете                          добавить в начало сценария регистрациичто-то вроде:

drop functiondbo.unzip_data

drop function dbo.zip_data

drop function dbo.split_text

drop assemblyCLRFunctions

Еще примечание

Тестируйте,прежде чем использовать в эксплуатационной среде.

Бонус – создание табличной CLR-функции

Функции, созданные выше, представляли собой простыескалярные функции – получаем на вход данные, возвращаем один результат. Помимоскалярных функций можно создавать и табличные CLR-функции. Давайте рассмотрим это напростом примере – создадим функцию, которая принимает на вход текст, разбиваетего на слова в соответствии с переданными разделителями, и возвращает слова ввиде таблицы.

Вернитесь в Visual Studio и откройте проект, содержащий функции сжатия и распаковки.Добавьте в проект новый класс, назовите его TextFunctions и пометьте модификаторами public иstatic:

namespace CLRFunctions

{

    public static class TextFunctions

    {

 

    }

}

Основное отличие табличных CLR-функций в том, что они возвращаютинтерфейс IEnumerable и для них необходимо указывать имя метода, который будетзаполнять и материализовывать отдельные записи:

[SqlFunction(FillRowMethodName= "SplitTextFillRow")]

public static IEnumerable SplitText(stringtext, string separators)

{

    //Разбиваем текстна элементы в соответствии с переданными разделителями

    string[] items =text.Split(separators.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

 

    //Этотмассив нужен для преобразования типа string к типу CLR SqlChars

    SqlChars[] ichars =new SqlChars[items.Length];

 

    //Преобразовываем элементы к типу SqlChars

    for (int i = 0; i< items.Length; i++)

    {

        ichars[i]= new SqlChars(items[i]);

    }

    return ichars;

}

 

//Этотметод заполняет и материализует одну строку набора данных

public static void SplitTextFillRow(objecto, out SqlCharsitem)

{

    item = (SqlChars)o;

}

Скомпилируйте и зарегистрируйте сборку, зарегиструйтефункцию, и протестируйте ее работу:

select *

from dbo.split_text(

                    'Some really stupid test data. Yeah, really stupid!'

             ,      ' . ,')

 

That’s all,folks! Sai gobe!

Приложение – сценарий регистрации/перерегистрации сборки и функций

use CLRFunctions

go

drop function dbo.unzip_data

drop function dbo.zip_data

drop function dbo.split_text

drop assembly CLRFunctions

go

alter database CLRFunctions set trustworthy on

go

 

create assembly CLRFunctions

from 'C:\Work\Tutorials\CLRFunctions\bin\Debug\CLRFunctions.dll'

with permission_set = unsafe

go

 

create function dbo.unzip_data(@data varbinary(max))

returns varbinary(max)

as

external name CLRFunctions.[CLRFunctions.CompressionFunctions].UnZipData

go

 

create function dbo.zip_data(@data varbinary(max))

returns varbinary(max)

as

external name CLRFunctions.[CLRFunctions.CompressionFunctions].ZipData

go

 

create function dbo.split_text(@text nvarchar(max), @separators nvarchar(128))

returns table (item nvarchar(max))

as

external name CLRFunctions.[CLRFunctions.TextFunctions].SplitText

go