如何 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);
}
}