2012-05-22 6 views
12

Tengo un QueryProvider personalizado simple que toma una expresión, la traduce a SQL y consulta una base de datos sql.Pase la expresión LINQ a otro QueryProvider

Quiero crear un pequeño caché en el QueryProvider que almacena los objetos a los que se accede comúnmente para que la recuperación pueda realizarse sin un golpe de base de datos.

El QueryProvider tiene el método

public object Execute(System.Linq.Expressions.Expression expression) 
{ 
    /// Builds an SQL statement from the expression, 
    /// executes it and returns matching objects 
} 

La memoria caché se sienta como un campo en esta clase QueryProvider y es una lista genérica simple.

Si utilizo el método List.AsQueryable y paso la expresión anterior al método Execute de List.AsQueryable's Provider, no funciona como lo desea. Parece que cuando una expresión se compila, el QueryProvider inicial se convierte en una parte integral.

¿Es posible pasar una expresión a un QueryProvider posterior y ejecutar la expresión como se desee?

código La llamada parece vagamente como sigue:

public class QueryProvider<Entity>() 
{ 
    private List<TEntity> cache = new List<Entity>(); 

    public object Execute(System.Linq.Expressions.Expression expression) 
    { 
     /// check whether expression expects single or multiple result 
     bool isSingle = true; 

     if (isSingle) 
     { 
      var result = this.cache.AsQueryable<Entity>().Provider.Execute(expression); 
      if (result != null) 
       return result; 
     } 

     /// cache failed, hit database 
     var qt = new QueryTranslator(); 
     string sql = qt.Translate(expression); 
     /// .... hit database 
    } 
} 

No devuelve un error, sino que se queda atascado en este bucle en el mismo proveedor se llama una y otra vez.

Aquí hay algo más de código que muestra lo que estoy tratando de hacer:

Colección:

class Collection<Entity> 
{ 

    internal List<Entity> cacheOne { get; private set; } 
    internal Dictionary<Guid, Entity> cacheTwo { get; private set; } 

    internal Collection() 
    { 
     this.cacheOne = new List<Entity>(); 
     this.cacheTwo = new Dictionary<Guid, Entity>(); 
    } 

    public IQueryable<Entity> Query() 
    { 
     return new Query<Entity>(this.cacheOne, this.cacheTwo); 
    } 

} 

Consulta:

class Query<Entity> : IQueryable<Entity> 
{ 
    internal Query(List<Entity> cacheOne, Dictionary<Guid, Entity> cacheTwo) 
    { 
     this.Provider = new QueryProvider<Entity>(cacheOne, cacheTwo); 
     this.Expression = Expression.Constant(this); 
    } 

    internal Query(IQueryProvider provider, Expression expression) 
    { 
     this.Provider = provider; 
     if (expression != null) 
      this.Expression = expression; 
    } 

    public IEnumerator<Entity> GetEnumerator() 
    { 
     return this.Provider.Execute<IEnumerator<Entity>>(this.Expression); 
    } 

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     return this.GetEnumerator(); 
    } 

    public Type ElementType 
    { 
     get { return typeof(Entity); } 
    } 

    public System.Linq.Expressions.Expression Expression { get; private set; } 

    public IQueryProvider Provider { get; private set; } 
} 

QueryProvider:

class QueryProvider<Entity> : IQueryProvider 
{ 

    private List<Entity> cacheOne; 
    private Dictionary<Guid, Entity> cacheTwo; 

    internal QueryProvider(List<Entity> cacheOne, Dictionary<Guid, Entity> cacheTwo) 
    { 
     this.cacheOne = cacheOne; 
     this.cacheTwo = cacheTwo; 
    } 

    public IQueryable<TElement> CreateQuery<TElement>(System.Linq.Expressions.Expression expression) 
    { 
     return new Query<TElement>(this, expression); 
    } 

    public IQueryable CreateQuery(System.Linq.Expressions.Expression expression) 
    { 
     throw new NotImplementedException(); 
    } 

    public TResult Execute<TResult>(System.Linq.Expressions.Expression expression) 
    { 
     return (TResult)this.Execute(expression); 
    } 

    public object Execute(System.Linq.Expressions.Expression expression) 
    { 
     Iterator<Entity> iterator = new Iterator<Entity>(expression, cacheOne, cacheTwo); 
     return (iterator as IEnumerable<Entity>).GetEnumerator(); 
    } 
} 

iterador:

class Iterator<Entity> : IEnumerable<Entity> 
{ 
    private Expression expression; 
    private List<Entity> cacheOne; 
    private Dictionary<Guid, Entity> cacheTwo; 

    internal Iterator(Expression expression, List<Entity> cacheOne, Dictionary<Guid, Entity> cacheTwo) 
    { 
     this.expression = expression; 
     this.cacheOne = cacheOne; 
     this.cacheTwo = cacheTwo; 
    } 

    public IEnumerator<Entity> GetEnumerator() 
    { 
     foreach (var result in (IEnumerable<Entity>)this.cacheOne.AsQueryable<Entity>().Provider.Execute(expression)) 
     { 
      yield return result; 
     } 

     foreach (var more in (IEnumerable<Entity>)this.cacheTwo.Values.AsQueryable<Entity>().Provider.Execute(expression)) 
     { 
      yield return more; 
     } 
    } 

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     return this.GetEnumerator(); 
    } 
} 

programa:

class Program 
{ 
    static void Main(string[] args) 
    { 
     /// Create collection + caches 
     var collection = new Collection<Giraffe>(); 
     collection.cacheOne.AddRange(new Giraffe[] { 
      new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(2011, 03, 21), Height = 192, Name = "Percy" }, 
      new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(2005, 12, 25), Height = 188, Name = "Santa" }, 
      new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(1999, 04, 01), Height=144, Name="Clown" } 
     }); 
     var cachetwo = new List<Giraffe>(new Giraffe[] { 
      new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(1980, 03,03), Height = 599, Name="Big Ears" }, 
      new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(1985, 04, 02), Height= 209, Name="Pug" } 
     }); 
     foreach (var giraffe in cachetwo) 
      collection.cacheTwo.Add(giraffe.Id, giraffe); 

     /// Iterate through giraffes born before a certain date 
     foreach (var result in collection.Query().Where(T => T.DateOfBirth < new DateTime(2006, 01, 01))) 
     { 
      Console.WriteLine(result.Name); 
     } 

    } 
} 

jirafa:

class Giraffe 
{ 
    public Guid Id { get; set; } 
    public string Name { get; set; } 
    public long Height { get; set; } 
    public DateTime DateOfBirth { get; set; } 
} 

casos especiales, por ejemplo, SingleAndDefault, etc. quedan fuera. La parte que quiero trabajar ocurre en Iterator, donde primero ejecuta el QueryProvider de List antes de ejecutar Dictionary.

Uno de los dos objetos consultables puede ser una base de datos u otra cosa.

+0

¿Podría agregar el código de llamada? –

+0

Código de llamada agregado – Anthony

+1

¿Podría dar un ejemplo de la expresión de Linq que está utilizando para llamar a su QueryProvider? (Estoy intentando reconstruir tu código localmente). Además, ¿implementa la versión genérica de Execute también? 'public TResult Execute (System.Linq.Expressions.Expression expression) {...}' – CodingWithSpike

Respuesta

6

No, una consulta no queda vinculada a un proveedor. Es por eso que tiene la interfaz IQueryable: proporciona tanto la Expresión como el Proveedor, por lo que LINQ puede llamar al proveedor para ejecutar la expresión.

El problema en su aplicación es en la forma Query<Entity> representa en sí: se está configurando la expresión de la raíz a Expression.Constant(this), donde this es el consulta (no la colección).

Así que cuando se ejecuta la consulta con LINQ a objetos, se llamará GetEnumerator en Query<>, que llama a continuación, LINQ a objetos para ejecutar Expression, que tiene una expresión raíz Expression.Constant(this) (de tipo Query<>), y LINQ -to-objetos y luego itera esta expresión raíz llamando GetEnumerator en este Query<>, etc.

El problema radica en

(IEnumerable<Entity>)this.cacheOne.AsQueryable<Entity>().Provider.Execute(expression) 

que es básicamente igual a

new Entity[0].AsQueryable().Provider.Execute(expression) 

o

linqToObjectsProvider.Execute(expression) 

El proveedor devuelto por una consulta es no vinculado a la fuente (this.cacheOne), por lo que es justo volver a ejecutar la expresión, no consultar sobre su caché.

¿Qué pasa con lo siguiente?

class Collection<Entity> 
{ 
    ... 

    public IQueryable<Entity> Query() 
    { 
     return this.cacheOne.Concat(this.cacheTwo.Values).AsQueryable(); 
    } 
} 

Tenga en cuenta que Concat utiliza la evaluación retrasada, por lo que sólo cuando se ejecuta la consulta son cacheOne y cacheTwo concatena y luego manipulado utilizando los operadores LINQ adicionales.

(en cuyo caso, me gustaría hacer Collection<Entity> IQueryable with Expresión equal to Expression.Constant (this.cacheOne.Concat (this.cacheTwo.Values)) `. Creo que se puede acabar con todas las otras clases.)


respuesta original

sin embargo, no creo que esta forma de cuestas LINQ a objetos será nunca capaz de hacer lo que cree que debería.

Por lo menos, debe mantener el proveedor original de consultas para que pueda llamar a ese cuando no tenga un caché. Si no lo hace, y utiliza su propio proveedor de consultas (no mostró el código que está utilizando para hacer la llamada real), su proveedor de consultas se llamará a sí mismo nuevamente, y de nuevo.

por lo que tendrá que crear un CachingQueryProvider y un CachingQuery:

class CachingQuery<T> : IQueryable<T> 
{ 
    private readonly CachingQueryProvider _provider; 
    private readonly Expression _expression; 

    public CachingQuery(CachingQueryProvider provider, Expression expression) 
    { 
     _provider = provider; 
     _expression = expression; 
    } 

    // etc. 
} 

class CachingQueryProvider : IQueryProvider 
{ 
    private readonly IQueryProvider _original; 

    public CachingQueryProvider(IQueryProvider original) 
    { 
     _original = original; 
    } 

    // etc. 
} 

public static class CachedQueryable 
{ 
    public static IQuerable<T> AsCached(this IQueryable<T> source) 
    { 
     return new CachingQuery<T>(
      new CachingQueryProvider(source.Provider), 
      source.Expression); 
    } 
} 

Además, si desea almacenar en caché consecuencia, tendrá que materializar el resultado antes de que se hace una caché, de lo contrario, almacena en caché la consulta, no el resultado. Y el resultado en sí mismo nunca debería ejecutarse nuevamente, ya que ya son los datos que debe devolver.

La dirección en la que se dirigiría hacia es el siguiente:

class CachingQueryProvider : IQueryProvider 
{ 
    public object Execute(Expression expression) 
    { 
     var key = TranslateExpressionToCacheKey(expression); 

     object cachedValue; 
     if (_cache.TryGetValue(key, out cachedValue)) 
      return cachedValue; 

     object result = _originalProvider.Execute(expression); 

     // Won't compile because we don't know T at compile time 
     IEnumerable<T> sequence = result as IEnumerable<T>; 
     if (sequence != null && !(sequence is ICollection<T>)) 
     { 
      result = sequence.ToList<T>(); 
     } 

     _cache[key] = result; 

     return result; 
    } 
} 

Para la parte marcada como Won't compile, tendrá que hacer algunos trucos reflexión.

Y cuidado: la cadena implementa IEnumerable, así que tenga cuidado no para tratar de materializar el valor de un único string.

+0

Gracias, Rubén, eso es útil, pero ¿por qué crees que usar piggy-backing LINQ-to-Objects nunca hará lo que yo espero? Su solución alternativa es buena, pero tengo curiosidad por saber por qué no cree que el respaldo puede funcionar. – Anthony

+0

LINQ to Objects está destinado a enumerar colecciones en memoria (como matrices). Eso es lo único que puede hacer. Entonces, si dejas que LINQ to Objects ejecute una consulta como 'from entity in table where ... select entity', pedirá a' table' que devuelva todos los elementos, y luego aplicará 'where' al resultado. Y 'table' usará su propio contexto de datos para hacerlo (y así ejecutar' SELECT * FROM Table' cada vez que lo use). Por lo tanto, debe ejecutar la consulta y transformar el resultado en una estructura en memoria y guardarla en caché. No veo dónde encaja L2O aquí. – Ruben

+0

También 'IQueryProvider.Execute' siempre debe devolver el * resultado * de la consulta. No debería devolver una representación intermedia. Tal vez esa es la confusión? – Ruben

Cuestiones relacionadas