【问题标题】:Generating Cache Keys from IQueryable For Caching Results of EF Code First Queries从 IQueryable 生成缓存键以缓存 EF Code First 查询的结果
【发布时间】:2011-11-26 02:37:59
【问题描述】:

我正在尝试为我的 EF 存储库实现一种缓存方案,类似于博客 here 中的缓存方案。正如作者和评论者所报告的那样,限制是密钥生成方法不能生成随给定查询参数而变化的缓存密钥。这里是缓存键的生成方法:

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 参数而异的缓存键的建议/见解将不胜感激。

感谢 Sergey Barskiy 分享 EF CodeFirst 缓存方案。

更新

我自己采取了遍历 IQueryable 的表达式树的方法,目的是解析查询中使用的参数的值。根据maxlego's 的建议,我扩展了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 类的灵感来自Bryan WattsMarc Gravell 对此question 的回答,看起来像这样:

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

我仍在对缓存密钥生成进行一些性能分析,并希望它不会太昂贵(一旦我得到结果,我会用结果更新问题)。我将保留这个问题,以防有人对如何优化此过程有建议,或者对生成缓存键的更有效方法提出建议,这些方法会随查询参数而变化。虽然这种方法产生了所需的输出,但它绝不是最优的。

【问题讨论】:

    标签: entity-framework caching repository ef-code-first iqueryable


    【解决方案1】:

    【讨论】:

      【解决方案2】:

      仅作记录,“Caching the results of LINQ queries”与 EF 配合得很好,并且能够正确处理参数,因此可以将其视为 EF 的良好二级缓存实现。

      【讨论】:

      • 您能否将您的答案修改为评论?澄清一下,这个问题旨在解决为进程外缓存集群创建缓存键的问题,因此相同查询的结果可以在多个服务器之间共享。
      • 我不会。请参阅上述文章的GetCacheKey(this IQueryable query) 以获取有关创建缓存键的更多信息,或者花一些时间阅读一次。
      【解决方案3】:

      虽然 OP 的解决方案运行良好,但我发现该解决方案的性能有点差。

      对于我的查询,密钥生成的持续时间在 300ms1200ms 之间变化。

      但是,我发现了另一种性能更好的解决方案 (&lt;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;
          }
      

      【讨论】:

      • 可能是这样,但此解决方案仅适用于 EF,因此您以性能换取灵活性。 OP 解决方案可以与任何其他 ORM 一起使用,只要它们符合 IQueryable 接口。
      猜你喜欢
      • 1970-01-01
      • 2015-04-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-13
      • 2017-08-17
      • 2023-03-23
      • 2015-08-01
      • 1970-01-01
      相关资源
      最近更新 更多