re-linq|ishing 痛苦:使用 re-linq 在 NHibernate 的例子上实现强大的 LINQ 提供程序






4.86/5 (18投票s)
本文展示了如何利用开源的 re-linq 库,以 LINQ to NHibernate HQL 为例,轻松实现一个强大的 LINQ 提供程序。
介绍
尽管市面上存在许多免费的 LINQ 提供程序,但它们通常只提供基本功能,即使是稍显复杂的查询也会失败。这是因为 .NET 并没有提供一种 LINQ 提供程序,能够以大多数人在 C# 源代码中使用的语法(如 from-where-select 等)来处理 LINQ 查询,而是以抽象语法树(AST)的形式来处理。不幸的是,这种抽象语法树并不适合转换为典型的查询语言(为单个特定的 LINQ 提供程序进行转换的工作量很容易达到数月的人工,而且基于抽象语法树表示,不同 LINQ 提供程序之间的代码复用性非常低)。
经过 TDD 开发的开源 re-motion 框架,通过其 re-linq 组件,提供了一个类 LINQ 提供程序库,它将抽象语法树转换回一个对象模型,该模型以更接近典型查询语言的形式表示 LINQ 查询表达式(事实上,re-linq 甚至可以将手工创建的抽象语法树转换为语义上相同的查询表达式语法表示)。通过 re-linq 提供的表示,可以轻松创建功能强大的 LINQ 提供程序,生成 SQL、NHibernate 的 HQL、XQuery 或 Entity Framework 的 Entity SQL 等各种查询语言的查询。
此外,re-linq 提供了一个灵活的查询模型,允许您在生成目标查询语言的代码之前修改现有查询。这使得您可以从应用程序代码创建和修改查询,并且可以在基于 re-linq 的不同 LINQ 提供程序之间共享转换方法。有关此高级主题的更多信息,请参阅 “如何编写 LINQ 提供程序 - 简单方法”博客文章中的“转换查询”部分,该文章还详细介绍了如何实现一个通用的基于 re-linq 的 LINQ 提供程序。
为什么选择 re-linq
现有的 LINQ 提供程序实现(如 Microsoft 的 Linq2SQL)通常通过将 LINQ 树表示分多个小步骤转换为另一种查询语言。这种方法有几个缺点:例如,它需要大量关于实际可能的树组合的预先知识,并且需要深入了解 LINQ 内部工作的确切机制。它还会导致实现与目标查询语言紧密耦合,也就是说,通用可重用代码片段的潜力非常小:您必须从头开始编写每个 LINQ 提供程序。re-linq 开源 LINQ 提供程序库采用了不同的方法:它将 LINQ 查询转换为内部的 C#-LINQ 风格模型,该模型更易于理解和翻译。re-linq 要求实现者只需覆盖 re-linq 提供给易于使用的参数的两个访问者类中的一些方法,而整个树遍历则在后台处理。这使得实现任何类型的 LINQ 提供程序都成为一项简单的任务。
您可以在 re-linq CodePlex 页面 上找到更多关于 re-linq 的信息。
背景
要理解本文,您需要对 Microsoft LINQ 技术有基本了解。本文描述了如何为 NHibernate 实现 LINQ 提供程序,因此了解 Hibernate 查询语言 (HQL) 会很有帮助;然而,由于 HQL 与 SQL 在语法上的相似性,示例中展示的原理可以很容易地转换为使用 re-linq 来实现 SQL LINQ 提供程序(甚至大部分代码将是相同的)。
了解 访问者模式 不是必需的,但可能会有助于理解 re-linq 的工作原理。
示例查询
以下是一些我们用 4 小时实现的 re-linq for NHibernate 所能处理的示例查询。成员访问和多个 from
语句(查询返回所有拥有自己住房的人)
from l in NHQueryFactory.Queryable<Location>(session)
from p in NHQueryFactory.Queryable<Person>(session)
where (l.Owner == p) && (p.Location == l)
select p;
连接和排序
from p in NHQueryFactory.Queryable<Person>(session)
where p.Surname == "Oerson"
orderby p.Surname
join pn in NHQueryFactory.Queryable<PhoneNumber>(session) on p equals pn.Person
select pn;
复杂表达式和方法调用
from l in NHQueryFactory.Queryable<Location>(session)
from p in NHQueryFactory.Queryable<Person>(session)
where (((((3 * l.ZipCode - 3) / 7)) == 9) && p.Surname.Contains ("M") && p.Location == l)
select l;
[设置]
在开始之前,您需要下载并解压缩示例源代码。如果您想执行测试,则需要打开 NHibernate.ReLinq.Sample.UnitTests
项目中的 "hibernate.cfg.xml" 文件,并根据您的 DBMS 调整 DBMS 连接数据。您还需要创建在 Initial Catalog=“...” 下提供的示例数据库,该数据库默认为 "NHibernate_ReLinq"。
请注意,虽然 re-linq 是按照 TDD 原则实现的,但示例代码只是一个集成测试原型(对于刚开始接触 TDD 的人来说,将其转化为 TDD 实现是一个很好的练习)。
快速概览
如果您是专家,并且想立即深入研究代码,这里有一个关于示例如何工作的快速介绍:NHQueryFactory
创建 NHQueryable<T>
,这是 NHibernate 特定的 LINQ IQueryable<T>
。NHQueryable<T>
创建并持有 NHQueryExecutor
,它通过其 ExecuteScalar<T>
、ExecuteSingle<T>
和 ExecuteCollection<T>
方法在 re-linq 和具体的查询提供程序实现之间建立桥梁。对于 NHibernate LINQ 提供程序,ExecuteScalar<T>
和 ExecuteSingle<T>
基本上转发到 ExecuteCollection<T>
就足够了;ExecuteCollection<T>
反过来使用 HqlGeneratorQueryModelVisitor
来遍历 re-linq 的 QueryModel
并获取生成的 HQL 查询字符串。HqlGeneratorQueryModelVisitor
包含特定的 LINQ 提供程序代码,通过其 VisitSelectClause
、VisitWhereClause
等方法为 LINQ 的 select
、from
、where
、orderby
、join
等命令创建生成的 HQL。在内部,它使用一个(同样是 LINQ 提供程序特定的)HqlGeneratorExpressionTreeVisitor
来处理不同的 LINQ 表达式,例如二元表达式(Equal
、Add
、Multiply
等)、成员访问(person.Location
)、方法调用(person.FirstName.Contains("John")
)等,通过其 VisitBinaryExpression
、VisitMemberExpression
等方法。这两个类构成了任何特定的基于 re-linq 的 LINQ 提供程序的核心。
HqlGeneratorQueryModelVisitor
在内部使用 QueryPartsAggregator
来收集多个 from
、where
等语句,并以正确的 HQL 顺序发出它们。这是必要的,因为 LINQ 在操作顺序方面比 HQL(或 SQL)灵活得多(QueryPartsAggregator
未来可能会被移至 re-linq 或 re-motion contrib 库)。
测试域
我们的测试使用了一个简单的测试域,包含Location
、Person
和 PhoneNumber
。每位 Person
都有一个 Location
,并且可以拥有任意数量的 PhoneNumber
。您可以在测试项目下的 "DomainObjects" 中找到这些类,NHibernate 映射在 "Mappings" 下(注意:您不需要了解 NHibernate 映射的工作原理;只要知道映射告诉 NHibernate 如何在 DBMS 中持久化我们测试域的实例,以及如何使用其类似 SQL 的 HQL 查询语言来查询它们)。re-linq| NHibernate
select ... from 语句
设置好数据库后,打开NHibernate.ReLinq.Sample.UnitTests
中的 "IntegrationTests.cs" 文件。它包含典型的 NHibernate 设置和拆卸代码,确保测试不会相互干扰。向下滚动直到看到第一个 [Test]
,SelectFrom()
。该测试显示了最简单的 LINQ 查询from pn in NHQueryFactory.Queryable<PhoneNumber>(session)
select pn;
该查询简单地返回在 SetupTestData()
方法中创建的所有 PhoneNumber
。NHQueryFactory
是 re-linq NHibernate LINQ 可查询工厂,它允许通过 LINQ 查询传入的泛型类型的域对象;例如,对于 PhoneNumber
实例
NHQueryFactory.Queryable<PhoneNumber>
在 NHibernate 的情况下,它必须传入要使用的 NHibernate.ISession
。出于测试目的,CreateNHQuery(session, query.Expression).QueryString
会给出与 LINQ query
相对应的 HQL 查询字符串,该字符串等于
"select pn from NHibernate.ReLinq.Sample.UnitTests.DomainObjects.PhoneNumber as pn"
执行 query
直接返回测试域中的所有 5 个 PhoneNumber
,如预期。实现表明 re-linq 为我们做了大部分工作。我们所要做的就是在 HqlGeneratorQueryModelVisitor
(它继承自 re-linq 的 QueryModelVisitorBase
)中重写 VisitMainFromClause
和 VisitSelectClause
,实现非常简单。
public override void VisitMainFromClause (MainFromClause fromClause, QueryModel queryModel)
{
_queryParts.AddFromPart (fromClause);
base.VisitMainFromClause (fromClause, queryModel);
}
public override void VisitSelectClause (SelectClause selectClause, QueryModel queryModel)
{
_queryParts.SelectPart = GetHqlExpression (selectClause.Selector);
base.VisitSelectClause (selectClause, queryModel);
}
我们所做的工作都由 QueryParts
完成,这是一个简单的辅助类,它收集相同类型的不同查询组件(例如 from
语句),并在最后以正确的顺序发出它们。from
语句仅被存储
public string SelectPart { get; set; }
对于 from
语句,它还处理发出正确的 HQL 别名语法
public void AddFromPart (IQuerySource querySource)
{
FromParts.Add (string.Format ("{0} as {1}", GetEntityName (querySource),
querySource.ItemName));
}
请注意,这很容易做到,因为 re-linq 已经为我们提供了所需的信息。where 语句
下一个测试介绍了where
语句和相等比较运算符from pn in NHQueryFactory.Queryable<PhoneNumber>(session)
where pn.CountryCode == "11111"
select pn;
实现与上述一样直接public override void VisitWhereClause (WhereClause whereClause,
QueryModel queryModel, int index)
{
_queryParts.AddWherePart (GetHqlExpression (whereClause.Predicate));
base.VisitWhereClause (whereClause, queryModel, index);
}
GetHqlExpression
使用 HqlGeneratorExpressionTreeVisitor
(它继承自 re-linq 的 ThrowingExpressionTreeVisitor
,顾名思义,它在遇到没有处理程序的表达式时会抛出异常),它处理例如二元表达式;对于相等运算符protected override Expression VisitBinaryExpression (BinaryExpression expression)
{
_hqlExpression.Append ("(");
VisitExpression (expression.Left);
switch (expression.NodeType)
{
case ExpressionType.Equal:
_hqlExpression.Append (" = ");
break;
// handle additional binary expressions
default:
base.VisitBinaryExpression (expression); // throws
break;
}
VisitExpression (expression.Right);
_hqlExpression.Append (")");
return expression;
}
(注意:在生产代码中,使用查找表而不是 switch-case 结构会是更好的选择)。VisitExpression
方法调用由 re-linq 代码执行,并进行所需的数据结构遍历。
二元表达式
其他二元表达式可以像相等运算符一样轻松处理 case ExpressionType.Equal:
_hqlExpression.Append (" = ");
break;
case ExpressionType.AndAlso:
case ExpressionType.And:
_hqlExpression.Append (" and ");
break;
case ExpressionType.OrElse:
case ExpressionType.Or:
_hqlExpression.Append (" or ");
break;
case ExpressionType.Add:
_hqlExpression.Append (" + ");
break;
case ExpressionType.Subtract:
_hqlExpression.Append (" - ");
break;
// etc...
join 子句
实现join
操作也只需要几行代码public override void VisitJoinClause (JoinClause joinClause,
QueryModel queryModel, int index)
{
_queryParts.AddFromPart (joinClause);
_queryParts.AddWherePart (
"({0} = {1})",
GetHqlExpression (joinClause.OuterKeySelector),
GetHqlExpression (joinClause.InnerKeySelector));
base.VisitJoinClause (joinClause, queryModel, index);
}
同样,VisitJoinClause
调用由 re-linq 执行。方法调用
以下展示了一个方法调用的实现,在这种情况下,测试一个字符串是否包含给定的子字符串;Contains
的调用被转换为 SQL/HQL "like %
<substring>%
" 语法protected override Expression VisitMethodCallExpression (MethodCallExpression expression)
{
var supportedMethod = typeof (string).GetMethod ("Contains");
if (expression.Method.Equals (supportedMethod))
{
_hqlExpression.Append ("(");
VisitExpression (expression.Object);
_hqlExpression.Append (" like '%'+");
VisitExpression (expression.Arguments[0]);
_hqlExpression.Append ("+'%')");
return expression;
}
else
{
return base.VisitMethodCallExpression (expression); // throws
}
}
整合起来
完成 LINQ to NHibernate 查询转换的最后一步是调用QueryPartsAggregator
的 BuildHQLString
,它将由两个访问者收集的信息以正确的 HQL 顺序发出。实现再次很简单(SeparatedStringBuilder.Build
是一个 re-motion 辅助方法,它简单地连接来自一个可枚举对象的字符串,并用其第一个参数分隔它们)public string BuildHQLString ()
{
var stringBuilder = new StringBuilder ();
if (string.IsNullOrEmpty (SelectPart) || FromParts.Count == 0)
throw new InvalidOperationException (
"A query must have a select part and at least one from part.");
stringBuilder.AppendFormat ("select {0}", SelectPart);
stringBuilder.AppendFormat (" from {0}",
SeparatedStringBuilder.Build (", ", FromParts));
if (WhereParts.Count > 0)
stringBuilder.AppendFormat (" where {0}",
SeparatedStringBuilder.Build (" and ", WhereParts));
if (OrderByParts.Count > 0)
stringBuilder.AppendFormat (" order by {0}",
SeparatedStringBuilder.Build (", ", OrderByParts));
return stringBuilder.ToString ();
}
其余部分
上面的示例已经接近复杂度的上限了,您现在应该对如何编写基于 re-linq 的 LINQ 提供程序有一个很好的了解。如需进一步探索,请查看演示项目(页面顶部的下载链接)。故障排除
问:如果我执行测试,所有测试都失败了。怎么回事?答:请检查您是否在 DBMS 中创建了 "NHibernate_ReLinq" 数据库并调整了 "hibernate.cfg.xml" 文件。
[拆卸]
在本文中,我们展示了如何利用 re-motion 的 re-linq 库,轻松地以很少的努力实现功能丰富的 LINQ 提供程序。re-linq 使之变得容易,因为它封装了复杂的 LINQ 解析算法,并提供了一个易于理解的查询模型,准备好由两个特定于提供程序的HqlGeneratorQueryModelVisitor
和 HqlGeneratorExpressionTreeVisitor
访问者类进行转换。// 愿您所有的测试都通过。
Cleanup();
// Ma;-)rkus
历史
- 2009.09.03 - 初次发布。
- 2009.09.04 - 将许可证放宽至 MIT。
- 2010.01.18 - 添加了版权声明、re-motion 用户组链接、re-linq 页面链接。
- 2010.01.19 - 修复了代码示例中的泛型参数。