14

式を受け取り、それを SQL に変換し、SQL データベースにクエリを実行する単純なカスタム QueryProvider があります。

データベースにヒットすることなく取得できるように、頻繁にアクセスされるオブジェクトを格納する 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.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 などの特殊なケースは除外されます。私が作業したい部分はイテレーターで発生します。そこでは、最初に辞書のクエリプロバイダーを実行する前にリストのクエリプロバイダーを実行します。

2 つの Queryable オブジェクトのうちの 1 つは、データベースまたはその他のものである可能性があります。

4

1 に答える 1

7

いいえ、クエリはプロバイダーにバインドされません。これが、IQueryableインターフェイスを使用している理由です。これは式とプロバイダーの両方を提供するため、LINQはプロバイダーを呼び出して式を実行できます。

実装の問題は、Query<Entity>それ自体を表す方法にあります。ルート式をに設定しています。Expression.Constant(this)ここthisで、はクエリ(コレクションではありません)です。

したがって、LINQ-to-Objectsを使用してクエリを実行すると、が呼び出さGetEnumeratorQuery<>、次にLINQ-to-Objectsが呼び出されます。これは(タイプの)Expressionルート式を持ち、LINQ-to-Objectsはこのルート式を繰り返します。これを呼び出すことなどによって。Expression.Constant(this)Query<>GetEnumeratorQuery<>

問題はにあります

(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 withExpression equal toExpression.Constant(this.cacheOne.Concat(this.cacheTwo.Values)) `を作成します。他のすべてのクラスを廃止できると思います。)


元の回答

ただし、LINQを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;
    }
}

とマークされた部分については、Won't compile反射のトリックを行う必要があります。

注意:文字列はIEnumerableを実装しているため、単一の文字列結果値を具体化しようとしないように注意してください。

于 2012-05-25T15:03:10.363 に答える