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

如何 LINQ To SQL:第三部分

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (6投票s)

2008 年 1 月 10 日

LGPL3

3分钟阅读

viewsIcon

48793

如何 LINQ To SQL:执行器。

引言

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

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

背景

在本系列上一篇文章中,我详细介绍了如何实现此实现中使用的一个类——Binder。本质上,Binder 的功能是将 DbDataReader 中的值分配给给定类的已实例化对象。如果您还没有这样做,请参阅上一篇文章以获取详细信息。

执行器类

顾名思义,Executor 类的目的是执行 SQL 语句,并将生成的 DbDataReader 中的结果返回为 IEnumerable<T>。我将使用上一篇文章中相同的示例来解释确切的含义。

var customers = from customer in customers
                where customer.City == city
                select new { Name = customer.ContactName, 
                             Phone = customer.Phone };

执行器:深入分析

上面的 LINQ 查询将转换为以下 SQL 语句

SELECT t0.ContactName, t0.Phone
FROM dbo.Customers AS t0
WHERE (t0.City = @p0)

Executor 需要以下内容才能执行其功能

  1. 到被查询数据库的 DbConnection
  2. 要执行的 SQL 语句。
  3. 产生上述 SQL 语句的表达式。(在我们上面的示例中,您会注意到该表达式包含一个名为 city 的变量。我们需要此变量的值才能初始化参数 @p0。)
  4. 一个接受 DbDataReader 并返回类型为 T 的对象的委托。在我们上面的示例中,T 是一个具有两个属性 NamePhone 的匿名类型。此委托是从上一篇文章中讨论的 Binder 获取的。

Executor 类的字段声明如下

private class Executor<T> : ExpressionVisitor, IEnumerable<T> {

    private readonly DbConnection connection = null;
    private readonly SqlExpressionParser sqlExpressionParser = null;
    private readonly Func<DbDataReader, T>  binder = null;
    private readonly List<object> parameters = new List<object>();

---------------------------------------------------------------------------
}

Executor 类继承自一个名为 ExpressionVisitor 的类(有关更多详细信息,请参阅第 1 部分和第 2 部分),并实现 IEnumerable<T>sqlExpressionParser 负责提供要执行的 SQL 语句以及产生该语句的表达式。

查询参数化

为了检索 SQL 语句(如果存在)所需的参数,我们必须检查产生该语句的表达式。然后,我们检索其中嵌入的常量,并将它们添加到我们的参数列表中。此操作如下执行

public Executor(DbConnection connection, 
                SqlExpressionParser sqlExpressionParser,
                Delegate binder) 
{

this.Visit(sqlExpressionParser.expression);
---------------------------------------------------------------------
}

protected override Expression VisitConstant(ConstantExpression c) 
{
      if (c.Value == null) {
          parameters.Add("NULL");
      }
      else {
          switch (Type.GetTypeCode(c.Value.GetType())) {
                case TypeCode.Boolean:
                      parameters.Add(((bool)c.Value) ? 1 : 0);
                        break;
                  case TypeCode.String:
                        parameters.Add(c.Value);
                        break;
                  case TypeCode.Object:
                        break;
                  default:
                      parameters.Add(c.Value.ToString());
                        break;
            }
      }
      return c;
}

还有一个额外的复杂性,为了可读性,我将在下一篇文章中处理。

查询执行

如前所述,Executor 实现 IEnumerable<T>。因此,我们必须实现 IEnumerator<T> GetEnumerator() 方法,并在调用 GetEnumerator() 时执行查询。这如下完成。

public IEnumerator<T> GetEnumerator() 
{

      DbCommand cmd = connection.CreateCommand();

      cmd.CommandText = sqlExpressionParser.GetSQLStatement();

      for (int i = 0; i < parameters.Count; i++) {
                
          var parameter = cmd.CreateParameter();

            parameter.ParameterName = "@p" + i;

            parameter.Value = parameters[i];
                    
            cmd.Parameters.Add(parameter);
      }

      DbDataReader reader = cmd.ExecuteReader();

      if (!reader.HasRows) 
      {
          reader.Close();
            yield break;
      }

      while (reader.Read()) 
      {
          yield return binder(reader);
      }

      reader.Close();
}

它几乎就是这么简单。

复杂性

假设我们不使用我们相当简单的示例,而是做一些更复杂的事情,例如以下 LINQ 查询

var x = from c in customers
    select new
    {
          Name = c.ContactName, 
          Orders = from o in orders
                   where o.CustomerID == c.CustomerID
                   select o
    };

此 LINQ 查询将生成以下方法调用

.Select(c => new <>f__AnonymousType0`2(Name = c.ContactName, 
        Orders = c.orders.Where(o => (o.CustomerID = c.CustomerID))))

然后,这将转换为以下 SQL 语句

SELECT t0.ContactName, CustomerID
FROM dbo.Customers AS t0

这看起来不对,对吧?我们只检索了 ContactNameCustomerID。客户的订单去哪了?我将尝试解释,但您可能需要查阅 LINQ 规范,特别是关于延迟执行的部分,以获取全面的详细信息。

两个查询的故事

假设您像这样在循环中使用上面的 x

foreach (var customer in x) {
  foreach (var order in customer.Orders) {
  ----------
  }
}

外层循环将生成以下查询

SELECT t0.ContactName, CustomerID
FROM dbo.Customers AS t0

并且,内层循环的每次执行都将生成一个如下所示的查询

SELECT t0.CustomerID, t0.EmployeeID, t0.Freight, 
    t0.OrderDate, t0.OrderID, t0.RequiredDate, t0.ShipAddress, 
    t0.ShipCity, t0.ShipCountry, t0.ShipName, t0.ShippedDate, 
    t0.ShipPostalCode, t0.ShipRegion, t0.ShipVia
FROM dbo.Orders AS t0
WHERE (t0.CustomerID = @p0)

为什么?外层循环的执行将生成一个如下所示的绑定器 Lambda 表达式

reader => new <>f__AnonymousType0`2(Name = IIF(Not(reader.IsDBNull(0)), 
        reader.GetString(0), Convert(null)), 
        Orders = .Where(o => (o.CustomerID = IIF(Not(reader.IsDBNull(1)),  
        reader.GetString(1), Convert(null)))))

如您所见,Orders 的值不是常量。它将从一个“按需”调用的方法调用中获取,因此是延迟执行。这值得思考,所以我暂时就说到这里。

注意

下一篇文章,我将上传完整的 LINQ to SQL IQueryable 提供程序的源代码,并提供其使用示例。在此之后,将恢复对实现细节的解释。

执行器源代码

private class Executor<T> : ExpressionVisitor, IEnumerable<T>
{
    private readonly DbConnection connection = null;
    private readonly SqlExpressionParser sqlExpressionParser = null;
    private readonly Func<DbDataReader, T> binder = null;
    private readonly List<object> parameters = new List<object>();

    public Executor(DbConnection connection, 
                    SqlExpressionParser sqlExpressionParser,
                    Delegate binder) {

        this.Visit(sqlExpressionParser.expression);
        this.connection = connection;
        this.sqlExpressionParser = sqlExpressionParser;
        this.binder = (Func<DbDataReader, T>)binder;
    }

    public IEnumerator<T> GetEnumerator() {

        DbCommand cmd = connection.CreateCommand();
        cmd.CommandText = sqlExpressionParser.GetSQLStatement();

        for (int i = 0; i < parameters.Count; i++) {
        
            var parameter = cmd.CreateParameter();
            parameter.ParameterName = "@p" + i;
            parameter.Value = parameters[i];
            cmd.Parameters.Add(parameter);
        }

        DbDataReader reader = cmd.ExecuteReader();

        if (!reader.HasRows) {
            reader.Close();
            yield break;
        }

        while (reader.Read()) {
            yield return binder(reader);
        }

        reader.Close();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator(); // probably wrong
    }

    protected override Expression VisitConstant(ConstantExpression c) 
    {

        if (c.Value == null) {
            parameters.Add("NULL");
        }
        else {
            switch (Type.GetTypeCode(c.Value.GetType())) {
                case TypeCode.Boolean:
                    parameters.Add(((bool)c.Value) ? 1 : 0);
                    break;
                case TypeCode.String:
                    parameters.Add(c.Value);
                    break;
                case TypeCode.Object:
                    break;
                default:
                    parameters.Add(c.Value.ToString());
                    break;
            }
        }

        return c;
    }

    protected override Expression VisitConditional(ConditionalExpression c) 
    {

        Debug.Assert(c.Test as ConstantExpression != null);

        if ((bool)(c.Test as ConstantExpression).Value == true) 
        {
            return this.Visit(c.IfTrue);
        }

        return this.Visit(c.IfFalse);
    }
}
© . All rights reserved.