使用LINQ Expression构建Que“.NET研究”ry Object

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 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> Prope上海闵行企业网站制作rtyBag </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 >&gt上海企业网站制作; 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 上海企业网站设计与制作span>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上海徐汇企业网站设计与制作 style="color: #000000;"> (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);上海网站建设br /> 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数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
13天前
|
存储 Shell Linux
快速上手基于 BaGet 的脚本自动化构建 .net 应用打包
本文介绍了如何使用脚本自动化构建 `.net` 应用的 `nuget` 包并推送到指定服务仓库。首先概述了 `BaGet`——一个开源、轻量级且高性能的 `NuGet` 服务器,支持多种存储后端及配置选项。接着详细描述了 `BaGet` 的安装、配置及使用方法,并提供了 `PowerShell` 和 `Bash` 脚本实例,用于自动化推送 `.nupkg` 文件。最后总结了 `BaGet` 的优势及其在实际部署中的便捷性。
50 10
|
23天前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
33 5
|
23天前
|
存储 缓存 安全
.NET 在金融行业的应用:高并发交易系统的构建与优化之路
【8月更文挑战第28天】在金融行业,交易系统需具备高并发处理、低延迟及高稳定性和安全性。利用.NET构建此类系统时,可采用异步编程提升并发能力,优化数据库访问以降低延迟,使用缓存减少数据库访问频率,借助分布式事务确保数据一致性,并加强安全性措施。通过综合优化,满足金融行业的严苛要求。
25 1
|
23天前
|
机器学习/深度学习 人工智能 算法
【悬念揭秘】ML.NET:那片未被探索的机器学习宝藏,如何让普通开发者一夜变身AI高手?——从零开始,揭秘构建智能应用的神秘旅程!
【8月更文挑战第28天】ML.NET 是微软推出的一款开源机器学习框架,专为希望在本地应用中嵌入智能功能的 .NET 开发者设计。无需深厚的数据科学背景,即可实现预测分析、推荐系统和图像识别等功能。它支持多种数据源,提供丰富的预处理工具和多样化的机器学习算法,简化了数据处理和模型训练流程。
33 1
|
23天前
|
大数据 开发工具 开发者
从零到英雄:.NET核心技术带你踏上编程之旅,构建首个应用,开启你的数字世界探险!
【8月更文挑战第28天】本文带领读者从零开始,使用强大的.NET平台搭建首个控制台应用。无论你是新手还是希望扩展技能的开发者,都能通过本文逐步掌握.NET的核心技术。从环境搭建到创建项目,再到编写和运行代码,详细步骤助你轻松上手。通过计算两数之和的小项目,你不仅能快速入门,还能为未来开发更复杂的应用奠定基础。希望本文为你的.NET学习之旅开启新篇章!
26 1
|
1月前
|
缓存 运维 前端开发
阿里云云效操作报错合集之如何解决在使用流水线构建net8应用时遇到无法构建的报错
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
|
19天前
|
C# Windows 开发者
超越选择焦虑:深入解析WinForms、WPF与UWP——谁才是打造顶级.NET桌面应用的终极利器?从开发效率到视觉享受,全面解读三大框架优劣,助你精准匹配项目需求,构建完美桌面应用生态系统
【8月更文挑战第31天】.NET框架为开发者提供了多种桌面应用开发选项,包括WinForms、WPF和UWP。WinForms简单易用,适合快速开发基本应用;WPF提供强大的UI设计工具和丰富的视觉体验,支持XAML,易于实现复杂布局;UWP专为Windows 10设计,支持多设备,充分利用现代硬件特性。本文通过示例代码详细介绍这三种框架的特点,帮助读者根据项目需求做出明智选择。以下是各框架的简单示例代码,便于理解其基本用法。
57 0
|
19天前
|
Java Spring 自然语言处理
Spring 框架里竟藏着神秘魔法?国际化与本地化的奇妙之旅等你来揭开谜底!
【8月更文挑战第31天】在软件开发中,国际化(I18N)与本地化(L10N)对于满足不同地区用户需求至关重要。Spring框架提供了强大支持,利用资源文件和`MessageSource`实现多语言文本管理。通过配置日期格式和货币符号,进一步完善本地化功能。合理应用这些特性,可显著提升应用的多地区适应性和用户体验。
28 0
|
23天前
|
传感器 开发框架 物联网
揭开.NET在IoT领域的神秘面纱:如何构建智能设备,让未来生活触手可及?
【8月更文挑战第28天】随着物联网技术的发展,智能设备正深入我们的生活。.NET作为跨平台开源框架,在IoT领域应用广泛。本文介绍如何利用.NET构建智能设备,通过实例展示从环境搭建到项目创建、代码编写及运行的全过程,帮助开发者快速实现IoT解决方案,开启智能设备开发的新篇章。
28 0
|
23天前
|
开发框架 监控 .NET
开发者的革新利器:ASP.NET Core实战指南,构建未来Web应用的高效之道
【8月更文挑战第28天】本文探讨了如何利用ASP.NET Core构建高效、可扩展的Web应用。ASP.NET Core是一个开源、跨平台的框架,具有依赖注入、配置管理等特性。文章详细介绍了项目结构规划、依赖注入配置、中间件使用及性能优化方法,并讨论了安全性、可扩展性以及容器化的重要性。通过这些技术要点,开发者能够快速构建出符合现代Web应用需求的应用程序。
31 0