Usando IQueryable con Linq

257

¿De qué sirve IQueryableen el contexto de LINQ?

¿Se utiliza para desarrollar métodos de extensión o para cualquier otro propósito?

user190560
fuente

Respuestas:

508

La respuesta de Marc Gravell es muy completa, pero también pensé en agregar algo sobre esto desde el punto de vista del usuario ...


La principal diferencia, desde la perspectiva del usuario, es que, cuando usa IQueryable<T> (con un proveedor que admite las cosas correctamente), puede ahorrar muchos recursos.

Por ejemplo, si está trabajando en una base de datos remota, con muchos sistemas ORM, tiene la opción de obtener datos de una tabla de dos maneras, una que devuelve IEnumerable<T>y otra que devuelve un IQueryable<T>. Digamos, por ejemplo, que tiene una tabla de Productos y desea obtener todos los productos cuyo costo es> $ 25.

Si lo haces:

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

Lo que sucede aquí es que la base de datos carga todos los productos y los pasa a través del cable a su programa. Su programa luego filtra los datos. En esencia, la base de datos hace un SELECT * FROM Products, y le devuelve CADA producto.

Con el IQueryable<T>proveedor adecuado , por otro lado, puede hacer:

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

El código se ve igual, pero la diferencia aquí es que el SQL ejecutado lo será SELECT * FROM Products WHERE Cost >= 25.

Desde su POV como desarrollador, esto se ve igual. Sin embargo, desde el punto de vista del rendimiento, solo puede devolver 2 registros a través de la red en lugar de 20,000 ...

Reed Copsey
fuente
99
¿Dónde está la definición de "GetQueryableProducts ();"?
Pankaj
12
@StackOverflowUser Está destinado a ser cualquier método que devuelva un IQueryable<Product>- sería específico para su ORM o repositorio, etc.
Reed Copsey
Derecha. pero mencionaste la cláusula where después de esta llamada a la función. Por lo tanto, el sistema aún desconoce el filtro. Quiero decir que todavía busca todos los registros de productos. ¿Derecha?
Pankaj
@StackOverflowUser No: esa es la belleza de IQueryable <T>: se puede configurar para evaluar cuándo se obtienen los resultados , lo que significa que la cláusula Where, utilizada después del hecho, seguirá traduciéndose en una declaración SQL ejecutada en el servidor, y jale solo los elementos necesarios a través del cable ...
Reed Copsey
40
@ Pruebas En realidad todavía no vas a la base de datos. Hasta que realmente enumere los resultados (es decir, use a foreacho llame ToList()), en realidad no golpea la base de datos.
Reed Copsey el
188

En esencia, su trabajo es muy similar a IEnumerable<T> - para representar una fuente de datos consultable - la diferencia es que los diversos métodos LINQ (on Queryable) pueden ser más específicos, para construir la consulta usando Expressionárboles en lugar de delegados (que es lo que Enumerableusa).

El proveedor LINQ elegido puede inspeccionar los árboles de expresión y convertirlos en un consulta real , aunque eso es un arte negro en sí mismo.

Esto realmente depende de ElementType, Expressiony Provider- pero en realidad rara vez necesita preocuparse por esto como usuario . Solo un implementador de LINQ necesita conocer los detalles sangrientos.


Re comentarios; No estoy muy seguro de lo que quiere a modo de ejemplo, pero considere LINQ-to-SQL; El objeto central aquí es un DataContext, que representa nuestro contenedor de base de datos. Esto normalmente tiene una propiedad por tabla (por ejemplo,Customers ) y una tabla se implementa IQueryable<Customer>. Pero no usamos mucho directamente; 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);
    }
}

esto se convierte (por el compilador de C #):

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

que nuevamente es interpretado (por el compilador de C #) como:

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

Es importante destacar que los métodos estáticos en Queryable árboles de expresión de toma, que, en lugar de IL normal, se compilan en un modelo de objetos. Por ejemplo, solo mirando el "Dónde", esto nos da algo comparable 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) ...

¿El compilador no hizo mucho por nosotros? Este modelo de objetos se puede desgarrar, inspeccionar para ver qué significa y volver a armar mediante el generador TSQL, dando algo como:

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

(la cadena podría terminar como un parámetro; no recuerdo)

Nada de esto sería posible si acabáramos de usar un delegado. Y este es el punto de Queryable/ IQueryable<T>: proporciona el punto de entrada para usar árboles de expresión.

Todo esto es muy complejo, por lo que es un buen trabajo que el compilador lo haga agradable y fácil para nosotros.

Para obtener más información, consulte " C # en profundidad " o " LINQ en acción ", que proporcionan cobertura de estos temas.

Marc Gravell
fuente
2
Si no le importa, ¿puede actualizarme con un ejemplo simple y comprensible (si tiene tiempo).
user190560
¿Puede explicar "¿Dónde está la definición de" GetQueryableProducts (); "?" en la respuesta del Sr. Reed Copsey
Pankaj
Disfrutado de la línea de traducir las expresiones de una consulta es "arte negro en sí mismo" ... una gran cantidad de verdad en eso
afreeland
¿Por qué nada de esto sería posible si hubiera usado un delegado?
David Klempfner
1
@Backwards_Dave porque un delegado apunta (esencialmente) a IL, e IL no es lo suficientemente expresivo como para que sea razonable intentar deconstruir la intención lo suficiente como para construir SQL. IL también permite demasiadas cosas, es decir, la mayoría de las cosas que se pueden expresar en IL no se pueden expresar en la sintaxis limitada que es razonable convertir en cosas como SQL
Marc Gravell
17

Aunque Reed Copsey y Marc Gravell ya describieron sobre IQueryable(y también IEnumerable) lo suficiente, quiero agregar un poco más aquí al proporcionar un pequeño ejemplo IQueryabley IEnumerabletantos usuarios lo pidieron

Ejemplo : he creado dos tablas en la base de datos

   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)

La clave primaria ( PersonId) de la tabla Employeetambién es una clave forgein ( personid) de la tablaPerson

A continuación, agregué el modelo de entidad ado.net en mi aplicación y creo la clase de servicio a continuación en ese

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;
    }
}

Contienen el mismo linq. Invocó program.cscomo se define a continuación

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();
    }
}

La salida es la misma 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

Entonces, la pregunta es ¿cuál / dónde está la diferencia? No parece tener ninguna diferencia ¿verdad? ¡¡De Verdad!!

Echemos un vistazo a las consultas sql generadas y ejecutadas por la entidad framwork 5 durante este período

Parte de ejecución 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]

I Parte de ejecución numerable

--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 común para ambas partes de ejecución

/* 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
*/

Entonces tienes algunas preguntas ahora, déjame adivinarlas y tratar de responderlas

¿Por qué se generan diferentes scripts para el mismo resultado?

Veamos algunos puntos aquí,

todas las consultas tienen una parte común

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

¿por qué? Porque tanto function IQueryable<Employee> GetEmployeeAndPersonDetailIQueryablecomo IEnumerable<Employee> GetEmployeeAndPersonDetailIEnumerableof SomeServiceClasscontienen una línea común en consultas linq

where employeesToCollect.Contains(e.PersonId)

Entonces, ¿por qué AND (N'M' = [Extent1].[Gender])falta la parte en la IEnumerableparte de ejecución, mientras que en ambas llamadas a funciones usamos Where(i => i.Gender == "M") inprogram.cs`

Ahora estamos en el punto donde la diferencia se interpuso entre IQueryabley IEnumerable

¿Qué entidad hace el marco cuando un IQueryable se llama a método, toma la instrucción linq escrita dentro del método e intenta averiguar si se definen más expresiones linq en el conjunto de resultados, luego reúne todas las consultas linq definidas hasta que el resultado necesita buscar y construir sql más apropiado consulta a ejecutar.

Proporciona muchos beneficios como,

  • solo aquellas filas pobladas por el servidor sql que podrían ser válidas por toda la ejecución de la consulta linq
  • ayuda al rendimiento del servidor sql al no seleccionar filas innecesarias
  • reducir el costo de red

como aquí en el ejemplo, el servidor sql regresó a la aplicación solo dos filas después de la ejecución IQueryable` pero devolvió TRES filas para la consulta IEnumerable ¿por qué?

En el caso del IEnumerablemétodo, el marco de la entidad tomó la declaración linq escrita dentro del método y construye la consulta sql cuando el resultado necesita ser recuperado. no incluye la parte rest linq para construir la consulta sql. Al igual que aquí, no se realiza ningún filtrado en el servidor SQL en la columna gender.

Pero las salidas son las mismas? Porque 'IEnumerable filtra el resultado aún más en el nivel de aplicación después de recuperar el resultado del servidor SQL

Entonces, ¿qué debería elegir alguien? Personalmente, prefiero definir el resultado de la función IQueryable<T>porque , debido a que tiene muchos beneficios IEnumerable, podría unir dos o más funciones IQueryable, que generan un script más específico para el servidor SQL.

Aquí, por ejemplo, puede ver y IQueryable Query(IQueryableQuery2)genera un script más específico que el IEnumerable query(IEnumerableQuery2)que es mucho más aceptable en mi punto de vista.

Moumit
fuente
2

Permite realizar más consultas más adelante. Si esto fuera más allá de un límite de servicio, entonces, el usuario de este objeto IQueryable podría hacer más con él.

Por ejemplo, si estaba usando carga diferida con nhibernate, esto podría provocar que el gráfico se cargue cuando sea necesario.

paloma
fuente