将LINQ表达式传递给另一个查询提供程序

14
我有一个简单的自定义查询提供程序,它接受表达式,将其转换为SQL并查询SQL数据库。
我想在QueryProvider中创建一个小缓存,以存储常用的对象,这样就可以在没有数据库命中的情况下进行检索。
QueryProvider具有以下方法:
public object Execute(System.Linq.Expressions.Expression expression)
{
    /// Builds an SQL statement from the expression, 
    /// executes it and returns matching objects
}

缓存作为QueryProvider类中的一个字段,是一个简单的通用List。

如果我使用List.AsQueryable方法并将上述表达式传递到List.AsQueryable的Provider的Execute方法中,它不会按预期工作。看起来当表达式被编译时,初始的QueryProvider成为了一个不可分割的部分。

是否可能将表达式传递给后续的QueryProvider并按预期执行表达式?

调用代码大致如下:

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

它不会返回错误,而是陷入循环中,一遍又一遍地调用相同的提供程序。

以下是更多代码,展示我试图做什么:

集合:

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

}

查询:

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

查询提供程序:

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

迭代器:

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

程序:

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

    }
}

长颈鹿:

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

特殊情况,例如SingleAndDefault等不予考虑。我想要处理的部分发生在迭代器中,在那里它首先执行列表的查询提供程序,然后再执行字典的查询提供程序。

两个Queryable对象中的一个可能是数据库或其他内容。


你能添加调用代码吗? - Joshua Drake
1
你能否给出一个Linq表达式的例子,用于调用你的QueryProvider?(我正在尝试在本地重构你的代码)。另外,你是否也实现了Execute的泛型版本?public TResult Execute<TResult>(System.Linq.Expressions.Expression expression) { ... } - CodingWithSpike
更新了LINQ表达式的示例以及Query、QueryProvider和Collection类中的其他代码。 - Anthony
1个回答

7
不,查询不会绑定到提供程序。这就是为什么有IQueryable接口:它提供了Expression和Provider,因此LINQ可以调用提供程序来执行表达式。
你的实现中的问题在于Query表示自己的方式:你将根表达式设置为Expression.Constant(this),其中this是查询(而不是集合)。
因此,当你使用LINQ-to-Objects执行查询时,它将在Query<>上调用GetEnumerator,然后调用LINQ-to-Objects来执行Expression,该Expression具有类型为Query<>的根表达式Expression.Constant(this),然后LINQ-to-Objects通过在Query<>上调用GetEnumerator等来迭代此根表达式。
问题出在...
(IEnumerable<Entity>)this.cacheOne.AsQueryable<Entity>().Provider.Execute(expression)

这基本上相当于

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

或者

linqToObjectsProvider.Execute(expression)

查询返回的提供者与源(this.cacheOne)没有关联,因此您只是重新执行表达式,而不是在缓存上进行查询。

以下代码有什么问题?

class Collection<Entity>
{
    ...

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

请注意,Concat使用延迟评估,因此只有在执行查询时,cacheOne和cacheTwo才会被连接并使用其他LINQ操作进行操作。
(在这种情况下,我将Collection<Entity>设置为IQueryable ,其Expression等于Expression.Constant(this.cacheOne.Concat(this.cacheTwo.Values))`。我认为你可以放弃所有其他类。)

原始答案

然而,我不认为这种利用LINQ to Objects的方式能够实现你想要的功能。

至少,你应该保留原始的查询提供程序,这样当缓存未命中时就可以调用它。如果不这样做,并且使用自己的查询提供程序(你没有展示用于实际调用的代码),你的查询提供程序将一遍又一遍地调用自身。

因此,你需要创建一个CachingQueryProvider和一个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);
    }
}

如果你想要缓存一个结果,你需要在缓存之前将结果实例化,否则你会缓存查询而不是结果。而且这个结果本身不应该再执行,因为它已经是你应该返回的数据。

我会按照以下方向进行:

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

对于标记为无法编译的部分,您需要进行一些反射技巧。

还要注意:字符串实现了IEnumerable接口,因此要小心,不要尝试将单个字符串结果值实例化。


LINQ to Objects是用于枚举内存集合(如数组)的。这是它唯一能做的事情。因此,如果您让LINQ to Objects执行像from entity in table where ... select entity这样的查询,它将要求table返回所有元素,然后将where应用于结果。而table将使用自己的数据上下文来执行此操作(因此每次使用它都会执行SELECT * FROM Table)。所以你必须执行查询并将结果转换为内存结构并缓存它。我不知道L2O在这里适用于哪里。 - Ruben
另外,IQueryProvider.Execute 应该始终返回查询的结果,而不是返回中间表示。也许这就是混淆的原因? - Ruben
@Totero 这可能是你需要计算缓存键的东西,因为查询表达式中的所有本地变量和字段都将由 MemberExpression 表示。或者我完全误解了原始问题。我的思路是:如何缓存随机查询(每个不同查询有一个单独的缓存条目)。如果您想缓存整个表格但在内存中应用所有查询操作,则需要完全不同的实现。 - Ruben
@Ruben 我明白你的意思,如果它是一个带有表引用的表达式,但如果只是像 .Where(T => T.Id == 34) 这样的表达式,我认为应该可以转换为 L2O 查询? - Anthony
@Anthony 正确,但 .Where( => ) 总是在某个东西上调用的,而且那个东西也是表达式的一部分。因此,您可以有一个没有上下文的表达式,例如 T => T.Id == 34(即 .Where 或甚至 .Select 的参数),或者像 table.Where(T => T.Id == 34) 这样的查询。您不能在中间放置任何东西,例如 .Where(T => T.Id == 34),这只是一个片段。 - Ruben
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接