LINQ TO SQL - Turbo Web Apps usando Compiled Queries
Para muitos não é novidade que podemos atingir ganhos de performance muito superiores utilizando as compiled queries em LINQ. Claro que isso não é verdade para todos e espero que para todos esses que estejam a ler este Post possam aprender algo,
Antes de mais gostava de explicar o porquê de estar a escrever sobre este tópico. Esta semana decidi dar uma olhadela na performance das páginas do trokas, uma vez que tenho estado a implementar novas funcionalidades, também aproveitei para fazer alguns cleanups (tanto de visual, como de código). Ao que decidi olhar também para o tempo de render das minhas página. Ora como portal que é o trokas tem dezenas de ligações à base de dados por página e eu que algures no tempo achei tão fixe a possibilidade de usar LINQ para não mais ter trabalho de escrever as consultas de SQL à mão, nunca mais olhei para a performance convencido de que o LINQ TO SQL optimizava toda aquela confusão que eu largava para ali.
Ao olhar então para o tempo de render de uma determinada página observei que os tempos de render variavam entre os 0,5 segundos e os muitos segundos (desvantagens de um servidor partilhado). Até que me lembrei mas espera lá quando comecei o projecto as páginas demoravam menos de 0,1'' para fazer o render. Após vários testes (ainda em curso diga-se de passagem), cheguei à conclusão que os tempos excessivos deviam-se às ligações à base de dados. E como qualquer rookie lembrei-me que se calhar até era fixe ter um único contexto de ligação à base de dados estático pois talvez assim tivesse uma ligação sempre activa e não fosse penalizado pelas aberturas e fechos de ligações. Wronnggg. ok depois de muiiitos testes e muitas reviravoltas acabei por perceber que simplesmente não é viável devido ao caching interno que é feito pelos objectos de LINQ.
À segunda tentativa lembrei-me de fazer testes com Stored Procedures e voilá que coisa tão rápida que até parecia mentira. Já estava todo contente e lixado ao mesmo tempo. Do género "no more linq to sql for me" (mais ou menos estava a colocar as stored procedures no DBML).
Claro que a história não ficou por aqui e então decidi pesquisar mais um pouco sobre como melhorar a performance das consultas de LINQ. Et voilá. Afinal existe uma coisa fixe, porreira, e até tem um nome porreiro, Compiled Queries. Ou seja, posso compilar as queries que são o verdadeiro terror da performance? Sim. Mas não há bela sem senão.
As compiled Queries
Uma Consulta Compilada em Linq básicamente servem para tirar o overhead de recompilar uma consulta em cada pedido ao servidor. Para isso é necessário criar um método estático na nossa classe (ou de preferência numa classe separada para manter o código mais limpo) utilizando o tipo FUNC.
A sintaxe é a seguinte:
public static Func<MySiteDataContext, int, Artigo> GetArtigo=
CompiledQuery.Compile((MySiteDataContext db, int artigoId) =>
(from a in db.Artigos
where a.artigoId == artigoId
select a).FirstOrDefault()
);
Neste caso que apresentei só retorna um artigo e como tal estamos a dizer na declaração que o tipo a devolver é um Artigo, mas fácilmente podemos devolver um iQueryable do tipo artigo.
Mas claro que isto é muito bonito só que vem logo outra bomba atrás... Mas e os tipos anónimos? Também tem solução. Um pouco mais de trabalho mas tem solução.
As Compiled Queries que devolvem um tipo anónimo
Agora já devem estar a pensar isto é uma brisa nem dá trabalho nenhum. Mas deparem-se com a necessidade de usar tipos anónimos e digam lá que não desejavam não ser programadores? Ok sim desejavam.
Mas infelizmente para devolver uma compiled query com um tipo anónimo só dá se a usarmos no contexto em que a estamos a aplicar (o que deixa de ter sentido pois não beneficiamos de uma consulta compilada em todos os pedidos), ou temos de criar o tipo de objecto a ser devolvido para podermos indicar o mesmo. Assim sendo criamos primeiro uma classe com as propriedades que devem ser devolvidas pelo que seria o tipo anónimo. Por exemplo:
public sealed class NovoTipoDeArtigo
{
public int artigoId {get;set;}
public string artigo {get;set;}
public string custoDoArtigoComIva{get;set;}
}
E agora já podemos dizer ao Delegate qual o tipo de objecto a devolver:
public static Func<MySiteDataContext, int, iQueryable<NovoTipoDeArtigo>> GetArtigo=
CompiledQuery.Compile<MySiteDataContext, int, iQueryable<NovoTipoDeArtigo>>((MySiteDataContext db, int artigoId) =>
from a in db.Artigos
where a.artigoId == artigoId
select new NovoTipoDeArtigo {
artigoId = a.artigoId,
artigo = a.artigo,
custoDoArtigoComIva = a.preço * db.Impostos.Where(x=> x.imposto == "IVA").FirstOrDefault().valor,
}
);
Para quem teve paciência para ler isto tudo até ao fim e até percebeu mais ou menos a pessegada que eu coloquei aqui, aposto que devem querer saber qual o ganho a nível de performance. Pois é, então o primeiro benefício é que a mesma página (que por sinal ainda não acabei de optimizar), já está a fazer um render mínimo de 0,07 segundos o que comparado com os 0,5 segundos e tal iniciais, equivale a algo como um ganho de muito mais de 200%.