分类目录

链接

2017 年 1 月
 1
2345678
9101112131415
16171819202122
23242526272829
3031  

近期文章

热门标签

新人福利,免费薅羊毛

现在位置:    首页 > .NET > 正文
轻量级ORM框架(六):处理表达式树
.NET 暂无评论 阅读(1,153)

处理表达式树可以说是所有要实现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句语的部分后面一章会讲到。也是最后一章。

 

============ 欢迎各位老板打赏~ ===========

本文版权归Bruce's Blog所有,转载引用请完整注明以下信息:
本文作者:Bruce
本文地址:轻量级ORM框架(六):处理表达式树 | Bruce's Blog

发表评论

留言无头像?