购物网站英语,wordpress列表框内显示标题,wordpress在这个站点注册,企业网企业网站制作本系列包含以下文章#xff1a;
DDD入门DDD概念大白话战略设计代码工程结构请求处理流程聚合根与资源库实体与值对象应用服务与领域服务领域事件CQRS#xff08;本文#xff09;
案例项目介绍 #
既然DDD是“领域”驱动#xff0c;那么我们便不能抛开业务而只讲技术…本系列包含以下文章
DDD入门DDD概念大白话战略设计代码工程结构请求处理流程聚合根与资源库实体与值对象应用服务与领域服务领域事件CQRS本文
案例项目介绍 #
既然DDD是“领域”驱动那么我们便不能抛开业务而只讲技术为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云不是马云也不是码云。如你已经在本系列的其他文章中了解过该案例可跳过。
码如云是一个基于二维码的一物一码管理平台可以为每一件“物品”生成一个二维码并以该二维码为入口展开对“物品”的相关操作典型的应用场景包括固定资产管理、设备巡检以及物品标签等。
在使用码如云时首先需要创建一个应用(App)一个应用包含了多个页面(Page)也可称为表单一个页面又可以包含多个控件(Control)比如单选框控件。应用创建好后可在应用下创建多个实例(QR)用于表示被管理的对象比如机器设备。每个实例均对应一个二维码手机扫码便可对实例进行相应操作比如查看实例相关信息或者填写页面表单等对表单的一次填写称为提交(Submission)更多概念请参考码如云术语。
在技术上码如云是一个无代码平台包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发其后端技术栈主要有Java、Spring Boot和MongoDB等。
码如云的源代码是开源的可以通过以下方式访问 码如云源代码GitHub - mryqr-com/mry-backend: 本代码库为码如云后端代码。码如云是一个基于二维码的一物一码管理平台可以为每一件“物品”生成一个二维码手机扫码即可查看物品信息并发起相关业务操作操作内容可由你自己定义典型的应用场景包括固定资产管理、设备巡检以及物品标签等。在技术上码如云是一个无代码平台全程采用DDD、整洁架构和事件驱动架构思想完成开发。 CQRS #
CQRS(Command Query Responsibility Segregation)直译成中文叫命令查询职责分离可不要被这个读起来有些拗口的名字吓到了事实上就是读写分离的意思不过这里的读写分离和我们通常所理解的数据库级别的读写分离是两个不同的概念CQRS指的读写分离是指在应用程序内部的代码级别的读写分离在本文中我将对此做出详细解释。
简单来讲CQRS的提出是基于这么一种现象软件中写数据的操作和读数据的操作是两个很不一样的过程它们各有各的特点因此可以并且应该将它们作为两个单独的关注点分别进行处理。“写数据”的过程也被称为“命令Command”即表示外界通过向软件发送一些列的命令达到更新软件内部数据的目的比如更新用户偏好设置、向电商网站下单等“读数据”的过程也被称为“查询Query”即从软件中获取数据比如查看订单信息等。读和写的不同主要体现在以下几个方面
业务逻辑的运用主要是在写数据一侧也就是说我们在本系列的其他文章中讲到的聚合根实体值对象领域服务等领域模型中的概念主要用于“写数据”的过程相比之下“读数据”一侧的业务逻辑则相对较少主要是数据展现逻辑读数据是幂等的即无论通过什么方式都不应该修改系统中的数据也即读数据相对安全而在写数据时则需要始终保证数据的正确性和一致性否则将导致严重Bug导致读数据和写数据过程发生变更的归因不同对写数据侧的变更主要基于业务逻辑的变化而读数据侧的变更则更多基于UI需求的变化比如根据不同的屏幕尺寸返回不同的数据等读数据和写数据的频率往往各不相同对于多数业务来说写数据的频率往往低于读数据的频率。
事实上读写分离这种思想早在上世纪80年代末便由Bertrand Meyer提出在他的《Object-Oriented Software Construction》一书中指出 Every method should either be a command that performs an action, or a query that returns data to the caller, but never both. (一个方法要么作为一个“命令”执行一个操作要么作为一次“查询”向调用方返回数据但两者不能共存。 可以看出Bertrand Meyer所谓的读写分离主要用于对象中的方法(Method)而CQRS将这种思想扩大到了软件架构层面接下来让我们分别看看CQRS中的各种读写分离模式。
流程分离 #
最简单的读写分离模式莫过于读写流程的分离了事实上这也是我们一直在用的一种方式是的没错你已经在用CQRS了。为此让我们来看看一个具体的例子在码如云中有权限的成员(Member)可以更新表单(Submission)也可以查看表单详情数据前者是一个写数据的过程后者则是一个读数据的过程。更新表单的应用服务代码如下 //SubmissionCommandServiceTransactional
public void updateSubmission(String submissionId, UpdateSubmissionCommand command, User user) {Submission submission submissionRepository.byIdAndCheckTenantShip(submissionId, user);AppedQr appedQr qrRepository.appedQrById(submission.getQrId());App app appedQr.getApp();QR qr appedQr.getQr();Page page app.pageById(submission.getPageId());SubmissionPermissions submissionPermissions submissionPermissionChecker.permissionsFor(user,app,submission.getGroupId());submissionDomainService.updateSubmission(submission,app,page,qr,command.getAnswers(),submissionPermissions.getPermissions(),user);submissionRepository.houseKeepSave(submission, app);log.info(Updated submission[{}]., submissionId);
} 源码出处com/mryqr/core/submission/command/SubmissionCommandService.java 应用服务方法SubmissionCommandService.updateSubmission()通过调用领域服务SubmissionDomainService.updateSubmission()完成对表单的更新然后再通过SubmissionRepository.houseKeepSave()完成对表单的持久化。
在查看表单详情时的应用服务代码如下
//SubmissionQueryServicepublic QDetailedSubmission fetchDetailedSubmission(String submissionId, User user) {Submission submission submissionRepository.byIdAndCheckTenantShip(submissionId, user);//将领域对象Submission转为展现对象QDetailedSubmissionreturn toSubmissionDetail(submission, user);
}源码出处com/mryqr/core/submission/query/SubmissionQueryService.java 在SubmissionQueryService.fetchDetailedSubmission()方法中先获取到需要查询的表单聚合根对象Submission然后调用toSubmissionDetail()将Submission转换为展现对象QDetailedSubmission。
在上述2个代码例子中写数据和读数据使用了不同的应用服务方法也即流程分离了。你可能会说“我平时就是这么做的呀”的确如此这种方式正是大家平时的编码实现但是这里我们更希望强调的原则在于写数据的SubmissionCommandService.updateSubmission()返回的是void也即不会返回任何数据而读数据的SubmissionQueryService.fetchDetailedSubmission()则只是获取数据而未修改任何数据。
此外虽然SubmissionCommandService和SubmissionQueryService均表示应用服务但是在编码实现中被分成了2个单独的类以示分离。事实上在码如云我们在代码的分包层面也做了相应的对读写分离的支持所有与写数据相关的代码被组织在了command包下而所有与读数据相关的代码则被放在了query包下。 在查询数据时先获取到聚合根对象Submission再将其转化为展现对象QDetailedSubmission也就是说读数据和写数据的过程共享了同一个聚合根对象Submission。这种方式对于简单的查询场景没有多大问题但是对于一些复杂的查询场景来说并不合适一是使得读数据侧对写数据侧存在依赖二是在跨表查询的时候需要将多个聚合根对象分别从数据库中加载到内存导致对数据库的多次访问在高并发场景下这可能影响系统性能。
模型分离 #
既然业务逻辑主要作用于写数据侧而读数据侧主要处理的是展现逻辑那是不是在读数据时可以绕过领域模型上例中的Submission呢当然可以这就是模型分离。模型分离的主要特点是在写数据时依然严格按照领域模型对业务逻辑的请求处理流程但是在读数据时可以绕过领域模型直接从数据库创建相应的读模型对象。落到编码层面在写数据侧可能需要通过ORM等工具完成对聚合根的持久化但是在读数据侧则不见得我们全然可以通过直接的SQL语句从数据库中加载所需查询的数据。 在码如云租户管理员可以查看租户下所有的成员其查询实现如下
//MemberQueryServicepublic PagedListQListMember listMyManagedMembers(ListMyManagedMembersQuery queryCommand, User user) {String tenantId user.getTenantId();Pagination pagination pagination(queryCommand.getPageIndex(), queryCommand.getPageSize());String departmentId queryCommand.getDepartmentId();String search queryCommand.getSearch();Query query new Query(buildMemberQueryCriteria(tenantId, departmentId, search));long count mongoTemplate.count(query, Member.class);if (count 0) {return pagedList(pagination, 0, List.of());}query.skip(pagination.skip()).limit(pagination.limit()).with(sort(queryCommand));//绕过Member直接将从数据库中查到的数据创建为QListMemberquery.fields().include(name).include(avatar).include(role).include(mobile).include(wxUnionId).include(wxNickName).include(email).include(active).include(createdAt).include(departmentIds);ListQListMember members mongoTemplate.find(query, QListMember.class, MEMBER_COLLECTION);return pagedList(pagination, (int) count, members);
}源码出处com/mryqr/core/member/query/MemberQueryService.java 可以看到在查询成员列表时直接通过mongotTemplate码如云使用的是MongoDB将从数据库中所查询到的数据创建为了读模型QListMember省去了加载Member并从Member转化为QListMember的过程。
数据源分离 #
模型分离可以解决很大一部分读写分离的问题不过它依然是一种相对简单的CQRS实现方式对于更加复杂的查询场景来说则显得有些力不从心主要有以下原因
模型分离事实上只是代码层面模型的分离底层的数据库模型并未分离依然是读写共享的对于主要服务于写数据一侧的数据库来说可能由于对读数据一侧的“照料不周”而无法满足某些查询需求模型分离只能用于在同一个进程空间之内的查询也即所查询的数据均位于同一个数据库的场景但是对于诸如微服务这种需要跨进程查询的情况则无法满足比如对于一个采用微服务架构的电商系统在用户首页需要同时查看用户基本信息和积分但是前者位于“用户”服务中而后者来自于“积分”服务此时需要分别从2个服务中获取数据并返回给前端查询所需数据不一定能够直接映射到数据库中的字段而是有可能需要做一些额外的加工比如将省份(province)、城市(city)和详细地址(detailAddress)拼接为最终的地址值等。
数据源分离便是用来解决这个问题的在这种方式下我们为数据查询侧单独创建一个数据库这个数据库存在的目的仅仅是为了方便查询用可以说是为读数据侧量身定制的该数据库中的数据依然来自于写数据一侧只是经过了一些预先的加工比如根据查询端前端所需摒弃了一些无用的字段或者将多个字段合并成单个字段便于前端的直接显示等。那么数据又如何从写数据一侧传递到读数据一侧呢答案是领域事件。 在写数据时对业务数据的变更将通过领域事件的形式发布到消息队列Kafka中 读数据侧作为一个独立的模块通过消费这些领域事件完成对读模型数据库的相应更新之后在查询数据时则采用与“模型分离”相似的模式直接从数据库构建读模型最后返回给查询方前端。
在技术栈的选择上读数据侧的数据库不必与写数据库保持一致比如写数据侧可以采用诸如MySQL这种强事务一致性的数据库为了保证业务数据的正确性但是读数据侧可以采用更有利于数据查询的数据库比如ElasticSearch等。
事实上以上3种CQRS的实现模式并不是彼此互斥的而是可以同时存在哪种方式相对简单则采用哪种方式。比如在码如云我们便同时采用了3种方式。
总结 #
CQRS即是读写分离的意思它将软件中的写数据过程和读数据过程分开处理各司其职是一种可以在很大程度上简化软件架构的编程模式。在这种模式下写数据的过程严格遵循DDD的各种原则而读数据的过程则可以绕开DDD中的领域模型主要是聚合根直接从数据库构建需要查询的数据模型。根据具体场景的不同可以采用不同的CQRS实现模式。