• LinqToDB 源码分析——处理表达式树


    处理表达式树可以说是所有要实现Linq To SQL的重点,同时他也是难点。笔者看完作者在LinqToDB框架里面对于这一部分的设计之后,心里有一点不知所然。由于很多代码没有文字注解。所以笔者只能接合上下代码来推断出作者大概在做什么。但是有些笔者只知道在做什么却很难推断出作者为什么要这么做。这一部分的主要核心类有俩个——Query<T>类和ExpressionBuilder类。可以用一句话来形容:由Query<T>类起也由Query<T>类落。

    处理优化表达树


    上一章我们能知道执行最后的操作一定是要通过Query<T>类实例来完成的。而Query<T>类又必须通过ExpressionBuilder类来获得的。很显然他们俩个之间的关系很复杂。但是有一点可以肯定——最后的工作都会交给Query<T>类的GetElement方法和GetIEnumerable方法。在Query<T>类的构造函数里面一开始就把GetIEnumerable方法赋于MakeEnumerable方法。

    public Query()
    {
        GetIEnumerable = MakeEnumerable;
    }

    也许是笔者理解上的错误——发现GetIEnumerable最后还会被别的方法取代。也就是说MakeEnumerable方法并没有被执行。但是MakeEnumerable方法的作用却明显就调用GetElement方法最后转化成需要的结果。可以看出对于实例化Query<T>类并没有过多复杂的操作。但是获得Query<T>类实例却要用自身的静态方法GetQuery来进行。如果直接实例Query<T>类的话,笔者也不觉得复杂。主要是他还要通过ExpressionBuilder类的进行加工。这一点让笔者有一种深入迷宫的快感。去掉那些缓存代码。让我们把重点移到迷宫口。

    Query<T>类:

     query = new ExpressionBuilder(new Query<T>(), dataContextInfo, expr, null).Build<T>();

    ExpressionBuilder类的构造函数的参数很简单——Query<T>类实例、数据上下文信息、当前表达式树。最后一参数跟LinqToDB框架的另一个功能有关系——CompiledQuery功能。所以如果你一直用Linq To SQL的话,最后一个参数一直是null。参数理解起来并不难。可是构造函数里面的代码却让笔者很头痛。笔者只能知道做什么却很难理解为什么要这样子做。

     1 public ExpressionBuilder(Query query, IDataContextInfo dataContext, Expression expression, ParameterExpression[] compiledParameters)
     2 {
     3             _query = query;
     4             _expressionAccessors = expression.GetExpressionAccessors(ExpressionParam);
     5 
     6             CompiledParameters = compiledParameters;
     7             DataContextInfo = dataContext;
     8             OriginalExpression = expression;
     9 
    10             _visitedExpressions = new HashSet<Expression>();
    11             Expression = ConvertExpressionTree(expression);
    12             _visitedExpressions = null;
    13 
    14             if (Configuration.AvoidSpecificDataProviderAPI)
    15             {
    16                 DataReaderLocal = DataReaderParam;
    17             }
    18             else
    19             {
    20                 DataReaderLocal = BuildVariable(Expression.Convert(DataReaderParam, dataContext.DataContext.DataReaderType), "ldr");
    21             }
    22 }

    红色代码部分便是笔者不能理解的部分。对于第4行的GetExpressionAccessors方法笔者也只能大概的猜测出他是意思。如果只看这一段代码的话,显然是不可能知道GetExpressionAccessors方法有什么作用。同样子你也不可能知道_expressionAccessors的目地。在前面几章中我们可以理解到LinqToDB框架是通过生成T-SQL来执行最后的数据库的。在生成T-SQL的时候一定会用到参数吧。就是ADO.NET中的IDbDataParameter接口实例类。那么这俩者又有什么联系呢?

    LinqToDB框架在生成T-SQL的时候要用到一个类叫做SelectQuery类。SelectQuery类实例是通过 Query<T>的Queries集合成员来获得的。Queries集合成员就是用于存放QueryInfo类的。好了。重点来了。QueryInfo类除了提供SelectQuery类实例外,还有一个重要功能——设置将来要用的参数信息。当然如果现在就开始讲设置参数信息的话,显然有一点不知所措。要想明白这一切就必须从上面提到代码段中的Build<T>()方法入手。

    ExpressionBuilder类:

     1 internal Query<T> Build<T>()
     2  {
     3      var sequence = BuildSequence(new BuildInfo((IBuildContext)null, Expression, new SelectQuery()));
     4 
     5      if (_reorder)
     6            lock (_sync)
     7            {
     8                _reorder = false;
     9                _sequenceBuilders = _sequenceBuilders.OrderByDescending(_ => _.BuildCounter).ToList();
    10            }
    11 
    12      _query.Init(sequence, CurrentSqlParameters);
    13 
    14      var param = Expression.Parameter(typeof(Query<T>), "info");
    15 
    16      sequence.BuildQuery((Query<T>)_query, param);
    17 
    18      return (Query<T>)_query;
    19 }

    Build<T>()方法中有三句代码很重要。笔者在上面的代码中用红色标出了。还记得上面笔者讲到的Query<T>的Queries集合成员吗?每二句红色代码就是用于初始化Queries集合成员的信息。说明白了就是增加QueryInfo类实例了。同时不要忘了CurrentSqlParameters集合成员。

    Query类:

     1 public override void Init(IBuildContext parseContext, List<ParameterAccessor> sqlParameters)
     2 {
     3       Queries.Add(new QueryInfo
     4       {
     5                 SelectQuery = parseContext.SelectQuery,
     6                 Parameters = sqlParameters,
     7       });
     8 }

    CurrentSqlParameters集合成员里面存放的是一个叫ParameterAccessor的类。就是用于表示生成T-SQL时所用到的参数信息。有几个参数CurrentSqlParameters集合里面就有几个ParameterAccessor类实例。显然目标很明显就是用于构建执行SQL的IDbDataParameter接口实例。那么这些信息是在哪里实例化的呢?从代码中我们可以知道一定是在BuildSequence方法中生成的。所以读者们只要跟踪一下就是可以找到对应的代码。那么笔者这里就直接贴出来。

    ExpressionBuilder类:

     1 ParameterAccessor BuildParameter(Expression expr)
     2 {
     3     ParameterAccessor p;
     4 
     5     if (_parameters.TryGetValue(expr, out p))
     6            return p;
     7 
     8     string name = null;
     9 
    10     var newExpr = ReplaceParameter(_expressionAccessors, expr, nm => name = nm);
    11 
    12     p = CreateParameterAccessor(
    13     DataContextInfo.DataContext, newExpr, expr, ExpressionParam, ParametersParam, name);
    14 
    15     _parameters.Add(expr, p);
    16     CurrentSqlParameters.Add(p);
    17 
    18     return p;
    19 }

    作者是这样子构思的。如果我们的Linq To SQL句话存在引用参数的时候,事实上就是在跟我们讲执行SQL要有一个传入的参数。当然,笔者讲的不是用字符串拼接成最后的SQL句语。而是用ADO.NET的传参数(IDbDataParameter类)。例如

    int n2 = 30;
    var query = from p in dbContext.Products where p.ProductID > n2 select p;
    List<Products> productList = query.ToList();

    上面的代码中的n2是Linq To SQL外面的变量。这很显明就是说有一个叫n2的传参了。自然,上面的CurrentSqlParameters集合里面就有一个成员了。所以在生成DataParamete参数的时候,设置对应的值很重。那么如何得到传参的值呢?作者就是用表达式树来建立一个“方法”(lambda表达式)。这个方法作用是就是在读取前面Linq To SQL生成的表达式树,找到参数值所在的表达式节点并获得相应的值。

    要获得参数值就要遍历表达式树。相信如果多次的操作一定会很伤性能的。所以_expressionAccessors事实上就是存放获得表达式节点的路径。有一点像缓存的作用。比如同一个参数多几调用。那么就可以不用多次遍历。只要一次就行了。(相应的代码在ExpressionBuilder.SqlBuilder文件的ReplaceParameter方法)。

    我们在实列化ExpressionBuilder类的时候,除了看到上面讲到的_expressionAccessors相关的代码之外。我们可以看到一叫ConvertExpressionTree的方法。这个方法里面最重要的要说前三段代码。

    ExpressionBuilder类:

    Expression ConvertExpressionTree(Expression expression)
    {
                var expr = ConvertParameters(expression);
    
                expr = ExposeExpression(expr);
                expr = OptimizeExpression(expr);
                  //......
                 //.......
                  //......
        
    }    

    笔者在上面提到过一个功能——CompiledQuery功能。上面的ConvertParameters方法在使用CompiledQuery功能的时候他的作用表现的最明显。让我们看一下例子吧。

    var query = CompiledQuery.Compile((IAdoContext db, int n2) =>
                   db.Products.Where(p => p.ProductID > n2));
    using (AdoContext dbContext = new AdoContext())
    {
         List<Products> catalogsList = query(dbContext, 30).ToList();
    }

    ConvertParameters方法

    我们从列子中可以知道一点——至少要有一个参数n2吧。事实上在使用CompiledQuery功能的时候,上面db和n2会作为实列化ExpressionBuilder类的最后参数传入。也就是compiledParameters参数对应的值。最后生成T-SQL对应的IDbDataParameter接口实例所需要的值就必须通过compiledParameters参数来获得。所以ConvertParameters方法就把表达树进行了转变。转变成通过compiledParameters参数来获得值的表达式树。这一点读者们可以自己做试验来看。

    ExposeExpression方法

    ExposeExpression方法跟LinqToDB框架中的ExpressionMethod功能有关系。就是去执行ExpressionMethod里面指定的方法,然后重新生成表达式树。笔者有时候真不知道这功能有什么用。

    OptimizeExpression方法

    OptimizeExpression方法就是用于优化当前的表达式树。笔者简单的说一个列子。

    dbContext.Products.Count(t => t.ProductID > 30);

    通过OptimizeExpression方法之后

     dbContext.Products.Where(t => t.ProductID > 30).Count();

    相信笔者不用多说你们也懂得的。为什么要变成这样子笔者想可能跟后面生成SQL有关系吧。

    对于实列化ExpressionBuilder类所做的事情,大至上可以说俩件事情。如下

    1.遍历表达式树。缓存当前表达式树节点的访问路径。不至于多次遍历表达式树。
    2.转化表达树。一、转化使用到的参数;二、转化对象存在的ExpressionMethod方法;三、优化表达式树的。

    生成相关的SQL信息


    实列化ExpressionBuilder类所做事情很多。但是最终还是为生成SQL服务的。所以在实列化ExpressionBuilder类的时候,LinqToDB框架就把当前表达式处理优化好了。接下来就是提取相关的生成SQL要用的信息。而这一部分所用的类都存放在LinqToDB.Linq.Builder的命名空间下。其入口方法还是在上面提到的BuildSequence方法里面。

    ExpressionBuilder类:

     1 public IBuildContext BuildSequence(BuildInfo buildInfo)
     2 {
     3      buildInfo.Expression = buildInfo.Expression.Unwrap();
     4 
     5      var n = _builders[0].BuildCounter;
     6 
     7      foreach (var builder in _builders)
     8      {
     9           if (builder.CanBuild(this, buildInfo))
    10           {
    11               var sequence = builder.BuildSequence(this, buildInfo);
    12 
    13               lock (builder)
    14                     builder.BuildCounter++;
    15 
    16                _reorder = _reorder || n < builder.BuildCounter;
    17 
    18               return sequence;
    19            }
    20 
    21            n = builder.BuildCounter;
    22      }
    23 
    24      throw new LinqException("Sequence '{0}' cannot be converted to SQL.", buildInfo.Expression);
    25 }

    所有用于的提取SQL信息的类都是基于ISequenceBuilder接口。这个方法中_builders存放了大量关于ISequenceBuilder接口实例。我们可以从名字上判断出一点——Linq To SQL关键字几乎都有一个对应的XxxxBuilder类。显然作者本意就表达出来了。比如Linq查询的where部分就是找WhereBuilder类来提取相关的SQL信息,Table<>部分就是去找TableBuilder类。如何进行的读者们可以跟代码看看。

    BuildSequence方法要传入一个BuildInfo类型的参数。上面讲到跟生成SQL相关的SelectQuery类也在这里体现出来了。因为SelectQuery类也是BuildInfo类的构造函数的参数之一。同时还要传入前面处理优化的表达式树。

    public BuildInfo(IBuildContext parent, Expression expression, SelectQuery selectQuery)
    {
         Parent = parent;
         Expression = expression;
         SelectQuery = selectQuery;
    }

    相信大家都会明白这些信息跟后面各个XxxxBuilder类处理要用到的信息有关系。笔者就不在这边都讲了。读者们这一部可以自己去查看代码。表达式树经历了上面XxxxBuilder类处理之后。相关信息都会被提取存放在SelectQuery类实例里面。但是传到后面却要用到IBuildContext接口。大家可以认为也是一个上下文的概念。如下

    _query.Init(sequence, CurrentSqlParameters);

    到了这一步提取SQL要用的信息算是结束了。也是通过Query<T>类的Init方法来设置后面要用的信息。如下

     1 public override void Init(IBuildContext parseContext, List<ParameterAccessor> sqlParameters)
     2 {
     3             Queries.Add(new QueryInfo
     4             {
     5                 SelectQuery = parseContext.SelectQuery,
     6                 Parameters = sqlParameters,
     7             });
     8 
     9             ContextID = parseContext.Builder.DataContextInfo.ContextID;
    10             MappingSchema = parseContext.Builder.MappingSchema;
    11             SqlProviderFlags = parseContext.Builder.DataContextInfo.SqlProviderFlags;
    12             SqlOptimizer = parseContext.Builder.DataContextInfo.GetSqlOptimizer();
    13             Expression = parseContext.Builder.OriginalExpression;
    14 }

    结束语


    处理表达式树,优化表达式树,提取生成SQL的信息。可以看到作者在生成SQL语句思考了很多。至于生成SQL句语的部分后面一章会讲到。也是最后一章。

  • 相关阅读:
    算术运算符
    短路运算
    基本运算符
    类型转换
    数据类型讲解
    关键字
    河北省重大技术需求征集八稿第六天
    河北省重大技术需求征集八稿第五天
    河北省重大技术需求征集八稿第四天
    河北省重大技术需求征集八稿第三天
  • 原文地址:https://www.cnblogs.com/hayasi/p/6079087.html
Copyright © 2020-2023  润新知