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