一起谈.NET技术,使用LINQ Expression构建Query Object

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介:   这个问题来源于Apworks应用开发框架的设计。由于命令与查询职责的分离,使得基于CQRS体系结构风格的应用系统的外部存储系统的结构变得简单起来:在“命令”部分,简单地说,只需要 Event Store和Snapshot Store来保存Domain Model;而“查询”部分,则又是基于事件派送与侦听的系统集成。

  这个问题来源于Apworks应用开发框架的设计。由于命令与查询职责的分离,使得基于CQRS体系结构风格的应用系统的外部存储系统的结构变得简单起来:在“命令”部分,简单地说,只需要 Event Store和Snapshot Store来保存Domain Model;而“查询”部分,则又是基于事件派送与侦听的系统集成。之前我也提到过,“查询”部分由于不牵涉到Domain Model,于是,它的设计应该随意性很大:不需要受到Domain Model的牵制,例如,我们可以根据UI所需的数据源结构进行“查询”库的设计。Greg Young在他的“CQRS Documents”一文中也提到了这样一些相关话题:就“查询”部分而言,虽然也存在“阻抗失衡”效应,但是事件模型与关系模型之间的这种效应,要远远小于对象模型与关系模型之间的“阻抗失衡”效应。这是因为,事件模型本身没有结构,它仅仅表述“该对关系模型做哪些操作”这样的概念。在设计上,Greg Young建议,采用一种非正规的方式设计“查询”数据库,以便尽量减少读取数据时所需的JOIN操作,比如可以选用基于第一范式(1NF)的关系模型。这是一种反范式模型,虽然Greg Young并没有建议根据UI所需的数据源结构进行设计,但思想是相同的:基于事件而不拘泥于对象模型本身。由此引申出来的另一个好处就是外部存储系统架构的随意性:你可以选用任何存储技术和存储媒介,这又给基于系统架构的性能优化提供了便利。因为并非所有的存储架构都支持“表”、“字段”、“存储过程”、“JOIN”这些概念。
  根据上面的简单分析,我们得到一个结论:通常情况下,或许基于CQRS体系结构风格的应用系统更多的是采用的“平整”的外部存储结构,简而言之,就是一个数据访问对象(DAO)对应一张数据表。这也是我所设计的Apworks应用开发框架中默认支持的一种存储结构。有读过Apworks源代码的朋友会发现,在Apworks.Events.Storage命名空间下,有两个定制的DAO:DomainEventDataObject,用于表述领域事件的数据结构,以及SnapshotDataObject,用于表述快照的数据结构,与之相对应的就是数据库中的两张表:DomainEvents和 Snapshots。虽然结构变得这么简单,但是映射关系总还是需要维护的:最简单的就是需要在对象类型名称与数据表名之间,以及对象属性与数据表字段之间建立起映射关系。在Apworks中,这种映射关系是由Apworks.Storage.IStorageMappingResolver接口完成的。有关这个接口的内容不是本文讨论的重点,暂且不深入分析了。
  至此,也许你不会接受我上面的讨论,认为“基于UI设计数据库结构”或者“采用1NF、反范式设计数据库结构”是无法接受的,那么,接下来的讨论可能对你来说意义也不大了。因为下面的问题是以上面的描述为基础的:一个数据访问对象对应一张数据表。不过即使你不认同我的观点,我也建议你继续看完本文。
  Query Object模式

  虽然只是简单的映射,但毕竟不能忽略这样的映射关系。Apworks作为一个应用开发框架,需要提供方便的整合接口,以便今后能够根据不同的客户需求进行扩展。例如在存储部分,数据的增删改查(CRUD)是基于数据访问对象(DAO)的,这样做的一个好处是能够对外部存储系统进行抽象,使得访问存储系统的部分能够无需关系存储系统的细节问题。客户有可能选择SQL Server、Oracle、MySQL等关系型数据库作为存储系统,也可以选择其它的非关系型数据库作为存储系统,因此,我们的设计不能仅仅局限于关系型数据库,我们需要同时考虑其它形式的数据存储产品以便将来能够方便地集成新的存储方案。假设我们要设计一个针对 DomainEventDataObject的“查询”功能,我们需要考虑的问题可能会有(但不一定仅限于):
  * 需要查询对象的哪些属性(或者说与DomainEventDataObject相对应的数据表的哪些字段)
  * 需要根据什么样的条件进行查询
  * 查询是否需要排序
  * 是否只查结果集中的任意一条记录,还是要返回所有的记录
  在Apworks框架的Alpha版本中,查询的方法定义在Apworks.Storage.IStorage接口中。比如,根据给定的查询条件和排序方式,对指定DAO进行查询的方法定义如下:

 
 
/// <summary>
/// Gets a list of ordered objects from storage by given selection criteria and order.
/// </summary>
/// <typeparam name="T"> The type of the object to get. </typeparam>
/// <param name="criteria"> The <c> PropertyBag </c> instance which contains the criteria. </param>
/// <param name="orders"> The <c> PropertyBag </c> instance which contains the ordering fields. </param>
/// <param name="sortOrder"> The sort order. </param>
/// <returns> A list of ordered objects. </returns>
IEnumerable < T > Select < T > (PropertyBag criteria, PropertyBag orders, SortOrder sortOrder)
where T : class , new ();

  这个方法是个泛型方法,泛型类型就是DAO的类型。它接受三个参数:前两个是用于指定查询条件和排序字段的PropertyBag,最后一个是指定排序方式的SortOrder。之所以采用PropertyBag,而不是接受SQL字符串,原因不仅是因为框架本身需要在今后能够方便地支持非关系型数据库,而且更重要的是,虽然SQL已经成为一种业界标准,但实际上不同的关系型数据库产品对SQL的支持和实现方式也有所不同,有些关系型数据库产品或许只支持SQL的一些子集,如果单纯地把SQL字符串作为Select方法的参数,明显是不合理的。事实上,Apworks.Storage.IStorage实现了Query Object模式[MF, PoEAA],Martin Fowler在他的PoEAA(《企业应用架构模式》)中有以下几点可以供读者参考:
  * “编程语言是可以支持SQL语句的,但大多数开发人员对此不太熟悉。而且,你需要了解数据库设计方案以便构造出查询。可以通过创建特殊的、隐藏了 SQL内部参数化方法的查询器方法避免这一点。但这样难以构造更多的特别查询。而且,如果数据库设计方案改变,就会需要复制到SQL语句中”
  * “查询对象的一个共同特征是,它能够利用内存对象的语言而不是用数据库方案来描述查询。这意味着我可以使用对象和域名,而不是表和列名。如果对象和数据库具有相同的结构,这一点就不重要,如果两者有差异,查询对象就很有用。为了实现这样的视角变化,查询对象需要知道数据库结构怎样映射到对象结构,这一功能实际上要用到元数据映射”【daxnet注:上面提到过,在Apworks框架中,这个元数据映射的实现,就是 IStorageMappingResolver】
  * “为了描述任意的查询,你需要一个灵活的查询对象。然而,应用程序经常能用远少于SQL全部功能的操作来完成这一任务,在此情况下,你的查询对象就会比较简单。它不能代表任何东西,但它可以满足特定的需要。此外,当需要更多功能而进行功能扩充时,通常不会比从零开始创建一个全能的查询对象更麻烦。因此,应该为当前需求创建一个功能最小化的查询对象,并随着需求的增加改进这个查询对象”
  以上三点让我很有感触,特别是第三点。目前基于PropertyBag的设计,只能够支持以AND连接的查询条件,比如,类似“WHERE a=va AND b=vb AND c=vc…”这样的查询,虽然在Apworks Alpha版本中,这样的查询已经够用了,但它不具备扩展性,基于关系型数据库的存储设计Apworks.Storage.RdbmsStorage已经将这种逻辑写死了,倘若我们需要一个复杂的查询,这种方式不仅没法胜任,而且没法扩展。PropertyBag应该要退休了。
  在下一个版本的Apworks中,我使用.NET中的LINQ Expression代替了PropertyBag,并引入了一个WhereClauseBuilder的对象,专门根据LINQ Expression,针对关系型数据库产生WHERE子句。使用LINQ Expression的好处有:
  * LINQ Expression是.NET下的一种查询标准,多数存储系统产品能够提供针对LINQ Expression的查询解决方案,即使不提供,也可以自己定制Provider,虽然麻烦一点,但总归是可以实现的
  * LINQ Expression能够完美地“利用内存对象的语言而不是用数据库方案”来描述查询,语言集成的特性,为开发人员带来了更多的便捷
  * Apworks中,Specification是基于LINQ Expression的,于是,Apworks.Storage.IStorage就能够实现基于Specification的查询,实现接口统一
  于是技术问题来了:如何将LINQ Expression转换成WHERE子句,以便Apworks.Storage.IStorage的类(Query Objects)能够使用这个WHERE子句构造出SQL语句,进而通过ADO.NET直接访问数据库?Apworks选用的是Expression Visitor的方案:使用Expression Visitor遍历表达式树(Expression Tree)然后产生WHERE子句。在讨论Expression Visitor之前,让我们回顾一下对象结构以及Visitor模式。
  Visitor模式
  网上面有关Visitor模式的文章太多了,还有相当一部分讨论的比较深入透彻,我也就不多说了。总之,Visitor模式在处理较复杂的对象结构时会显得十分自然:它能够遍历结构中的每一个对象,然后针对不同的对象类型作不同的处理。这就看上去像是为这些对象扩展了一些方法一样。之前,我有用过 Visitor模式来验证程序配置节点的合理性,当节点的类型增加后,只需要扩展Visitor即可实现新的验证逻辑,非常方便。模式归模式,不同的应用场景,实现方式还是有所不同的。经典的Visitor例子,通常都是利用了函数的重载(多态性),并结合了Composite模式来说明问题,但实际上 Visitor并非一定需要使用函数重载,也不是仅能用在Composite上。Expression Visitor的实现方式,就与这经典的Visitor案例有所不同。
  Expression Visitor
  在System.Linq.Expressions命名空间下,有一个ExpressionVisitor的抽象类,我们只需要继承这个抽象类,并重写其中的某些Visit方法,即可实现WHERE子句的生成。在这里我不打算继续去细究ExpressionVisitor是如何遍历表达式树的,我还是描述一下实现WHERE子句生成的几个细节问题。
  1. 支持哪些运算?

  LINQ Expression的类型有85种,但并不是SQL中会支持到所有的这85种类型。目前Apworks打算支持常用的条件运算,比如:大于、大于等于、小于、小于等于、不等于、等于这几种,打算支持常用的逻辑运算:AND、OR、NOT
  2. 支持哪些方法(函数)?
      目前Apworks支持的方法仅有三种:object.Equals、string.StartsWith和 string.EndsWith。object.Equals将被翻译成“object = value”,string.StartsWith和string.EndsWith将被翻译成“LIKE”子句
  3. 支持内联函数和变量?
      目前仅支持变量,不支持内联函数。
      比如:可以用下面的方式来指定Expression:

 
 
int a = GetAge();
Expression < Func < Employee, bool >> expr = p => p.Age.Equals(a);

  而不能使用下面的方式来指定Expression:

 
 
Expression < Func < Employee, bool >> expr = p => p.Age.Equals(GetAge());

  4. 支持扩展?
  当然,只需要继承已有的ExpressionVisitor类,并重写其中某些方法即可
  在当前的Apworks版本中,Apworks.Storage.Builders命名空间下定义了针对关系型数据库的 IWhereClauseBuilder接口,以及一个抽象实现:Apworks.Storage.Builders.WhereClauseBuilder类,它不仅实现了IWhereClauseBuilder 接口,同时继承于System.Linq.Expressions.ExpressionVisitor抽象类,因此,WHERE子句生成的主体逻辑都在这个类中。SqlWhereClauseBuilder类继承WhereClauseBuilder类,以便实现特定于SQL Server语法的WHERE子句生成器。
  由于Apworks.Storage.Builders.WhereClauseBuilder类的源代码比较长,我就不贴在这里了,读者朋友请【点击此处】查看该类的全部源代码。
  与规约(Specification)整合
  在《EntityFramework之领域驱动设计实践(十):规约模式》一文中,我提出了基于.NET的规约模式的实现方式,为了迎合.NET对LINQ Expression的支持,规约模式的实现也采用了LINQ Expression,而原来的IsSatisfiedfiedBy方法则改为直接使用LINQ Expression来获得结果:

 
 
public interface ISpecification < T >
{
bool IsSatisfiedBy(T obj);
Expression
< Func < T, bool >> Expression { get ; }
}

public abstract class Specification < T > : ISpecification < T >
{
#region ISpecification Members
public virtual bool IsSatisfiedBy(T obj)
{
return this .Expression.Compile()(obj);
}
public abstract Expression < Func < T, bool >> Expression { get ; }
#endregion
}

  回过头来考察Select方法,原本第一个参数是用Expression<Func<T, bool>>类型代替PropertyBag的,现在则可以直接使用ISpecification接口了,于是,我们的Query Object可以使用规约模式来支持数据查询了。

 
 
IEnumerable < T > Select < T > (ISpecification < T > specification, PropertyBag orders, SortOrder sortOrder)
where T : class , new ();

  执行过程与客户端调用示例
  基于上面的讨论,Select方法的定义,已经从使用PropertyBag作为查询条件,转变为使用ISpecification接口。注意:orders参数仍然使用PropertyBag,因为目前不打算支持基于表达式的排序条件:
  在Apworks.Storage.RdbmsStorage中,使用WhereClauseBuilder.BuildWhereClause方法,根据LINQ Expression生成WHERE子句,进而产生SQL语句并使用ADO.NET访问关系型数据库:

 
 
public IEnumerable < T > Select < T > (ISpecification < T > specification, PropertyBag orders, Storage.SortOrder sortOrder)
where T : class , new ()
{
try
{
Expression
< Func < T, bool >> expression = null ;
WhereClauseBuildResult whereBuildResult
= null ;
string sql = string .Format( " SELECT {0} FROM {1} " ,
GetFieldNameList
< T > (), GetTableName < T > ());
if (specification != null )
{
expression
= specification.GetExpression();
whereBuildResult
= GetWhereClauseBuilder < T > ().BuildWhereClause(expression);
sql
+= " WHERE " + whereBuildResult.WhereClause;
}
if (orders != null && sortOrder != Storage.SortOrder.Unspecified)
{
sql
+= " ORDER BY " + GetOrderByFieldList < T > (orders);
switch (sortOrder)
{
case Storage.SortOrder.Ascending:
sql
+= " ASC " ;
break ;
case Storage.SortOrder.Descending:
sql
+= " DESC " ;
break ;
default : break ;
}
}
using (DbCommand command = CreateCommand(sql))
{
if (command.Connection == null )
command.Connection
= Connection;
if (Transaction != null )
command.Transaction
= Transaction;
if (specification != null )
{
command.Parameters.Clear();
var parameters
= GetSelectCriteriaDbParameterList < T > (whereBuildResult.ParameterValues);
foreach (var parameter in parameters)
{
command.Parameters.Add(parameter);
}
}
DbDataReader reader
= command.ExecuteReader();
List
< T > ret = new List < T > ();
while (reader.Read())
{
ret.Add(CreateFromReader
< T > (reader));
}
reader.Close();
// Very important: reader MUST be closed !!!
return ret;
}
}
catch (ExpressionParseException)
{
throw ;
}
catch (InfrastructureException)
{
throw ;
}
catch (Exception ex)
{
throw ExceptionManager.HandleExceptionAndRethrow < StorageException > (ex,
Resources.EX_SELECT_FROM_STORAGE_FAIL,
typeof (T).AssemblyQualifiedName,
specification
!= null ? specification.ToString() : " NULL " ,
orders
!= null ? orders.ToString() : " NULL " ,
sortOrder);
}
}

  下面这个方法将根据Aggregate Root的类型与ID,返回与之相关的所有Domain Events:

 
 
public virtual IEnumerable < IDomainEvent > LoadEvents(Type aggregateRootType, long id)
{
try
{
PropertyBag sort
= new PropertyBag();
sort.AddSort
< long > ( " Version " );
var aggregateRootTypeName
= aggregateRootType.AssemblyQualifiedName;
ISpecification
< DomainEventDataObject > specification = Specification < DomainEventDataObject >
.Eval(p
=> p.AggregateRootId == id && p.AggregateRootType == aggregateRootTypeName);
return Select < DomainEventDataObject > (specification, sort, Apworks.Storage.SortOrder.Ascending)
.Select(p
=> p.ToEntity());
}
catch { throw ; }
}
相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
12天前
|
机器学习/深度学习 人工智能 Cloud Native
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台
在数字化时代,.NET 技术凭借其跨平台兼容性、丰富的类库和工具集以及卓越的性能与效率,成为软件开发的重要平台。本文深入解析 .NET 的核心优势,探讨其在企业级应用、Web 开发及移动应用等领域的应用案例,并展望未来在人工智能、云原生等方面的发展趋势。
23 3
|
12天前
|
开发框架 安全 Java
.NET技术的独特魅力与优势,涵盖高效的开发体验、强大的性能表现、高度的可扩展性及丰富的生态系统等方面,展示了其在软件开发领域的核心竞争力
本文深入探讨了.NET技术的独特魅力与优势,涵盖高效的开发体验、强大的性能表现、高度的可扩展性及丰富的生态系统等方面,展示了其在软件开发领域的核心竞争力。.NET不仅支持跨平台开发,具备出色的安全性和稳定性,还能与多种技术无缝集成,为企业级应用提供全面支持。
21 3
|
15天前
|
人工智能 开发框架 前端开发
C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)
C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)
|
18天前
|
Kubernetes Cloud Native Ubuntu
庆祝 .NET 9 正式版发布与 Dapr 从 CNCF 毕业:构建高效云原生应用的最佳实践
2024年11月13日,.NET 9 正式版发布,Dapr 从 CNCF 毕业,标志着云原生技术的成熟。本文介绍如何使用 .NET 9 Aspire、Dapr 1.14.4、Kubernetes 1.31.0/Containerd 1.7.14、Ubuntu Server 24.04 LTS 和 Podman 5.3.0-rc3 构建高效、可靠的云原生应用。涵盖环境准备、应用开发、Dapr 集成、容器化和 Kubernetes 部署等内容。
43 5
|
14天前
|
人工智能 开发框架 安全
C#/.NET/.NET Core技术前沿周刊 | 第 13 期(2024年11.11-11.17)
C#/.NET/.NET Core技术前沿周刊 | 第 13 期(2024年11.11-11.17)
|
2月前
|
人工智能 开发框架 C#
C#/.NET/.NET Core技术前沿周刊 | 第 6 期(2024年9.16-9.22)
C#/.NET/.NET Core技术前沿周刊 | 第 6 期(2024年9.16-9.22)
|
2月前
|
人工智能 开发框架 Cloud Native
C#/.NET/.NET Core技术前沿周刊 | 第 9 期(2024年10.07-10.13)
C#/.NET/.NET Core技术前沿周刊 | 第 9 期(2024年10.07-10.13)
|
2月前
|
数据可视化 NoSQL C#
C#/.NET/.NET Core技术前沿周刊 | 第 8 期(2024年10.01-10.06)
C#/.NET/.NET Core技术前沿周刊 | 第 8 期(2024年10.01-10.06)
|
3月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
44 7
|
3月前
|
存储 开发框架 前端开发
ASP.NET MVC 迅速集成 SignalR
ASP.NET MVC 迅速集成 SignalR
65 0