操作方法:LINQ To SQL 转换 - 第二部分






4.16/5 (7投票s)
关于 LINQ to SQL 转换的文章。
引言
本文是系列文章的第二篇,概述了如何将 LINQ 表达式树转换为 SQL 语句,这些语句不仅可以针对 Microsoft 的 SQL Server 产品执行,还可以针对多个 RDBMS 系统执行。文章还将说明如何
- 正确且全面地翻译具有有效 SQL 翻译的二元和一元表达式。
- 翻译具有 SQL 等效项的函数调用(例如
customer.FirstName.ToUpper()
)。 - 实现
GroupBy
。 - 实现
IQueryable
方法ANY
、ALL
、COUNT
、AVERAGE
等。 - 参数化查询,而不是在 SQL 转换中嵌入常量。
- 缓存先前翻译的表达式树。
- 可能不使用 MARS。
背景
在本系列的上一篇文章中,我概述了如何实现实现中使用的类,即 Binder
。本质上,binder 的功能是将 DbDataReader
中的值分配给给定类的新实例化对象。
动机示例将是以下 LINQ 查询
var customers = from customer in customers
where customer.City == city
select new { Name = customer.ContactName,
Phone = customer.Phone };
这将转换为以下 SQL 语句
SELECT t0.ContactName, t0.Phone
FROM dbo.Customers AS t0
WHERE (t0.City = @p0)
以及以下方法调用
.Where(customer => (customer.City = value(LinqTest.NorthwindLinq+<>c__DisplayClass1).city))
.Select(customer => new <>f__AnonymousType0`2(Name = customer.ContactName,
Phone = customer.Phone))
Binder
将转换 Lambda 表达式
customer => new <>f__AnonymousType0`2(Name = customer.ContactName,
Phone = customer.Phone)
to
reader => new <>f__AnonymousType0`2(Name = IIF(Not(reader.IsDBNull(0)),
reader.GetString(0), Convert(null)),
Phone = IIF(Not(reader.IsDBNull(1)),
reader.GetString(1), Convert(null)))
Binder:深入解析
为了执行上述转换,Binder
需要执行以下任务
- 确定
DbDataReader
中与给定字段对应的位置(或索引,如果您愿意)。 - 确定我们要从
DbDataReader
中检索的字段类型,并生成适当的表达式。 - 处理我们动机示例中未说明的几个特殊情况。为便于阅读,将在文章末尾讨论这些情况。
在动机示例中,字段 customer.ContactName
将位于 DbDataReader
的索引 0 处。
在动机示例中,customer.ContactName
是一个字符串,位于索引 0 处。因此,我们需要生成一个对 reader.GetString(0)
的调用。
让我们从字段声明开始
private class Binder : ExpressionVisitor {
private readonly LambdaExpression selector = null;
private readonly LambdaExpression binderLambda = null;
private readonly Delegate binderMethod = null;
private readonly Dictionary<string,> fieldPositions = new Dictionary<string,>();
private readonly ParameterExpression reader =
Expression.Parameter(typeof(DbDataReader), "reader");
private static readonly MethodInfo getBoolean =
typeof(DbDataReader).GetMethod("GetBoolean");
private static readonly MethodInfo getByte =
typeof(DbDataReader).GetMethod("GetByte");
private static readonly MethodInfo getChar =
typeof(DbDataReader).GetMethod("GetChar");
private static readonly MethodInfo getDateTime =
typeof(DbDataReader).GetMethod("GetDateTime");
private static readonly MethodInfo getDecimal =
typeof(DbDataReader).GetMethod("GetDecimal");
private static readonly MethodInfo getDouble =
typeof(DbDataReader).GetMethod("GetDouble");
private static readonly MethodInfo getGUID =
typeof(DbDataReader).GetMethod("GetGuid");
private static readonly MethodInfo getInt16 =
typeof(DbDataReader).GetMethod("GetInt16");
private static readonly MethodInfo getInt32 =
typeof(DbDataReader).GetMethod("GetInt32");
private static readonly MethodInfo getInt64 =
typeof(DbDataReader).GetMethod("GetInt64");
private static readonly MethodInfo getString =
typeof(DbDataReader).GetMethod("GetString");
private static readonly MethodInfo getValue =
typeof(DbDataReader).GetMethod("GetValue");
------------------------------------------------------------------------------------------
}
- 选择器字段将保存对我们正在转换的 Lambda 表达式的引用。
binderLambda
字段将保存转换结果的引用。binderMethod
是一个委托,它将通过调用binderLambda.Compile()
来生成。fieldPositions
字段是一个字典,它将跟踪我们需要从DbDataReader
中检索的字段及其位置。reader
是一个DbDataReader
类型的参数,Lambda 表达式将引用它。MethodInfo
类型的字段用于在reader
中查找字段并按所需类型返回所需值。
在动机示例中,它将是表达式
customer => new <>f__AnonymousType0`2(Name = customer.ContactName,
Phone = customer.Phone)
在动机示例中,它将是表达式
reader => new <>f__AnonymousType0`2(Name = IIF(Not(reader.IsDBNull(0)),
reader.GetString(0), Convert(null)),
Phone = IIF(Not(reader.IsDBNull(1)),
reader.GetString(1), Convert(null)))
概念上,它将这样使用
<>f__AnonymousType0`2 anyonymousType =
(<>f__AnonymousType0`2)binder.DynamicInvoke(reader)
其使用细节将在下一篇文章中涵盖。
在动机示例中,它将是表达式
reader => new <>f__AnonymousType0`2(Name = IIF(Not(reader.IsDBNull(0)),
reader.GetString(0), Convert(null)),
Phone = IIF(Not(reader.IsDBNull(1)),
reader.GetString(1), Convert(null)))
我不会逐一介绍每个方法的作用;而是提供更详细的概念概述。
概念概述
像这样的 LINQ 表达式
customer => new <>f__AnonymousType0`2(Name = customer.ContactName,
Phone = customer.Phone)
是一个由一系列节点组成的树。
在上面的示例中,表达式树的概念上看起来是这样的
Expression.Lambda(Expression.New(anonymousType2Constructor,
new Expression[]{
Expression.MakeMemberAccess(
Expression.Parameter(typeof(customer, "customer"),
typeof(Customer).GetProperty("ContactName")),
Expression.MakeMemberAccess(
Expression.Parameter(customer "customer"),
typeof(Customer).GetProperty("Phone"))
}));
您会从上面回忆起,我们将使用的 reader 将通过执行如下所示的 SQL 语句生成
SELECT t0.ContactName, t0.Phone
FROM dbo.Customers AS t0
WHERE (t0.City = @p0)
需要注意的关键是,DbDataReader
中的字段将按照它们在 Lambda 表达式中访问的相同顺序返回,因此 ContactName
将位于 DbDataReader
的位置 0,Phone
将位于 DbDataReader
的位置 1。这本质上是我们知道哪些字段位于什么位置的方式。
现在,我们只需要用对 reader
参数的引用替换对 customer
参数的引用,然后用对适当的 reader.GetXXX(fieldPosition)
方法的调用替换对 customer
参数的属性/字段的引用。例如:
替换
Expression.MakeMemberAccess(Expression.Parameter(typeof(Customer), "customer"),
typeof(Customer).GetProperty("ContactName"))
用
Expression.Call(reader, getString, Expression.Constant(0));
为了进行这些更改,我们必须能够检查表达式树中的每个节点,并在必要时更改该节点。这就是第一部分中讨论的 ExpressionVisitor
发挥作用的地方。我们继承 ExpressionVisitor
并重写以下方法
protected override Expression VisitMemberAccess(MemberExpression m) {
------------
}
本质上就是这样。
注释
- 方法调用
- 不产生投影的 Lambda 查询
假设我们有一个 Lambda 查询,例如
var x = from customer in customers
where customer.City == city
select new { Name = customer.ContactName,
OrderCount = customer.Orders.Count() };
我们应该期望生成一个 SQL 语句,如下所示
SELECT t0.ContactName,
(
SELECT Count(*)
FROM dbo.Orders AS t2
WHERE (t2.CustomerID = t0.CustomerID)
)
AS OrderCount
FROM dbo.Customers AS t0
WHERE (t0.City = @p0)
我们需要拦截对
customer.Orders.Count()
请参阅源代码了解详情。
假设我们有一个 Lambda 查询,例如
var x = from customer in customers
where customer.City == city
select customer;
我们需要实例化一个 customer
类型的对象,然后将每个具有相应数据库列的属性与 DbDataReader
中的相应值进行赋值。
请参阅源代码了解详情。
Binder 类列表
private class Binder : ExpressionVisitor {
private readonly LambdaExpression selector = null;
private readonly LambdaExpression binderLambda = null;
private readonly Delegate binderMethod = null;
private readonly Dictionary<string,> fieldPositions = new Dictionary<string,>();
private readonly ParameterExpression reader =
Expression.Parameter(typeof(DbDataReader), "reader");
private static readonly MethodInfo getBoolean =
typeof(DbDataReader).GetMethod("GetBoolean");
private static readonly MethodInfo getByte =
typeof(DbDataReader).GetMethod("GetByte");
private static readonly MethodInfo getChar =
typeof(DbDataReader).GetMethod("GetChar");
private static readonly MethodInfo getDateTime =
typeof(DbDataReader).GetMethod("GetDateTime");
private static readonly MethodInfo getDecimal =
typeof(DbDataReader).GetMethod("GetDecimal");
private static readonly MethodInfo getDouble =
typeof(DbDataReader).GetMethod("GetDouble");
private static readonly MethodInfo getGUID =
typeof(DbDataReader).GetMethod("GetGuid");
private static readonly MethodInfo getInt16 =
typeof(DbDataReader).GetMethod("GetInt16");
private static readonly MethodInfo getInt32 =
typeof(DbDataReader).GetMethod("GetInt32");
private static readonly MethodInfo getInt64 =
typeof(DbDataReader).GetMethod("GetInt64");
private static readonly MethodInfo getString =
typeof(DbDataReader).GetMethod("GetString");
private static readonly MethodInfo getValue =
typeof(DbDataReader).GetMethod("GetValue");
public Delegate BinderMethod {
get {
return binderMethod;
}
}
public Binder(LambdaExpression selector) {
this.selector = selector;
if (selector.Body.NodeType != ExpressionType.Parameter) {
binderLambda = Expression.Lambda(((LambdaExpression)this.Visit(selector)).Body,
reader);
}
else {
binderLambda = GetBindingLambda(selector);
}
binderMethod = binderLambda.Compile();
}
protected override Expression VisitMethodCall(MethodCallExpression m) {
switch (m.Method.Name) {
case "Count":
case "Average":
case "Max":
case "Min":
case "Sum":
break;
default:
return base.VisitMethodCall(m);
}
Debug.Assert(m.Arguments.Count > 0);
Debug.Assert(m.Arguments[0].NodeType == ExpressionType.MemberAccess);
if (GetAccessedType(m.Arguments[0] as MemberExpression) !=
selector.Parameters[0].Type) {
return m;
}
int fieldPosition = GetFieldPosition(m.ToString());
return GetFieldReader(m, fieldPosition);
}
protected override Expression VisitMemberAccess(MemberExpression m) {
Debug.Assert(selector.Parameters.Count == 1);
if (GetAccessedType(m) != selector.Parameters[0].Type) {
return m;
}
int fieldPosition = GetFieldPosition(m);
return GetFieldReader(m, fieldPosition);
}
private Expression GetFieldReader(Expression m, int fieldPosition) {
var field = Expression.Constant(fieldPosition, typeof(int));
var readerExpression = GetReaderExpression(m, field);
var isDbNull = Expression.Call(reader,
typeof(DbDataReader).GetMethod("IsDBNull"),
field);
var conditionalExpression =
Expression.Condition(Expression.Not(isDbNull),
readerExpression,
Expression.Convert(Expression.Constant(null),
readerExpression.Type));
return conditionalExpression;
}
private Expression GetReaderExpression(Expression m, ConstantExpression field) {
MethodInfo getReaderMethod = GetReaderMethod(m);
var readerExpression = Expression.Call(reader, getReaderMethod, field);
if (getReaderMethod.ReturnType == m.Type) {
return readerExpression;
}
return Expression.Convert(readerExpression, m.Type);
}
private static MethodInfo GetReaderMethod(Expression m) {
Type memberType = GetMemberType(m);
MethodInfo getMethod = null;
switch (Type.GetTypeCode(memberType)) {
case TypeCode.Boolean:
getMethod = getBoolean;
break;
case TypeCode.Byte:
getMethod = getByte;
break;
case TypeCode.Char:
getMethod = getChar;
break;
case TypeCode.DateTime:
getMethod = getDateTime;
break;
case TypeCode.Decimal:
getMethod = getDecimal;
break;
case TypeCode.Double:
getMethod = getDouble;
break;
case TypeCode.Int16:
getMethod = getInt16;
break;
case TypeCode.Int32:
getMethod = getInt32;
break;
case TypeCode.Int64:
getMethod = getInt64;
break;
case TypeCode.String:
getMethod = getString;
break;
case TypeCode.Object:
getMethod = getValue;
break;
default:
if (m.Type == typeof(Guid)) {
getMethod = getGUID;
}
else {
getMethod = getValue;
}
break;
}
return getMethod;
}
private int GetFieldPosition(MemberExpression m) {
return GetFieldPosition(m.Member.Name);
}
private int GetFieldPosition(string fieldName) {
int fieldPosition = 0;
if (fieldPositions.ContainsKey(fieldName)) {
fieldPosition = fieldPositions[fieldName];
return fieldPosition;
}
fieldPosition = fieldPositions.Count();
fieldPositions.Add(fieldName, fieldPosition);
return fieldPosition;
}
private static Type GetMemberType(Expression m) {
Type memberType = null;
if (m.Type.Name == "Nullable`1") {
memberType = m.Type.GetGenericArguments()[0];
}
else {
memberType = m.Type;
}
return memberType;
}
private static Type GetAccessedType(MemberExpression m) {
if (m.Expression.NodeType == ExpressionType.MemberAccess) {
return GetAccessedType((MemberExpression)m.Expression);
}
return m.Expression.Type;
}
private LambdaExpression GetBindingLambda(LambdaExpression selector) {
var instanceType = selector.Body.Type;
// this is a hack
var properties = (from property in instanceType.GetProperties()
where property.PropertyType.IsValueType ||
property.PropertyType == typeof(string)
orderby property.Name
select instanceType.GetField("_" + property.Name,
BindingFlags.Instance |
BindingFlags.NonPublic))
.ToArray();
var bindings = new MemberBinding[properties.Length];
for (int i = 0; i < properties.Length; i++) {
var callMethod = GetFieldReader(
Expression.MakeMemberAccess(
Expression.Parameter(instanceType, "param"),
properties[i]),
i);
bindings[i] = Expression.Bind(properties[i], callMethod);
}
return Expression.Lambda(Expression.MemberInit(Expression.New(instanceType),
bindings),
reader);
}
}