如何 LINQ To SQL:第三部分






3.86/5 (6投票s)
如何 LINQ To SQL:执行器。
引言
本文是系列文章的第三篇,概述了如何将 LINQ 表达式树转换为 SQL 语句,这些语句可以针对多个 RDBMS 系统执行,而不仅仅是 Microsoft 的 SQL Server 产品。文章还将说明如何
- 正确且全面地翻译具有有效 SQL 翻译的二元和一元表达式。
- 翻译具有 SQL 等效项的函数调用(例如 customer.FirstName.ToUpper())。
- 实现 GroupBy。
- 实现 IQueryable方法ANY、ALL、COUNT、AVERAGE等。
- 参数化查询,而不是在 SQL 转换中嵌入常量。
- 缓存先前翻译的表达式树。
- 可能不使用 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 需要以下内容才能执行其功能
- 到被查询数据库的 DbConnection。
- 要执行的 SQL 语句。
- 产生上述 SQL 语句的表达式。(在我们上面的示例中,您会注意到该表达式包含一个名为 city的变量。我们需要此变量的值才能初始化参数@p0。)
- 一个接受 DbDataReader并返回类型为T的对象的委托。在我们上面的示例中,T是一个具有两个属性Name和Phone的匿名类型。此委托是从上一篇文章中讨论的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
这看起来不对,对吧?我们只检索了 ContactName 和 CustomerID。客户的订单去哪了?我将尝试解释,但您可能需要查阅 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);
    }
}

