C# Entity Framework中的IQueryable和IQueryProvider详解
前言
相信大家对entity framework一定不陌生,我相信其中linq to sql是其最大的亮点之一,但是我们一直使用到现在却不曾明白内部是如何实现的,今天我们就简单的介绍iqueryable和iqueryprovider。
iqueryable接口
我们先聊聊这个接口,因为我们在使用ef中经常看到linq to sql语句的返回类型是iqueryable,我们可以看下这个接口的结构:
public interface iqueryable : ienumerable
{
type elementtype { get; }
expression expression { get; }
iqueryprovider provider { get; }
}
或许会有人很奇怪,当我们在开发过程中使用这个接口的时候,提供的方法远远不止这么点,因为微软提供了强大的queryable类,当然大家不要以为这个类是实现iqueryable然后实现了很多方法,如果是那样那些第三方库怎么自定义呢?所以queryable只是一个静态类,对iqueryable接口进行了扩展,下面是笔者在.net reflector截图中一部分:
如果读者细心一点会发现linq to sql并不会导致实际的查询,只有当我们真正开始使用的时候才从数据库中开始查询数据。
iqueryprovider接口
如果我们调试的ef的话,会看到生成的t-sql语句。t-sql就是根据表达式树分析从而得出的,而核心就是iqueryprovider接口,下面就是该接口的结构:
public interface iqueryprovider
{
iqueryable createquery(expression expression);
iqueryable<telement> createquery<telement>(expression expression);
object execute(expression expression);
tresult execute<tresult>(expression expression);
}
其中createquery就是负责解析表达式树的,当然还要将处理后的结果返回,以便接着分析下面的语句,当然这中间只是分析,你完全可以根据表达式树得出你自己需要的查询语句,比如sql或者其他什么,只有在真正使用数据的时候才会调用execute方法,这个时候就可以根据我们自己分析的语句开始进行实际的查询了。
实例分析
queryprovider类
光说不练我们永远不能明白其中的原理,所以下面我们就简单的举一个例子来展示下。首先我们先实现iqueryprovider接口,其中会用到一个query类,这个类会在后面进行介绍,首先我们新建一个queryprovider类实现iqueryprovider接口,首先我们看下createquery<s>方法:
这里的expression就是传递给我们,并且需要我们处理的表达式树,最后还要返回实现iqueryable<s>接口的示例,以便linq在此基础上进行下面的查询,这里我们仅仅只是创建了一个query的实例,同时将expression传递给它,因为此处仅仅只是一个demo,所以我们没有去真正解析表达式树(这其中要做的工作很多)。接着还有createquery方法:
我们可以看到下面这句话:
实际的含义就是创建query<>的实例,并且泛型参数是elementtype,参数是this和expression。
最后就是execute方法了,传递一个expression参数,并获取最后的结果,笔者在这里直接是写死的值:
query类
仅仅只有queryprovider还没用,我们还需要一个能够保存表达式树状态的类,当然也包括了我们解析表达式后的结果也可以保存在其中,这样我们在iqueryprovider的execute方法中就可以根据我们解析的结果执行执行并返回结果了。
这里我们可以看到query的expression值在创建这个实例时,如果没有传递expression参数时该值就是:
但是在后面的过程中query中的expression将是queryprovider中的expression值。
到此我们其实就完成了一个简单的示例了,我们就可以开始测试我们的成果了,笔者在利用如下的代码来测试:
ok,我们开始看看是如何分析这句linq语句的。
首先我们看下在一开始执行时query中expression的返回值(如下图):
在获取到这个表达式后,就开始执行linq,首先执行的是where item == 123。
分析where item == 123
接着我们f5,就可以看到在queryprovider中的createquery<s>命中了,并且expression参数如下图所示:
我们看到里面的字符串是 where(item => (item == 123)),通过这句话我们就可以明白其实linq中的where实质上就是利用where方法,并传递给它对应的lambda表达式。分析完了where部分,下面就是firstordefault部分了。
分析firstordefault
当执行到firstordefault的时候我们可以查看t的值,会发现t实际上就是queryprovider中createquery<s>的返回值。
接着我们开始执行下面firstordefault方法,发现会再一次的去获取expression的值,而此时expression的值就是上面createquery<t>传递给我们的参数expression。
然后在将这个表达式树和由表达式树表示firstordefault方法调用的值拼接起来,并调用queryprovider中的execute<s>方法,我们可以看到这个时候传递给我们的参数expression的值。
至此一个简单的流程就结束了,最后就是返回笔者写死的123这个值了。
通过上面这个例子我们基本了解了其工作的流程,下面我们将一步一步的分析我们这个where item == 123,当然我们将会用到递归,所以请大家整理好自己的思路,一步一步的看如何从一个表达式树中分析这条语句。
分析表达式树实战
首先我们一个分析表达式树的方法,这个方法我们暂且放在queryprovider中:
public void analysisexpression(expression exp)
{
switch (exp.nodetype)
{
case expressiontype.call:
{
methodcallexpression mce = exp as methodcallexpression;
console.writeline("the method is {0}", mce.method.name);
for (int i = 0; i < mce.arguments.count; i++)
{
analysisexpression(mce.arguments[i]);
}
}
break;
case expressiontype.quote:
{
unaryexpression ue = exp as unaryexpression;
analysisexpression(ue.operand);
}
break;
case expressiontype.lambda:
{
lambdaexpression le = exp as lambdaexpression;
analysisexpression(le.body);
}
break;
case expressiontype.equal:
{
binaryexpression be = exp as binaryexpression;
console.writeline("the method is {0}", exp.nodetype.tostring());
analysisexpression(be.left);
analysisexpression(be.right);
}
break;
case expressiontype.constant:
{
constantexpression ce = exp as constantexpression;
console.writeline("the value type is {0}", ce.value.tostring());
}
break;
case expressiontype.parameter:
{
parameterexpression pe = exp as parameterexpression;
console.writeline("the parameter is {0}", pe.name);
}
break;
default:
{
console.write("unknow");
}
break;
}
}
并在createquery<s>中调用这个方法
然后我们可以开始运行测试了,为了能够让读者明白当前处理的表达式树,所以在下面的截图中将会包含analysisexpression中参数exp的值,这样可以便于读者区分当前处理的表达式树。
ps:expression类型中的nodetype是非常重要的,因为传递给我们的都是父类expression类型,而我们需要根据nodetype的转换成对应的子类,这样我们才能够获取到更详细的信息。
expressiontype.call
我们根据一开始的exp的nodetype进入到这个分支,因为where实质上就是ss调用where方法,所以我们通过将exp转换成对应的methodcallexpression类型,这样我们就可以看到调用的方法名称了。
当然调用一个方法必须要有参数,所以下面还需要循环arguments去分析具体的参数,其中也包括调用这个方法的对象,自然我们首先是分析调用这个方法的对象,这里我们进行了第一次的递归调用,跳到了expressiontype.constant。
expressiontype.constant
nodetype为这个类型,我们就可以通过constantexpression类型来获取对应的参数,通过value我们可以可以获取到调用where方法的对象,当然到这里就不会继续往下分析了。
所以我们继续跳到之前的for循环,开始分析第二个参数,就是 item => item == 123这个部分了。
expressiontype.quote
如果接触过lambda的人可能会认为类型应该是lambda,但实际上不会直接跳转到那,而是先跳转到quote,然后我们再把转换成unaryexpression类型,然后再继续分析其中operand属性,而这个属性的nodetype就是lambda了。个人认为这个应该是区分lambda和普通的方法,因为where不仅仅可以接收lambda同时也可以是常规的方法,所以这里还需要这一层。
expressiontype.lambda
跳转到这,大家就不会感觉奇怪了,这里为了简洁。笔者并没有分析参数,而是直接分析body部分,因为这部分才是我们的关键。
expressiontype.equal
我们看到这个lambda很简单,就是一个相等比较,所以直接跳转到了equal,当然还有and、or等对应的枚举,而到了这一步我们就可以直接分析left和right,当然这里还有一个小插曲,就是在跳到这个枚举的时候我查看exp的类型时,实际上是logicalbinaryexpression类型,并不是binaryexpression类型,然后用reflector查看了下,我就呵呵了。
我当时还奇怪,怎么没有这个类型呢,最后才知道玩的是这一出。到此为止,我们继续分析这个相等操作的左右两边的参数吧。
首先分析的是左边参数item。
expressiontype.parameter
item挑传到这,并将其转换成parameterexpression类型,笔者在此仅仅只输出了参数的名称。
到这左边的参数分析完毕,我们开始分析右边的参数。
expressiontype.constant
我们可以轻松的想到对应的value就是123了,到此整个表达式就分析完毕了。
我们看看最后控制台的输出结果吧。
在此笔者还要声明一个问题,就是我们应该去理解我们使用的各种库的原理,这样便于我们以后添加符合实际开发的一些功能,当然这并不是浪费时间。而是提高今后项目开发的时间,随着不断的积累,我们会发现很多重复的功能并不需要我们去重复写了,而节省下来的时间我们就可以做自己想做的事了,所以我们要做一个有思想的懒程序员。