操作方法: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);
    }
}

