处理表达式树可以说是所有要实现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。参数理解起来并不难。可是构造函数里面的代码却让笔者很头痛。笔者只能知道做什么却很难理解为什么要这样子做。

 public ExpressionBuilder(Query query, IDataContextInfo dataContext, Expression expression, ParameterExpression[] compiledParameters)
{
_query = query;
_expressionAccessors = expression.GetExpressionAccessors(ExpressionParam); CompiledParameters = compiledParameters;
DataContextInfo = dataContext;
OriginalExpression = expression; _visitedExpressions = new HashSet<Expression>();
Expression = ConvertExpressionTree(expression);
_visitedExpressions = null; if (Configuration.AvoidSpecificDataProviderAPI)
{
DataReaderLocal = DataReaderParam;
}
else
{
DataReaderLocal = BuildVariable(Expression.Convert(DataReaderParam, dataContext.DataContext.DataReaderType), "ldr");
}
}

红色代码部分便是笔者不能理解的部分。对于第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类:

 internal Query<T> Build<T>()
{
var sequence = BuildSequence(new BuildInfo((IBuildContext)null, Expression, new SelectQuery())); if (_reorder)
lock (_sync)
{
_reorder = false;
_sequenceBuilders = _sequenceBuilders.OrderByDescending(_ => _.BuildCounter).ToList();
} _query.Init(sequence, CurrentSqlParameters); var param = Expression.Parameter(typeof(Query<T>), "info"); sequence.BuildQuery((Query<T>)_query, param); return (Query<T>)_query;
}

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

Query类:

 public override void Init(IBuildContext parseContext, List<ParameterAccessor> sqlParameters)
{
Queries.Add(new QueryInfo
{
SelectQuery = parseContext.SelectQuery,
Parameters = sqlParameters,
});
}

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

ExpressionBuilder类:

 ParameterAccessor BuildParameter(Expression expr)
{
ParameterAccessor p; if (_parameters.TryGetValue(expr, out p))
return p; string name = null; var newExpr = ReplaceParameter(_expressionAccessors, expr, nm => name = nm); p = CreateParameterAccessor(
DataContextInfo.DataContext, newExpr, expr, ExpressionParam, ParametersParam, name); _parameters.Add(expr, p);
CurrentSqlParameters.Add(p); return p;
}

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

int n2 = ;
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, ).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 > );

通过OptimizeExpression方法之后

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

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

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

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

生成相关的SQL信息


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

ExpressionBuilder类:

 public IBuildContext BuildSequence(BuildInfo buildInfo)
{
buildInfo.Expression = buildInfo.Expression.Unwrap(); var n = _builders[].BuildCounter; foreach (var builder in _builders)
{
if (builder.CanBuild(this, buildInfo))
{
var sequence = builder.BuildSequence(this, buildInfo); lock (builder)
builder.BuildCounter++; _reorder = _reorder || n < builder.BuildCounter; return sequence;
} n = builder.BuildCounter;
} throw new LinqException("Sequence '{0}' cannot be converted to SQL.", buildInfo.Expression);
}

所有用于的提取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方法来设置后面要用的信息。如下

 public override void Init(IBuildContext parseContext, List<ParameterAccessor> sqlParameters)
{
Queries.Add(new QueryInfo
{
SelectQuery = parseContext.SelectQuery,
Parameters = sqlParameters,
}); ContextID = parseContext.Builder.DataContextInfo.ContextID;
MappingSchema = parseContext.Builder.MappingSchema;
SqlProviderFlags = parseContext.Builder.DataContextInfo.SqlProviderFlags;
SqlOptimizer = parseContext.Builder.DataContextInfo.GetSqlOptimizer();
Expression = parseContext.Builder.OriginalExpression;
}

结束语


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

LinqToDB 源码分析——处理表达式树的更多相关文章

  1. LinqToDB 源码分析——生成表达式树

    当我们知道了Linq查询要用到的数据库信息之后.接下就是生成对应的表达式树.在前面的章节里面笔者就已经介绍过.生成表达式树是事实离不开IQueryable<T>接口.而处理表达式树离不开I ...

  2. 死磕以太坊源码分析之MPT树-上

    死磕以太坊源码分析之MPT树-上 前缀树Trie 前缀树(又称字典树),通常来说,一个前缀树是用来存储字符串的.前缀树的每一个节点代表一个字符串(前缀).每一个节点会有多个子节点,通往不同子节点的路径 ...

  3. 死磕以太坊源码分析之MPT树-下

    死磕以太坊源码分析之MPT树-下 文章以及资料请查看:https://github.com/blockchainGuide/ 上篇主要介绍了以太坊中的MPT树的原理,这篇主要会对MPT树涉及的源码进行 ...

  4. LinqToDB 源码分析——生成与执行SQL语句

    生成SQL语句的功能可以算是LinqToDB框架的最后一步.从上一章中我们可以知道处理完表达式树之后,相关生成SQL信息会被保存在一个叫SelectQuery类的实例.有了这个实例我们就可以生成对应的 ...

  5. LinqToDB 源码分析——设计原理

    我们知道实现了IQueryable<T>接口和IQueryProvider接口就可以使用Linq To SQL的功能.关于如何去实现的话,上一章也为我们引导了一个方向.LinqToDB框架 ...

  6. LinqToDB 源码分析——DataContext类

    LinqToDB框架是一个轻量级的ORM框架.当然,功能上来讲一定比不上Entity Framework的强大.但是在使用上总让笔者感觉有一点Entity Framework的影子.笔者想过可能的原因 ...

  7. LinqToDB 源码分析——轻谈Linq查询

    LinqToDB框架最大的优势应该是实现了对Linq的支持.如果少了这一个功能相信他在使用上的快感会少了一个层次.本来笔者想要直接讲解LinqToDB框架是如何实现对Linq的支持.写到一半的时候却发 ...

  8. LinqToDB 源码分析——前言

    记得笔者进入公司的时候接触的第一个ORM框架是Entity Framework.为了Entity Framework也看了不些的英文资料(不是笔者装B哦).正式使用三个月后.笔者对他有一个全面性的认识 ...

  9. Android应用层View绘制流程与源码分析

    1  背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...

随机推荐

  1. 菜鸟学Struts2——Actions

    在对Struts2的工作原理学习之后,对Struts2的Action进行学习.主要对Struts2文档Guides中的Action分支进行学习,如下图: 1.Model Driven(模型驱动) St ...

  2. 高大上的微服务可以很简单,使用node写微服务

    安装 npm install m-service --save 使用 编写服务处理函数 // dir1/file1.js // 使用传入的console参数输出可以自动在日志里带上request id ...

  3. python黑魔法 -- 内置方法使用

    很多pythonic的代码都会用到内置方法,根据自己的经验,罗列一下自己知道的内置方法. __getitem__ __setitem__ __delitem__ 这三个方法是字典类的内置方法,分别对应 ...

  4. 如何利用ETW(Event Tracing for Windows)记录日志

    ETW是Event Tracing for Windows的简称,它是Windows提供的原生的事件跟踪日志系统.由于采用内核(Kernel)层面的缓冲和日志记录机制,所以ETW提供了一种非常高效的事 ...

  5. 破解SQLServer for Linux预览版的3.5GB内存限制 (UBUNTU篇)

    在上一篇中我提到了如何破解RHEL上SQLServer的内存大小限制,但是Ubuntu上还有一道检查 这篇我将会讲解如何在3.5GB以下内存的Ubuntu中安装和运行SQLServer for Lin ...

  6. Java消息队列--ActiveMq 实战

    1.下载安装ActiveMQ ActiveMQ官网下载地址:http://activemq.apache.org/download.html ActiveMQ 提供了Windows 和Linux.Un ...

  7. 后缀数组的倍增算法(Prefix Doubling)

    后缀数组的倍增算法(Prefix Doubling) 文本内容除特殊注明外,均在知识共享署名-非商业性使用-相同方式共享 3.0协议下提供,附加条款亦可能应用. 最近在自学习BWT算法(Burrows ...

  8. ESLint的使用笔记

    原文地址:https://csspod.com/getting-started-with-eslint/?utm_source=tuicool&utm_medium=referral 在团队协 ...

  9. Spring异步功能

    使用 Spring 的异步功能时,实质是使用的 Servlet3 及以上版本的异步功能. Spring 的异步处理机制需要在 web.xml 中全部的 servlet 和 filter 处配置 < ...

  10. Leetcode 笔记 116 - Populating Next Right Pointers in Each Node

    题目链接:Populating Next Right Pointers in Each Node | LeetCode OJ Given a binary tree struct TreeLinkNo ...