Генерация ключей кэша из IQueryable для кэширования результатов первых запросов кода EF - PullRequest
7 голосов
/ 26 ноября 2011

Я пытаюсь реализовать схему кэширования для моего репозитория EF, аналогичную той, что приведена в блоге здесь .Как сообщают автор и комментаторы, ограничение заключается в том, что метод генерации ключей не может создавать ключи кэша, которые зависят от параметров данного запроса.Вот метод генерации ключа кеша:

private static string GetKey<T>(IQueryable<T> query)
{
    string key = string.Concat(query.ToString(), "\n\r",
        typeof(T).AssemblyQualifiedName);
    return key;
}

Таким образом, следующие запросы приведут к одному и тому же ключу кеша:

var isActive = true;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();

и

var isActive = false;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();

Обратите внимание, чтоединственное отличие состоит в том, что isActive = true в первом запросе и isActive = false во втором.

Любые предложения / идеи по эффективной генерации ключей кэша, которые различаются по параметрам IQueryable, будут по-настоящему оценены.

Благодарность Сергею Барскому за предоставление схемы кэширования EF CodeFirst.

Обновление

Я сам использовал обход дерева выражений IQueryable с целью разрешения значенийпараметры, используемые в запросе.С предложением maxlego я расширил класс System.Linq.Expressions.ExpressionVisitor , чтобы посетить интересующие нас узлы выражений - в данном случае MemberExpression .Обновленный метод GetKey выглядит примерно так:

public static string GetKey<T>(IQueryable<T> query)
{
    var keyBuilder = new StringBuilder(query.ToString());
    var queryParamVisitor = new QueryParameterVisitor(keyBuilder);
    queryParamVisitor.GetQueryParameters(query.Expression);
    keyBuilder.Append("\n\r");
    keyBuilder.Append(typeof (T).AssemblyQualifiedName);

    return keyBuilder.ToString();
}

И класс QueryParameterVisitor, вдохновленный ответами Брайана Ватта и Марка Гравелла на этот вопрос , выглядит так:

/// <summary>
/// <see cref="ExpressionVisitor"/> subclass which encapsulates logic to 
/// traverse an expression tree and resolve all the query parameter values
/// </summary>
internal class QueryParameterVisitor : ExpressionVisitor
{
    public QueryParameterVisitor(StringBuilder sb)
    {
        QueryParamBuilder = sb;
        Visited = new Dictionary<int, bool>();
    }

    protected StringBuilder QueryParamBuilder { get; set; }
    protected Dictionary<int, bool> Visited { get; set; }

    public StringBuilder GetQueryParameters(Expression expression)
    {
        Visit(expression);
        return QueryParamBuilder;
    }

    private static object GetMemberValue(MemberExpression memberExpression, Dictionary<int, bool> visited)
    {
        object value;
        if (!TryGetMemberValue(memberExpression, out value, visited))
        {
            UnaryExpression objectMember = Expression.Convert(memberExpression, typeof (object));
            Expression<Func<object>> getterLambda = Expression.Lambda<Func<object>>(objectMember);
            Func<object> getter = null;
            try
            {
                getter = getterLambda.Compile();
            }
            catch (InvalidOperationException)
            {
            }
            if (getter != null) value = getter();
        }
        return value;
    }

    private static bool TryGetMemberValue(Expression expression, out object value, Dictionary<int, bool> visited)
    {
        if (expression == null)
        {
            // used for static fields, etc
            value = null;
            return true;
        }
        // Mark this node as visited (processed)
        int expressionHash = expression.GetHashCode();
        if (!visited.ContainsKey(expressionHash))
        {
            visited.Add(expressionHash, true);
        }
        // Get Member Value, recurse if necessary
        switch (expression.NodeType)
        {
            case ExpressionType.Constant:
                value = ((ConstantExpression) expression).Value;
                return true;
            case ExpressionType.MemberAccess:
                var me = (MemberExpression) expression;
                object target;
                if (TryGetMemberValue(me.Expression, out target, visited))
                {
                    // instance target
                    switch (me.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            value = ((FieldInfo) me.Member).GetValue(target);
                            return true;
                        case MemberTypes.Property:
                            value = ((PropertyInfo) me.Member).GetValue(target, null);
                            return true;
                    }
                }
                break;
        }
        // Could not retrieve value
        value = null;
        return false;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        // Only process nodes that haven't been processed before, this could happen because our traversal
        // is depth-first and will "visit" the nodes in the subtree before this method (VisitMember) does
        if (!Visited.ContainsKey(node.GetHashCode()))
        {
            object value = GetMemberValue(node, Visited);
            if (value != null)
            {
                QueryParamBuilder.Append("\n\r");
                QueryParamBuilder.Append(value.ToString());
            }
        }

        return base.VisitMember(node);
    }
}

Я все еще делаю профилирование производительности при генерации ключа кеша и надеюсь, что это не слишком дорого (я будуобновите вопрос с результатами, как только я их получу).Я оставлю вопрос открытым, если у кого-то есть предложения по оптимизации этого процесса или есть рекомендации по более эффективному способу генерации ключей кэша, зависящих от параметров запроса.Хотя этот метод дает желаемый результат, он ни в коем случае не является оптимальным.

Ответы [ 3 ]

1 голос
/ 09 июня 2012

Только для записи: " Кэширование результатов запросов LINQ " хорошо работает с EF и способно корректно работать с параметрами, поэтому его можно рассматривать как хорошую реализацию кэша второго уровня для EF.

1 голос
/ 29 мая 2013

Хотя решение OP работает достаточно хорошо, я обнаружил, что производительность решения немного низкая.

Продолжительность генерации ключа варьировалась от 300ms до 1200ms для моих запросов.

Однако я нашел другое решение, которое имеет более высокую производительность (<10ms).

    public static string ToTraceString<T>(DbQuery<T> query)
    {
        var internalQueryField = query.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(f => f.Name.Equals("_internalQuery")).FirstOrDefault();

        var internalQuery = internalQueryField.GetValue(query);

        var objectQueryField = internalQuery.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(f => f.Name.Equals("_objectQuery")).FirstOrDefault();

        var objectQuery = objectQueryField.GetValue(internalQuery) as ObjectQuery<T>;

        return ToTraceStringWithParameters(objectQuery);
    }

    private static string ToTraceStringWithParameters<T>(ObjectQuery<T> query)
    {
        string traceString = query.ToTraceString() + "\n";

        foreach (var parameter in query.Parameters)
        {
            traceString += parameter.Name + " [" + parameter.ParameterType.FullName + "] = " + parameter.Value + "\n";
        }

        return traceString;
    }
1 голос
/ 29 ноября 2011

я предлагаю использовать ExpressionVisitor http://msdn.microsoft.com/en-us/library/bb882521(v=vs.90).aspx

...