概要
在前面的文章中,本人曾经分析了IQueryable和 IEnumerable两个接口的异同点。但是整个分析过程,侧重于基本概念层面,本文从设计和代码应用的角度来分析它们的区别。
现象讨论
相比于IEnumerable,IQueryable多了一个Expression属性。
我们在Linq(IEnumerable)和EF(IQueryable)中,都会使用Where 方法,这两个方法从表现上看有些类似。但是它们有一处明显的区别:
IEnumable
public static System.Collections.Generic.IEnumerable Where (this System.Collections.Generic.IEnumerable source, Func<TSource,bool> predicate);
IQueryable
public static System.Linq.IQueryable Where (this System.Linq.IQueryable source, System.Linq.Expressions.Expression<Func<TSource,bool>> predicate);
从上面的定义上,可以看出,上面两个方法的参数类型,不完全相同,一个是委托类型,一个是以委托作为泛型参数的Expression类型。
基本分析
从设计和应用层面,IEnumerable的设计目的是管理内存操作,例如使用Where方法,对内存数据进行过滤。这个时候,只需要传入一个返回bool类型的委托,在迭代过程中调用该委托,根据返回值,过滤掉不需要的元素即可。
这对于IQueryable,显然是不够的。IQueryable要为ORM框架提供一个标准,EF或者NHibernate等三方框架,需要按照这个标准,将IQueryable下面的扩展方法和对应的参数,转换成SQL语句。例如将Where方法和其Predict参数转换成SQL中的where子句。
事实上,IQueryable需要做的是将委托类型的传入参数进行拆解,把它们变成一棵表达式目录树,第三方的框架在遍历这个树的过程中,逐一将内容转换成SQL。
代码举例
在代码层面,无论是IEnumerable和IQueryable,还是它们各自的Where扩展方法,主要区别都集中在Expression上。也就是说IQueryable可以通过Expression,做一些IEnumerable做不了的事情。
下面我们就使用表达式目录树,实现一个简单的SQL翻译过程,从而搞清楚到底IEnumerable为什么不能做这些。
基本案例
我们要在很多种类型的信用卡中找到币种是人民币并且一次刷卡金额在10000以下,或者是不能绑定微信的信用卡。
基本代码如下:
Expression<Func<CreditCard, bool>> whereQuery = el => el.CurrencyType == "RMB"
&& el.AmountLimitation > 10000
|| el.BindToWeChat == false;
我们按照上述要求构造了Expression。具体CreditCard的定义请见附录。
在我们构造好Expression后,也就生成了一棵表达式目录树,如下所示:
该树的根节点和分支节点是操作符,叶子节点是表达式。
我们就模仿EF,遍历这课表达式目录树,生成对应SQL,关键代码如下:
我们定义TranslateExpressionToSql类,该类继承自ExpressionVisitor,ExpressionVisitor能帮助我们递归遍历整棵树,我们可以将我们的逻辑嵌入到整个遍历过程中。_sqlAccumulator用于记录生成的SQL语句。
internal class TranslateExpressionToSql : ExpressionVisitor
{
private StringBuilder _sqlAccumulator = new StringBuilder();
public string Translate(Expression expression)
{
_sqlAccumulator = new StringBuilder(); ;
this.Visit(expression);
return " WHERE " + _sqlAccumulator.ToString();
}
protected override Expression VisitBinary(BinaryExpression node)
{
_sqlAccumulator.Append("(");
this.Visit(node.Left);
switch (node.NodeType)
{
case ExpressionType.And:
_sqlAccumulator.Append(" AND ");
break;
case ExpressionType.AndAlso:
_sqlAccumulator.Append(" AND ");
break;
case ExpressionType.Or:
_sqlAccumulator.Append(" OR ");
break;
case ExpressionType.OrElse:
_sqlAccumulator.Append(" OR ");
break;
case ExpressionType.Equal:
if (IsNullConstant(node.Right))
{
_sqlAccumulator.Append(" IS ");
}else
{
_sqlAccumulator.Append(" = ");
}
break;
case ExpressionType.NotEqual:
if (IsNullConstant(node.Right))
{
_sqlAccumulator.Append(" IS NOT ");
}
else
{
_sqlAccumulator.Append(" <> ");
}
break;
case ExpressionType.LessThan:
_sqlAccumulator.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
_sqlAccumulator.Append(" <= ");
break;
case ExpressionType.GreaterThan:
_sqlAccumulator.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
_sqlAccumulator.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", node.NodeType));
}
this.Visit(node.Right);
_sqlAccumulator.Append(")");
return node;
}
protected bool IsNullConstant (Expression exp)
{
return (exp.NodeType == ExpressionType.Constant && ((ConstantExpression)exp).Value == null);
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression != null && node.Expression.NodeType == ExpressionType.Parameter)
{
_sqlAccumulator.Append(node.Member.Name);
return node;
}
throw new NotSupportedException(string.Format("The member '{0}' is not supported", node.Member.Name));
}
protected override Expression VisitConstant(ConstantExpression node)
{
IQueryable q = node.Value as IQueryable;
if (q == null && node.Value == null)
{
_sqlAccumulator.Append("NULL ");
}else if (q == null)
{
switch (Type.GetTypeCode(node.Value.GetType())) {
case TypeCode.Boolean:
_sqlAccumulator.Append((bool)node.Value ? 1:0);
break;
case TypeCode.String:
_sqlAccumulator.Append("'");
_sqlAccumulator.Append(node.Value);
_sqlAccumulator.Append("'");
break;
case TypeCode.DateTime:
_sqlAccumulator.Append("'" );
_sqlAccumulator.Append(node.Value);
_sqlAccumulator.Append("'");
break;
default:
_sqlAccumulator.Append(node.Value);
break;
}
}
return node;
}
}
- Translate作为对外提供的方法,接收一个Expression表达式,该方法调用ExpressionVisitor基类的Visit方法,开始遍历整棵表达式目录树;
- VisitBinary是ExpressionVisitor的一个virtual方法。我们可以利用该方法,解析出表达式中的操作符,并将其翻译成SQL的操作符。
首先,我们覆盖该方法,加入我们自己的逻辑,即操作符的翻译过程,例如将委托中的“==”翻译成SQL的“=”;
其次,该方法可以遍历所有的二进制表达式,包括加、减、乘、除、乘方、按位、bool等表达式;本例子我们主要用它来遍历我们的三个bool表达式; - VisitMember方法是ExpressionVisitor的一个virtual方法。该方法可以帮助我们解析表达式中的成员变量;例如在本例中可以将el.CurrencyType翻译成表中列名CurrencyType;
- VisitConstantr方法是ExpressionVisitor的一个virtual方法。该方法可以帮助我们获取表达式中的常量。我们覆盖该方法,加入将委托中的常量转换成SQL中的常亮。例如将“RMB”转换成‘RMB’;
最后我使用上面的方法对表达式目录树进行SQL转换,代码如下:
using ExpressTree;
using System.Linq.Expressions;
var translator = new TranslateExpressionToSql();
Expression<Func<CreditCard, bool>> whereQuery = el => el.CurrencyType == "RMB"
&& el.AmountLimitation > 10000
|| el.BindToWeChat == false;
var sql = translator.Translate(whereQuery);
Console.WriteLine(sql);
最后输出如下:
总结
IQueryable 的主要作用是通过其扩展方法和调用过程中使用的委托参数,将这些内容转换成表达式目录树。第三方的ORM框架可以在此基础上进行SQL语句的转换。
附录
internal class Entity
{
public int Id { get; set; }
}
internal class CreditCard : Entity
{
public int AmountLimitation { get; set; }
public bool BindToWeChat { get; set; }
public string CurrencyType { get; set; }
}