Usando IQueryable com Linq

257

Para que serve IQueryableno contexto do LINQ?

É usado para o desenvolvimento de métodos de extensão ou qualquer outro propósito?

user190560
fonte

Respostas:

508

A resposta de Marc Gravell é muito completa, mas pensei em acrescentar algo sobre isso do ponto de vista do usuário também ...


A principal diferença, da perspectiva do usuário, é que, quando você usa IQueryable<T>(com um provedor que suporta as coisas corretamente), você pode economizar muitos recursos.

Por exemplo, se você estiver trabalhando em um banco de dados remoto, com muitos sistemas ORM, terá a opção de buscar dados de uma tabela de duas maneiras, uma que retorne IEnumerable<T>e outra que retorne um IQueryable<T>. Digamos, por exemplo, você tem uma tabela Produtos e deseja obter todos os produtos cujo custo é> US $ 25.

Se você fizer:

 IEnumerable<Product> products = myORM.GetProducts();
 var productsOver25 = products.Where(p => p.Cost >= 25.00);

O que acontece aqui é que o banco de dados carrega todos os produtos e os transmite para o seu programa. Seu programa então filtra os dados. Em essência, o banco de dados faz um SELECT * FROM Productse retorna TODOS os produtos para você.

Com o IQueryable<T>provedor certo , por outro lado, você pode:

 IQueryable<Product> products = myORM.GetQueryableProducts();
 var productsOver25 = products.Where(p => p.Cost >= 25.00);

O código parece o mesmo, mas a diferença aqui é que o SQL será executado SELECT * FROM Products WHERE Cost >= 25.

Do seu POV como desenvolvedor, isso parece o mesmo. No entanto, do ponto de vista de desempenho, você só pode retornar 2 registros na rede em vez de 20.000 ....

Reed Copsey
fonte
9
Onde está a definição de "GetQueryableProducts ();"?
Pankaj
12
@StackOverflowUser É destinado a ser qualquer método que retorna um IQueryable<Product>- seria específico para o seu ORM ou repositório, etc.
Reed Copsey
certo. mas você mencionou a cláusula where após esta chamada de função. Portanto, o sistema ainda não tem conhecimento do filtro. Quero dizer, ele ainda busca todos os registros de produtos. certo?
Pankaj
@StackOverflowUser Não - essa é a beleza de IQueryable <T> - ela pode ser configurada para avaliar quando você obtém os resultados - o que significa que a cláusula Where, usada após o fato, ainda será traduzida em uma instrução SQL executada no servidor e puxar apenas os elementos necessários através do fio ...
Reed Copsey
40
@ Testes Na verdade, você ainda não está indo para o banco de dados. Até que você realmente enumere os resultados (por exemplo: use a foreachou call ToList()), na verdade você não atinge o banco de dados.
Reed Copsey
188

Em essência, seu trabalho é muito semelhante a IEnumerable<T>- para representar uma fonte de dados consultável - a diferença é que os vários métodos LINQ (ativados Queryable) podem ser mais específicos, para criar a consulta usando Expressionárvores em vez de delegados (que é o que Enumerableusa).

As árvores de expressão podem ser inspecionadas pelo provedor LINQ escolhido e transformadas em uma consulta real - embora essa seja apenas uma arte negra.

Isso realmente depende do ElementType, Expressione Provider- mas, na realidade, você raramente precisa se preocupar com isso como usuário . Somente um implementador de LINQ precisa conhecer os detalhes sangrentos.


Re comentários; Não sei bem o que você deseja como exemplo, mas considere o LINQ-to-SQL; o objeto central aqui é a DataContext, que representa nosso invólucro de banco de dados. Isso geralmente tem uma propriedade por tabela (por exemplo Customers) e uma tabela é implementada IQueryable<Customer>. Mas não usamos muito diretamente; considerar:

using(var ctx = new MyDataContext()) {
    var qry = from cust in ctx.Customers
              where cust.Region == "North"
              select new { cust.Id, cust.Name };
    foreach(var row in qry) {
        Console.WriteLine("{0}: {1}", row.Id, row.Name);
    }
}

isso se torna (pelo compilador C #):

var qry = ctx.Customers.Where(cust => cust.Region == "North")
                .Select(cust => new { cust.Id, cust.Name });

que é novamente interpretado (pelo compilador C #) como:

var qry = Queryable.Select(
              Queryable.Where(
                  ctx.Customers,
                  cust => cust.Region == "North"),
              cust => new { cust.Id, cust.Name });

É importante ressaltar que os métodos estáticos nas Queryableárvores de expressão, que - em vez da IL regular, são compilados em um modelo de objeto. Por exemplo - basta olhar para "Onde", isso nos dá algo comparável a:

var cust = Expression.Parameter(typeof(Customer), "cust");
var lambda = Expression.Lambda<Func<Customer,bool>>(
                  Expression.Equal(
                      Expression.Property(cust, "Region"),
                      Expression.Constant("North")
                  ), cust);

... Queryable.Where(ctx.Customers, lambda) ...

O compilador não fez muito por nós? Esse modelo de objeto pode ser desmembrado, inspecionado quanto ao significado e reunido novamente pelo gerador TSQL - fornecendo algo como:

 SELECT c.Id, c.Name
 FROM [dbo].[Customer] c
 WHERE c.Region = 'North'

(a cadeia pode acabar como um parâmetro; não me lembro)

Nada disso seria possível se tivéssemos acabado de usar um delegado. E este é o ponto de Queryable/ IQueryable<T>: fornece o ponto de entrada para o uso de árvores de expressão.

Tudo isso é muito complexo, por isso é um bom trabalho que o compilador o torne agradável e fácil para nós.

Para obter mais informações, consulte " C # in Depth " ou " LINQ in Action ", ambos que fornecem cobertura desses tópicos.

Marc Gravell
fonte
2
Se você não se importa, pode me atualizar com um exemplo simples e compreensível (se tiver tempo).
user190560
Você pode explicar "Onde está a definição de" GetQueryableProducts (); "?" em resposta do Sr. Reed Copsey
Pankaj
Gostei muito da linha de traduzir expressões para uma consulta é "arte negra em si" ... um monte de verdade nisso
afreeland
Por que nada disso seria possível se você tivesse usado um delegado?
David Klempfner
1
@Backwards_Dave porque um delegado aponta (essencialmente) para IL e IL não é suficientemente expressivo para tornar razoável tentar desconstruir a intenção suficientemente para criar SQL. A IL também permite muitas coisas - ou seja, a maioria das coisas que podem ser expressas em IL não podem ser expressas na sintaxe limitada de que é razoável se transformar em coisas como SQL
Marc Gravell
17

Embora Reed Copsey e Marc Gravelltenham descrito sobre IQueryable(e também IEnumerable) o suficiente, eu quero acrescentar um pouco mais aqui, fornecendo um pequeno exemplo IQueryablee IEnumerableo número de usuários solicitados.

Exemplo : Criei duas tabelas no banco de dados

   CREATE TABLE [dbo].[Employee]([PersonId] [int] NOT NULL PRIMARY KEY,[Gender] [nchar](1) NOT NULL)
   CREATE TABLE [dbo].[Person]([PersonId] [int] NOT NULL PRIMARY KEY,[FirstName] [nvarchar](50) NOT NULL,[LastName] [nvarchar](50) NOT NULL)

A chave primária ( PersonId) da tabela Employeetambém é uma chave forja ( personid) da tabelaPerson

Em seguida, adicionei o modelo de entidade ado.net no meu aplicativo e criei a classe de serviço abaixo

public class SomeServiceClass
{   
    public IQueryable<Employee> GetEmployeeAndPersonDetailIQueryable(IEnumerable<int> employeesToCollect)
    {
        DemoIQueryableEntities db = new DemoIQueryableEntities();
        var allDetails = from Employee e in db.Employees
                         join Person p in db.People on e.PersonId equals p.PersonId
                         where employeesToCollect.Contains(e.PersonId)
                         select e;
        return allDetails;
    }

    public IEnumerable<Employee> GetEmployeeAndPersonDetailIEnumerable(IEnumerable<int> employeesToCollect)
    {
        DemoIQueryableEntities db = new DemoIQueryableEntities();
        var allDetails = from Employee e in db.Employees
                         join Person p in db.People on e.PersonId equals p.PersonId
                         where employeesToCollect.Contains(e.PersonId)
                         select e;
        return allDetails;
    }
}

eles contêm o mesmo linq. Ele chamou program.csconforme definido abaixo

class Program
{
    static void Main(string[] args)
    {
        SomeServiceClass s= new SomeServiceClass(); 

        var employeesToCollect= new []{0,1,2,3};

        //IQueryable execution part
        var IQueryableList = s.GetEmployeeAndPersonDetailIQueryable(employeesToCollect).Where(i => i.Gender=="M");            
        foreach (var emp in IQueryableList)
        {
            System.Console.WriteLine("ID:{0}, EName:{1},Gender:{2}", emp.PersonId, emp.Person.FirstName, emp.Gender);
        }
        System.Console.WriteLine("IQueryable contain {0} row in result set", IQueryableList.Count());

        //IEnumerable execution part
        var IEnumerableList = s.GetEmployeeAndPersonDetailIEnumerable(employeesToCollect).Where(i => i.Gender == "M");
        foreach (var emp in IEnumerableList)
        {
           System.Console.WriteLine("ID:{0}, EName:{1},Gender:{2}", emp.PersonId, emp.Person.FirstName, emp.Gender);
        }
        System.Console.WriteLine("IEnumerable contain {0} row in result set", IEnumerableList.Count());

        Console.ReadKey();
    }
}

A saída é a mesma para ambos, obviamente

ID:1, EName:Ken,Gender:M  
ID:3, EName:Roberto,Gender:M  
IQueryable contain 2 row in result set  
ID:1, EName:Ken,Gender:M  
ID:3, EName:Roberto,Gender:M  
IEnumerable contain 2 row in result set

Então a questão é qual / onde está a diferença? Não parece ter nenhuma diferença, certo? Realmente!!

Vamos dar uma olhada nas consultas sql geradas e executadas pelo framwork de entidade 5 durante esse período

Parte de execução IQueryable

--IQueryableQuery1 
SELECT 
[Extent1].[PersonId] AS [PersonId], 
[Extent1].[Gender] AS [Gender]
FROM [dbo].[Employee] AS [Extent1]
WHERE ([Extent1].[PersonId] IN (0,1,2,3)) AND (N'M' = [Extent1].[Gender])

--IQueryableQuery2
SELECT 
[GroupBy1].[A1] AS [C1]
FROM ( SELECT 
    COUNT(1) AS [A1]
    FROM [dbo].[Employee] AS [Extent1]
    WHERE ([Extent1].[PersonId] IN (0,1,2,3)) AND (N'M' = [Extent1].[Gender])
)  AS [GroupBy1]

Parte de execução IEnumerable

--IEnumerableQuery1
SELECT 
[Extent1].[PersonId] AS [PersonId], 
[Extent1].[Gender] AS [Gender]
FROM [dbo].[Employee] AS [Extent1]
WHERE [Extent1].[PersonId] IN (0,1,2,3)

--IEnumerableQuery2
SELECT 
[Extent1].[PersonId] AS [PersonId], 
[Extent1].[Gender] AS [Gender]
FROM [dbo].[Employee] AS [Extent1]
WHERE [Extent1].[PersonId] IN (0,1,2,3)

Script comum para ambas as partes de execução

/* these two query will execute for both IQueryable or IEnumerable to get details from Person table
   Ignore these two queries here because it has nothing to do with IQueryable vs IEnumerable
--ICommonQuery1 
exec sp_executesql N'SELECT 
[Extent1].[PersonId] AS [PersonId], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName]
FROM [dbo].[Person] AS [Extent1]
WHERE [Extent1].[PersonId] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1

--ICommonQuery2
exec sp_executesql N'SELECT 
[Extent1].[PersonId] AS [PersonId], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName]
FROM [dbo].[Person] AS [Extent1]
WHERE [Extent1].[PersonId] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=3
*/

Então, você tem algumas perguntas agora, deixe-me adivinhar e tentar respondê-las

Por que scripts diferentes são gerados para o mesmo resultado?

Vamos descobrir alguns pontos aqui,

todas as consultas têm uma parte comum

WHERE [Extent1].[PersonId] IN (0,1,2,3)

porque? Porque a função IQueryable<Employee> GetEmployeeAndPersonDetailIQueryablee IEnumerable<Employee> GetEmployeeAndPersonDetailIEnumerablede SomeServiceClasscontém uma linha comum nas consultas linq

where employeesToCollect.Contains(e.PersonId)

Então, por que a AND (N'M' = [Extent1].[Gender])parte está faltando na IEnumerableparte de execução, enquanto nas duas chamadas de funções usamos Where(i => i.Gender == "M") inprogram.cs`

Agora estamos no ponto em que a diferença surgiu IQueryablee IEnumerable

O que o framwork de entidade faz quando um IQueryablemétodo é chamado, leva a instrução linq escrita dentro do método e tenta descobrir se mais expressões linq são definidas no conjunto de resultados e, em seguida, reúne todas as consultas linq definidas até que o resultado precise buscar e construa sql mais apropriado consulta para executar.

Fornece muitos benefícios, como

  • apenas as linhas preenchidas pelo servidor sql que podem ser válidas por toda a execução da consulta linq
  • ajuda o desempenho do servidor sql ao não selecionar linhas desnecessárias
  • custo de rede se reduzir

como aqui no exemplo, o sql server retornou ao aplicativo apenas duas linhas após a execução do IQueryable`, mas retornou TRÊS linhas para a consulta IEnumerable, por que?

No caso do IEnumerablemétodo, a estrutura da entidade pegou a instrução linq gravada dentro do método e constrói a consulta sql quando o resultado precisar buscar. ele não inclui resto linq parte para construir a consulta sql. Como aqui, nenhuma filtragem é feita no servidor sql na coluna gender.

Mas as saídas são as mesmas? Como 'IEnumerable filtra o resultado ainda mais no nível do aplicativo após recuperar o resultado do servidor sql

Então, o que alguém deve escolher? Pessoalmente, prefiro definir o resultado da função, IQueryable<T>pois, como existem muitos benefícios IEnumerable, você pode associar duas ou mais funções IQueryable, que geram scripts mais específicos para o sql server.

Aqui, no exemplo, você pode ver que IQueryable Query(IQueryableQuery2)gera um script mais específico do IEnumerable query(IEnumerableQuery2)que é muito mais aceitável no meu ponto de vista.

Moumit
fonte
2

Permite mais consultas mais abaixo na linha. Se isso estivesse além do limite de serviço, o usuário desse objeto IQueryable teria permissão para fazer mais com ele.

Por exemplo, se você estiver usando carregamento lento com nhibernate, isso poderá resultar no carregamento do gráfico quando / se necessário.

pomba
fonte