Customizando o SQLCLR

Uma das maiores novidades (e aparentemente pouco utilizada/comentada) que o SQL Server 2005 trouxe em conjunto com o .NET Framework 2.0, foi a capacidade de escrevermos Stored Procedures, Triggers e Functions em código gerenciado, ou seja, VB.NET ou C#, como dito aqui.

A idéia por trás da criação destes objetos em .NET é justamente fornecer ao SQL Server, acesso a recursos que são extremamente difícieis em uma linguagem de banco, garantindo também que outras aplicações (não gerenciadas) possam fazer o acesso as mesmas Stored Procedures sem pagar pelo preço do COM Interop. Sendo assim, dentro de uma Stored Procedure, antes de retornar os dados para o chamador, voce pode customizar o result-set, definindo os campos que achar conveniente e, além disso, pode aplicar a manipulação que desejar.

Imagine o seguinte cenário: voce tem uma coluna na sua tabela e, dentro dela, é necessário guardar um relatório. Dependendo do tamanho do relatório e da quantidade de registros, voce poderá “inchar” a sua base de dados rapidamente; sendo assim, voce poderia recorrer a compactação fornecida pelo .NET 2.0 e, antes de guardar ou antes de exibir, aplicar essa (des)compactação. A idéia do código abaixo é justamente, antes de retornar o result-set para o cliente, customizar a sua saída, ou seja, não é necessário retornar exatamente os dados provenientes da query que voce faz internamente.

[SqlProcedure]
public static void ResgatarConsultas()
{
    using (SqlConnection conn = new SqlConnection(“context connection=true”))
    {
        string query = “SELECT Data, Relatorio FROM Tabela”;

        using (SqlCommand cmd = new SqlCommand(query, conn))
        {
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                SqlDataRecord record =
                    new SqlDataRecord(
                        new SqlMetaData(“Data”, SqlDbType.DateTime),
                        new SqlMetaData(“Relatorio”, SqlDbType.Text));

                SqlContext.Pipe.SendResultsStart(record);

                while (dr.Read())
                {
                    if (!dr.IsDBNull(0) && !dr.IsDBNull(1))
                    {
                        record.SetDateTime(0, dr.GetDateTime(0));
                        record.SetString(1,
                            Encoding.Default.GetString(IO.Decompress((byte[])dr.GetValue(1))));

                        SqlContext.Pipe.SendResultsRow(record);
                    }
                }

                SqlContext.Pipe.SendResultsEnd();
            }
        }
    }
}

Se repararmos no código acima, não há nada de novidade, pois utilizamos as mesmas classes do ADO.NET em uma aplicação tradicional. A única diferença é que não vamos expor o result-set da forma em que ele é retornado pela query interna; antes disso, será necessário passar pelo algoritmo de descompactação.

A classe SqlDataRecord representa uma linha de metadados; é através dela que vamos construir a estrutura do result-set. O construtor recebe um array de objetos do tipo SqlMetadata onde, em cada uma delas, precisamos especificar o nome e o tipo de dado do campo. Feito isso, recorremos ao método SendResultsStart passando a instancia da classe SqlDataRecord para caracterizar o ínicio do retorno para o cliente.

Uma vez que isso está pronto, utilizamos o tradicional SqlDataReader para percorrer os registros internos (retornados pelo query) e definirmos os valores fornecidos por ele para o SqlDataRecord. Note que vinculamos cada coluna do SqlDataReader a uma determinada coluna do SqlDataRecord, fazendo isso através de métodos do tipo SetXXX, onde XXX deve ser substituído pelo tipo de dado a ser carregado. O primeiro parametro de métodos SetXXX é um número inteiro que representa a coluna (do result-set) em que uma determinada coluna do SqlDataReader será carregada. Fazemos esse processo por cada iteração do SqlDataReader e, dentro deste loop, passamos o SqlDataRecord para o método SendResultsRow que, por sua vez, envia a linha para o cliente.

Finalmente, para encerrar o processamento, invocamos o método SendResultsEnd para informar ao chamador que o result-set está finalizado.

ManualResetEvent vs. AutoResetEvent

Ambas as classes que são temas do post tem basicamente a mesma finalidade, ou seja, efetuar a sinalização entre threads. Isso permite que uma determinada thread notifique as outras threads que ela finalizou ou liberou um determinado recurso e, conseqüentemente, essas outras threads poderão dar seqüencia na execução.

Leve em consideração o seguinte código:

delegate void Executor();

private static XXX _resetEvent = new XXX(false);

static void Main(string[] args)
{
    new Executor(Teste1).BeginInvoke(null, null);
    new Executor(Teste2).BeginInvoke(null, null);
    new Executor(Teste3).BeginInvoke(null, null);

    Console.WriteLine(“Fim”);
    Console.ReadLine();
}

static void Teste1()
{
    Console.WriteLine(“Teste1 – Antes”);

    Thread.Sleep(5000); //Simula um processamento pesado
    _resetEvent.Set();
   
    Console.WriteLine(“Teste1 – Depois”);
}

static void Teste2()
{
    Console.WriteLine(“Teste2 – Antes”);
    _resetEvent.WaitOne();
    Console.WriteLine(“Teste2 – Depois”);
}

static void Teste3()
{
    Console.WriteLine(“Teste3 – Antes”);
    _resetEvent.WaitOne();
    Console.WriteLine(“Teste3 – Depois”);
}

Resultados para quando XXX = ManualResetEvent
Fim
Teste1 – Antes
Teste2 – Antes
Teste3 – Antes
Teste1 – Depois
Teste3 – Depois
Teste2 – Depois

Resultados para quando XXX = AutoResetEvent
Fim
Teste1 – Antes
Teste2 – Antes
Teste3 – Antes
Teste1 – Depois
Teste3 – Depois

Como sabemos, o método BeginInvoke fornecido pela instancia do delegate permite a chamado para o método que ele aponta de forma assíncrona, ou seja, será criada uma worker thread para cada um deles; sendo assim, os processos acontecerão paralelamente e o reset event está aqui para garantir a sincronização das informações, ou melhor, destes métodos.

Quando utilizamos o método WaitOne do ManualResetEvent nos métodos 2 e 3, eles aguardarão um sinal que, por sua vez, será dado através do método Set, também do ManualResetEvent. Enquanto isso não acontecer, os métodos 2 e 3 não darão sequencia no processamento. Se repararmos o resultado final, as mensagens (writelines) que a aparecem depois do método WaitOne retornará, isso quer dizer que ele ficará “travado” até receber o sinal para prosseguir. Uma vez recebido, todos aqueles que estiveremos pendentes serão executados.

Já com um AutoResetEvent, o processo é muito semelhante, com a exceção que quando o método Set for chamado, apenas uma thread da fila será executada. Isso explica o motivo pelo qual a mensagem do método 2 (“Teste2 – Depois“) não aparece no resultado final.

Bem, isso é uma das N possibilidades que o .NET fornece para efetuar a sincronização através de sinalização. Para aqueles que querem se aprofundar, vejam o namespace System.Threading.