Question

J'ai un simple QueryProvider personnalisé qui prend une expression, la traduit en SQL et interroge une base de données SQL.

Je souhaite créer un petit cache dans le QueryProvider qui stocke les objets couramment consultés afin que la récupération puisse avoir lieu sans accès à la base de données.

Le QueryProvider a la méthode

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

Le cache se présente sous la forme d'un champ dans cette classe QueryProvider et est une simple liste générique.

Si j'utilise la méthode List.AsQueryable et que je transmets l'expression ci-dessus dans la méthode Execute du fournisseur de List.AsQueryable, cela ne fonctionne pas comme souhaité.Il semble que lorsqu'une expression est compilée, le QueryProvider initial en fait partie intégrante.

Est-il possible de transmettre une expression à un QueryProvider suivant et d'exécuter l'expression comme vous le souhaitez ?

Le code appelant ressemble vaguement à ceci :

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

Il ne renvoie pas d'erreur, mais reste bloqué dans une boucle où ce même fournisseur est appelé encore et encore.

Voici un code supplémentaire montrant ce que j'essaie de faire :

Collection:

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

}

Requête:

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

Fournisseur de requête :

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

Itérateur :

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

Programme:

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

    }
}

Girafe:

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

Cas particuliers, par ex.SingleAndDefault, etc. sont laissés de côté.La partie sur laquelle je souhaite travailler se déroule dans Iterator, où il exécute d'abord le QueryProvider de la liste avant d'exécuter celui du dictionnaire.

L'un des deux objets interrogeables peut être une base de données ou autre chose.

Était-ce utile?

La solution

Non, une requête n'est pas liée à un fournisseur.C'est pourquoi vous disposez de l'interface IQueryable :il fournit à la fois l'expression et le fournisseur, afin que LINQ puisse appeler le fournisseur pour exécuter l'expression.

Le problème dans votre implémentation réside dans la manière Query<Entity> se représente :vous définissez l'expression racine sur Expression.Constant(this), où this est le requête (pas la collection).

Ainsi, lorsque vous exécutez la requête avec LINQ-to-Objects, elle appellera GetEnumerator sur Query<>, qui appelle ensuite LINQ-to-Objects pour exécuter Expression, qui a une expression racine Expression.Constant(this) (de type Query<>), et LINQ-to-Objects itère ensuite cette expression racine en appelant GetEnumerator sur ce Query<>, etc.

Le problème réside dans

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

ce qui est fondamentalement égal à

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

ou

linqToObjectsProvider.Execute(expression)

Le fournisseur renvoyé par une requête est pas lié à la source (this.cacheOne), vous réexécutez donc simplement l'expression, sans interroger votre cache.

Quel est le problème avec ce qui suit ?

class Collection<Entity>
{
    ...

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

Noter que Concat utilise une évaluation retardée, donc ce n'est que lorsque vous exécutez la requête que cacheOne et cacheTwo sont concaténés puis manipulés à l'aide des opérateurs LINQ supplémentaires.

(Auquel cas, je ferais Collection<Entity> un IQueryablewithExpressionequal toExpression.Constant(this.cacheOne.Concat(this.cacheTwo.Values))`.Je pense que vous pouvez supprimer toutes les autres classes.)


Réponse originale

Cependant, je ne pense pas que cette façon de superposer LINQ to Objects sera un jour capable de faire ce que vous pensez qu'elle devrait faire.

À tout le moins, vous devriez conserver le original fournisseur de requêtes afin que vous puissiez l'appeler en cas d'échec du cache.Si vous ne le faites pas et utilisez votre propre fournisseur de requêtes (vous n'avez pas affiché le code que vous utilisez pour effectuer l'appel), votre fournisseur de requêtes s'appellera encore et encore.

Vous devrez donc créer un CachingQueryProvider et une requête de mise en cache :

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

Aussi si vous souhaitez mettre en cache un résultat, vous devrez matérialiser le résultat avant vous le mettez en cache, sinon vous mettez en cache la requête, pas le résultat.Et le résultat lui-même ne doit plus jamais être exécuté, car ce sont déjà les données que vous devez renvoyer.

La direction dans laquelle je me dirigerais est la suivante :

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

Pour la pièce marquée comme Won't compile, vous devrez faire quelques astuces de réflexion.

Et prudence :la chaîne implémente IEnumerable, alors soyez prudent pas pour essayer de matérialiser une valeur de résultat de chaîne unique.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top