65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.16/5 (7投票s)

2008 年 1 月 9 日

LGPL3

4分钟阅读

viewsIcon

47248

关于 LINQ to SQL 转换的文章。

引言

本文是系列文章的第二篇,概述了如何将 LINQ 表达式树转换为 SQL 语句,这些语句不仅可以针对 Microsoft 的 SQL Server 产品执行,还可以针对多个 RDBMS 系统执行。文章还将说明如何

  1. 正确且全面地翻译具有有效 SQL 翻译的二元和一元表达式。
  2. 翻译具有 SQL 等效项的函数调用(例如 customer.FirstName.ToUpper())。
  3. 实现 GroupBy
  4. 实现 IQueryable 方法 ANYALLCOUNTAVERAGE 等。
  5. 参数化查询,而不是在 SQL 转换中嵌入常量。
  6. 缓存先前翻译的表达式树。
  7. 可能不使用 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 需要执行以下任务

  1. 确定 DbDataReader 中与给定字段对应的位置(或索引,如果您愿意)。
  2. 在动机示例中,字段 customer.ContactName 将位于 DbDataReader 的索引 0 处。

  3. 确定我们要从 DbDataReader 中检索的字段类型,并生成适当的表达式。
  4. 在动机示例中,customer.ContactName 是一个字符串,位于索引 0 处。因此,我们需要生成一个对 reader.GetString(0) 的调用。

  5. 处理我们动机示例中未说明的几个特殊情况。为便于阅读,将在文章末尾讨论这些情况。

让我们从字段声明开始

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");

------------------------------------------------------------------------------------------
}
  1. 选择器字段将保存对我们正在转换的 Lambda 表达式的引用。
  2. 在动机示例中,它将是表达式

    customer => new <>f__AnonymousType0`2(Name = customer.ContactName,       
                                          Phone = customer.Phone)
  3. binderLambda 字段将保存转换结果的引用。
  4. 在动机示例中,它将是表达式

    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)))
  5. binderMethod 是一个委托,它将通过调用 binderLambda.Compile() 来生成。
  6. 概念上,它将这样使用

    <>f__AnonymousType0`2 anyonymousType = 
                  (<>f__AnonymousType0`2)binder.DynamicInvoke(reader)

    其使用细节将在下一篇文章中涵盖。

  7. fieldPositions 字段是一个字典,它将跟踪我们需要从 DbDataReader 中检索的字段及其位置。
  8. reader 是一个 DbDataReader 类型的参数,Lambda 表达式将引用它。
  9. 在动机示例中,它将是表达式

    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)))
  10. MethodInfo 类型的字段用于在 reader 中查找字段并按所需类型返回所需值。

我不会逐一介绍每个方法的作用;而是提供更详细的概念概述。

概念概述

像这样的 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) {
    ------------
}

本质上就是这样。

注释

  1. 方法调用
  2. 假设我们有一个 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()

    请参阅源代码了解详情。

  3. 不产生投影的 Lambda 查询
  4. 假设我们有一个 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);
    }
}
© . All rights reserved.