Detalhes do uso de IQueryable


Como sabemos, a Microsoft incluiu nas linguagens .NET a capacidade de se escrever queries para interrogar coleções, de forma bem semelhante ao que fazemos quando desejamos trabalhar com alguma base de dados. Esse recurso ganhou o nome de LINQ, e várias funcionalidades e mudanças essas linguagens sofreram para conseguir acomodá-lo.

Para manipular um conjunto de informações, temos que ser capazes de executar duas tarefas: escrever/executar a query e iterar pelo resultado, caso ele exista. Parte disso já é suportado pelo .NET, através da interface IEnumerable<T>, que fornece um enumerador (representado pela interface IEnumerator<T>) para iterar por alguma coleção. A parte faltante (escrita) foi implementada também como uma interface e adicionada ao .NET Framework. Para isso, passamos a ter a interface IQueryable e IQueryable<T>. Com esses interfaces, somos capazes de expressar uma query através de uma AST (Abstract Syntax Tree), e mais tarde, traduzí-la para alguma fonte de dados específica.

Hoje temos os providers: LINQ To SQL, LINQ To XML e LINQ To Entities (Entity Framework). Cada um desses providers traduzem a query para uma fonte de dados específica, tais como: SQL Server (T-SQL), XML (XPath), ou até mesmo, Oracle (PL-SQL). A interface IQueryable<T> herda diretamente de IEnumerable<T>, para assim agregar a funcionalidade de iteração do resultado. A principal propriedade que ela expõe é a Expression, que retorna a instância da classe Expression, utilizada como raiz da expression tree. A imagem abaixo ilustra a hierarquia entre elas:

Existem duas classes dentro do assembly System.Core.dll, debaixo do namespace System.Linq, e que se chamam: Enumerable e Queryable. São duas classes estáticas que, via extension methods, agregam funcionalidades aos tipos IEnumerable<T> e IQueryable<T>, respectivamente. Grande parte dessas funcionalidades (métodos), refletem operadores para manipulação da query, tais como: Where, Sum, Max, Min, etc. A principal diferença entre essas duas classes é que enquanto a classe Enumerable recebe delegates como parâmetro, a classe Queryable recebe a instância da classe Expression (que envolve um delegate), que será utilizada para montar a query, e mais tarde, ser traduzida e executada pelo provider. Abaixo temos a assinatura do método Where para as duas classes:

namespace System.Data.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TSource> Where<TSource>(
            this IEnumerable<TSource> source, Func<TSource, bool> predicate)
        {
            //…
        }
    }

    public static class Queryable
    {
        public static IQueryable<TSource> Where<TSource>(
            this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
        {
            //…
        }
    }
}

Ambas interfaces postergam a execução da query até o momento que você precisa realmente iterar pelo resultado. Da mesma forma, como IQueryable<T> herda de IEnumerable<T>, você pode definí-la como retorno de uma função, e mais tarde, iterar pelo resultado. Só que utilizar uma ou outra, tem um comportamento bastante diferente, que pode prejudicar a performance. Para exemplificar, vamos considerar um método que retorna as categorias da base de dados, e depois disso, vamos aplicar um filtro para retornar somente aquelas categorias cujo Id seja maior que 2. Com isso, escrevemos o seguinte código:

class Program
{
    private static DBTestesEntities ctx = new DBTestesEntities();

    static void Main(string[] args)
    {
        var query = RecuperarCategorias().Where(c => c.Id > 2);

        foreach (var item in query)
            Console.WriteLine(item.Descricao);
    }

    static IEnumerable<Dados> RecuperarCategorias()
    {
        return from c in ctx.Categorias select c;
    }
}

Repare que depois de executar o método RecurarCategorias, invocamos o método de estensão Where, e neste caso, fornecido pela classe Enumerable, já que o retorno da função é do tipo IEnumerable<T>. Ao compilar a aplicação, o compilador faz o parse da query LINQ e a transforma em dados, fazendo com que a query seja representada por objetos do tipo Expression. Abaixo temos o código decompilado, e podemos perceber que a expressão lambda que passamos para o método Where, está sendo utilizada para filtrar o resultado.

internal class Program
{
    private static DBTestesEntities ctx = new DBTestesEntities();

    private static void Main(string[] args)
    {
        IEnumerable<Categoria> query = RecuperarCategorias().Where<Categoria>(delegate (Categoria c) {
            return c.Id > 2;
        });

        foreach (Categoria item in query)
            Console.WriteLine(item.Descricao);
    }

    private static IEnumerable<Categoria> RecuperarCategorias()
    {
        ParameterExpression CS$0$0001;
        return ctx.Categorias.Select<Categoria, Categoria>(
            Expression.Lambda<Func<Categoria, Categoria>>(CS$0$0001 = Expression.Parameter(typeof(Categoria), “c”), 
            new ParameterExpression[] { CS$0$0001 }));
    }
}

O grande detalhe aqui é que as estensões para a interface IEnumerable<T>, recebem delegates como parâmetro, o que quer dizer que o filtro será aplicado do lado cliente, ou seja, ao invocar o método RecuperarCategorias, todas elas (categorias) serão retornadas, carregadas em memória, e depois aplicado o filtro, “descartando” aqueles elementos que não se enquadram no critério. Abaixo temos a query que chegou até o SQL Server:

SELECT
    [Extent1].[Id] AS [Id], 
    [Extent1].[Descricao] AS [Descricao]
FROM [dbo].[Categorias] AS [Extent1]

Agora, se o retorno da função RecuperarCategorias for do tipo IQueryable<Categoria>, o comportamento é totalmente diferente. O simples fato de alterar o tipo de retorno, é o suficiente para a compilação ser substancialmente diferente. No código abaixo temos esse código para ilustrar. Note que depois de invocar o método RecuperarCategorias, já temos o método Where na sequência. A diferença é que o método Where aqui é oriundo da classe Queryable, que ao invés de receber um delegate, recebe uma Expression, obrigando o compilador a reescrever o nosso código, transformando o filtro “c => c.Id > 2” em uma instância da classe Expression, expressando aquele mesmo critério.

internal class Program
{
    private static DBTestesEntities ctx = new DBTestesEntities();

    private static void Main(string[] args)
    {
        ParameterExpression CS$0$0000;
        IQueryable<Categoria> query = RecuperarCategorias().Where<Categoria>(
            Expression.Lambda<Func<Categoria, bool>>(
                Expression.GreaterThan(Expression.Property(CS$0$0000 = Expression.Parameter(typeof(Categoria), “c”), 
                (MethodInfo) methodof(Categoria.get_Id)), Expression.Constant(2, typeof(int))), 
            new ParameterExpression[] { CS$0$0000 }));

        foreach (Categoria item in query)
            Console.WriteLine(item.Descricao);
    }

    private static IQueryable<Categoria> RecuperarCategorias()
    {
        ParameterExpression CS$0$0001;
        return ctx.Categorias.Select<Categoria, Categoria>(
            Expression.Lambda<Func<Categoria, Categoria>>(CS$0$0001 = Expression.Parameter(typeof(Categoria), “c”), 
            new ParameterExpression[] { CS$0$0001 }));
    }
}

Apesar da ligeira mudança em tempo de compilação, isso permite o processamento remoto da query. Utilizar IQueryable<T> neste cenário, irá possibilitar o build-up de queries, ou seja, a medida que você vai adicionando critérios, operadores, etc., ele irá compondo a query principal e, finalmente, quando precisar iterar pelo resultado, apenas uma única query será disparada no respectivo banco de dados, já fazendo todo o filtro por lá, retornando menos dados. Isso pode ser comprovado visualizando a query que foi encaminhada para o banco de dados depois da alteração acima:

SELECT
    [Extent1].[Id] AS [Id], 
    [Extent1].[Descricao] AS [Descricao]
FROM [dbo].[Categorias] AS [Extent1]
WHERE [Extent1].[Id] > 2

Publicidade

2 comentários sobre “Detalhes do uso de IQueryable

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s