10CQRS

news2024/11/24 14:19:48

本系列包含以下文章:

  1. DDD入门
  2. DDD概念大白话
  3. 战略设计
  4. 代码工程结构
  5. 请求处理流程
  6. 聚合根与资源库
  7. 实体与值对象
  8. 应用服务与领域服务
  9. 领域事件
  10. 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),也可以查看表单详情数据,前者是一个写数据的过程,后者则是一个读数据的过程。更新表单的应用服务代码如下:


//SubmissionCommandService

@Transactional
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()完成对表单的持久化。

在查看表单详情时的应用服务代码如下:

//SubmissionQueryService

public QDetailedSubmission fetchDetailedSubmission(String submissionId, User user) {
    Submission submission = submissionRepository.byIdAndCheckTenantShip(submissionId, user);

    //将领域对象Submission转为展现对象QDetailedSubmission
    return toSubmissionDetail(submission, user);
}

源码出处:com/mryqr/core/submission/query/SubmissionQueryService.java

SubmissionQueryService.fetchDetailedSubmission()方法中,先获取到需要查询的表单聚合根对象Submission,然后调用toSubmissionDetail()Submission转换为展现对象QDetailedSubmission

在上述2个代码例子中,写数据和读数据使用了不同的应用服务方法,也即流程分离了。你可能会说“我平时就是这么做的呀!”,的确如此,这种方式正是大家平时的编码实现,但是这里我们更希望强调的原则在于:写数据的SubmissionCommandService.updateSubmission()返回的是void,也即不会返回任何数据,而读数据的SubmissionQueryService.fetchDetailedSubmission()则只是获取数据而未修改任何数据。

此外,虽然SubmissionCommandServiceSubmissionQueryService均表示应用服务,但是在编码实现中被分成了2个单独的类以示分离。事实上,在码如云我们在代码的分包层面也做了相应的对读写分离的支持,所有与写数据相关的代码被组织在了command包下,而所有与读数据相关的代码则被放在了query包下。

在查询数据时,先获取到聚合根对象Submission,再将其转化为展现对象QDetailedSubmission,也就是说读数据和写数据的过程共享了同一个聚合根对象Submission。这种方式对于简单的查询场景没有多大问题,但是对于一些复杂的查询场景来说并不合适,一是使得读数据侧对写数据侧存在依赖,二是在跨表查询的时候,需要将多个聚合根对象分别从数据库中加载到内存,导致对数据库的多次访问,在高并发场景下,这可能影响系统性能。

模型分离 #

既然业务逻辑主要作用于写数据侧,而读数据侧主要处理的是展现逻辑,那是不是在读数据时可以绕过领域模型(上例中的Submission)呢?当然可以,这就是模型分离。模型分离的主要特点是:在写数据时,依然严格按照领域模型对业务逻辑的请求处理流程,但是在读数据时,可以绕过领域模型,直接从数据库创建相应的读模型对象。落到编码层面,在写数据侧可能需要通过ORM等工具完成对聚合根的持久化,但是在读数据侧则不见得,我们全然可以通过直接的SQL语句从数据库中加载所需查询的数据。

在码如云,租户管理员可以查看租户下所有的成员,其查询实现如下:

//MemberQueryService

public PagedList<QListMember> 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,直接将从数据库中查到的数据创建为QListMember
    query.fields().include("name").include("avatar").include("role").include("mobile")
            .include("wxUnionId").include("wxNickName").include("email")
            .include("active").include("createdAt").include("departmentIds");
    
    List<QListMember> 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实现方式,对于更加复杂的查询场景来说则显得有些力不从心,主要有以下原因:

  1. 模型分离事实上只是代码层面模型的分离,底层的数据库模型并未分离,依然是读写共享的,对于主要服务于写数据一侧的数据库来说,可能由于对读数据一侧的“照料不周”而无法满足某些查询需求;
  2. 模型分离只能用于在同一个进程空间之内的查询,也即所查询的数据均位于同一个数据库的场景,但是对于诸如微服务这种需要跨进程查询的情况则无法满足,比如对于一个采用微服务架构的电商系统,在用户首页需要同时查看用户基本信息和积分,但是前者位于“用户”服务中,而后者来自于“积分”服务,此时需要分别从2个服务中获取数据并返回给前端;
  3. 查询所需数据不一定能够直接映射到数据库中的字段,而是有可能需要做一些额外的加工,比如将省份(province)城市(city)详细地址(detailAddress)拼接为最终的地址值等。

数据源分离便是用来解决这个问题的,在这种方式下,我们为数据查询侧单独创建一个数据库,这个数据库存在的目的仅仅是为了方便查询用,可以说是为读数据侧量身定制的,该数据库中的数据依然来自于写数据一侧,只是经过了一些预先的加工,比如根据查询端(前端)所需摒弃了一些无用的字段,或者将多个字段合并成单个字段便于前端的直接显示等。那么,数据又如何从写数据一侧传递到读数据一侧呢?答案是领域事件。

在写数据时,对业务数据的变更将通过领域事件的形式发布到消息队列(Kafka)中, 读数据侧作为一个独立的模块通过消费这些领域事件完成对读模型数据库的相应更新,之后在查询数据时,则采用与“模型分离”相似的模式直接从数据库构建读模型,最后返回给查询方(前端)。

在技术栈的选择上,读数据侧的数据库不必与写数据库保持一致,比如写数据侧可以采用诸如MySQL这种强事务一致性的数据库(为了保证业务数据的正确性),但是读数据侧可以采用更有利于数据查询的数据库,比如ElasticSearch等。

事实上,以上3种CQRS的实现模式并不是彼此互斥的,而是可以同时存在,哪种方式相对简单则采用哪种方式。比如,在码如云我们便同时采用了3种方式。

总结 #

CQRS即是读写分离的意思,它将软件中的写数据过程和读数据过程分开处理,各司其职,是一种可以在很大程度上简化软件架构的编程模式。在这种模式下,写数据的过程严格遵循DDD的各种原则,而读数据的过程则可以绕开DDD中的领域模型(主要是聚合根),直接从数据库构建需要查询的数据模型。根据具体场景的不同,可以采用不同的CQRS实现模式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1044673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SigFit—光-机-热耦合分析工具

美国Sigmadyne公司的SigFit软件是光机热耦合分析工具&#xff0c;可以将有限元分析得到的光学表面变形等结果文件通过多项式拟合或插值转化为光学分析软件的输入文件&#xff0c;还可实现动态响应分析、光程差分析、设计优化、主动控制/自适应控制光学系统的促动器布局及优化等…

美篇作文网教学资源源码-自带作文数据

非常漂亮的UI设计和页面排版&#xff01; 自适应手机pc端 页面内容均支持自定义 可以用来做网站矩阵&#xff0c;或者增强你其他网站板块&#xff0c;或者单独运营都可以。 可以通过广告方式变现&#xff0c;或者引流等等 友好的seo&#xff0c;更容易被浏览器收录 关注青狐…

软件测试面试题 —— 整理与解析(5)

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

5153. 删除

题目&#xff1a; 样例1&#xff1a; 输入 3454 输出 YES 344 样例2&#xff1a; 输入 10 输出 YES 0 样例3&#xff1a; 输入 111111 输出 NO 思路&#xff1a; 这道题就三个条件 关键条件是 能够被 8 整除。 而能被 8 整除的有一个重要的性质是 能够被 8 整除的重要…

MySQL - 全表分组后,获取组内排序首条数据信息

性能 不详!!! 不详!!! 不详!!! 请谨慎使用!!!环境 MySQL服务: 8.0版本;思路 使用8.0版本的新函数特性: row_number(): 序号函数; 顾名思义, 就是给每组中的元素从1开始按顺序加上序号;over(): 其中两个语法如下 partition: 按某字段分组;order by: 按某字段排序;注意: 两函数详…

【深度学习实验】前馈神经网络(final):自定义鸢尾花分类前馈神经网络模型并进行训练及评价

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 构建数据集&#xff08;IrisDataset&#xff09; 2. 构建模型&#xff08;FeedForward&#xff09; a. __init__(初始化) b. forward(前向传播) 3.整合训练、评估…

Json文件反序列化读取

Json文件 [{"name":"清华大学","location":"北京","grade":"1"},{"name":"北京大学","location":"北京","grade":"2"} ] 安装包 代码 Program.c…

Redis应用(8)——Redis的项目应用:结合SpringBoot如何在Redis里面存对象 RedisUtil工具类的封装 遇到的问题

前言 Redis作为一款优秀的开源、高效的内存数据库&#xff0c;在各种项目中都能见到其身影&#xff0c;熟练使用Redis是程序员必备的技能之一。本系列博客结合应用场景&#xff0c;阐述Redis从安装到使用的&#xff0c;从入门到进阶的相关内容。 本篇博客介绍在Spring项目中&…

免费AI人工智能,人工智能写文章软件

在当今信息爆炸的时代&#xff0c;内容创作已经成为了无处不在的需求。从博客、新闻报道到广告宣传&#xff0c;人们对于高质量的文本内容的需求愈发迫切。然而&#xff0c;面对繁忙的生活节奏和不断增长的写作任务&#xff0c;许多创作者感到焦头烂额。 从博客作者到广告写手&…

系统集成|第十六章(笔记)

目录 第十六章 信息&#xff08;文档&#xff09;和配置管理16.1 文档管理16.2 配置管理 上篇&#xff1a;第十五章、采购管理 下篇&#xff1a;第十七章、变更管理 第十六章 信息&#xff08;文档&#xff09;和配置管理 16.1 文档管理 信息系统项目相关信息&#xff08;文档…

记录一次错误---想让U-net网络输入大小不一致的图片

最近在看Deeplab系列的论文&#xff0c;文中提到了语义分割领域的一个难题是&#xff1a;将图片输入网络之前需要resize成统一大小&#xff0c;但是resize的话会造成细节信息的损失&#xff0c;所以想要网络处理任意大小的图片输入。我之前训练的U-net网络都是resize成224*224大…

基于若依ruoyi-nbcio支持flowable流程角色,同时修改流转用户为username,流程启动做大调整(三)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; ruoyi-nbcio: nbcio-boot的若依版本,基于ruoyi-flowable-plus和flowable6.7.2&#xff0c;目前处于移植功能阶段&#xff0c;目标是打造一个最好的若依平台上flowable流程管理系统开源版本&#xff0c…

【项目】Http服务器

【项目】Http服务器 项目简介 背景&#xff1a; http协议被广泛使用&#xff0c;从移动端&#xff0c;pc端浏览器&#xff0c;http协议无疑是打开互联网应用窗口的重要协议&#xff0c;http在网络应用层中的地位不可撼动&#xff0c;是能准确区分前后台的重要协议。 描述&a…

MD5 绕过第一式:弱比较绕过

文章目录 参考环境MD5韧性脆弱性md5() 隐式类型转换字符串连接数学运算布尔判断相等运算符 科学计数法科学计数法前缀 0E 与 0e PHP8 与 PHP 其他版本下字符串转化为数值的具体规则PHP8数值字符串优化 其他版本更为详细的讲解 字符串与字符串的弱比较字符串与数值的弱比较0e215…

git查看自己所在的分支

很多时候可能大家不太想切换其他工具&#xff0c;又不知道自己是否在自己需要操作的分支 可以直接终端执行 git branch此时 他就会在终端将所有的本地分支输出出来 并特殊标注自己所在的分支 这样我们就可以进一步去做自己想要做的操作了 当然 随着各种编辑器的发展 这个命令…

视频剪辑软件哪个好? 2023年最新功能解析

如今聊到视频的话题&#xff0c;大家可能都注意到近年来火爆的抖音短视频。从图片到动画&#xff0c;从动画到视频。时代在发展&#xff0c;技术在更新。到了近几年小视频也火了&#xff0c;其实不乏缺有视频剪辑软件的功劳。现在不少朋友在业余时间都喜欢剪辑视频来丰富自己的…

支撑电动汽车规模化,特来电智能化升级群充产品

9月26日&#xff0c;中国领先的充电网生态运营商特来电重磅发布智能群充4.0产品&#xff0c;标志着特来电群充产品体系进一步升级&#xff0c;充电行业迎来更高质量、更高性能的设备与系统&#xff0c;充电网基础设施将更好地支撑大规模电动汽车的发展。 群充技术路线引领充电…

音频处理基础的一些笔记碎片

感谢 B 站上关于 EQ 扫盲视频分享 &#x1f50a;声音变好听-让均衡器改变你的音色|均衡器怎么调&#xff1f;&#xff08;adobe audition 2020基础教程&#xff09; 简要内容 a. 人耳能够听到的频率 20HZ-20000HZ&#xff1b; b. 人耳普遍认为低于 80HZ 是噪声&#xff1b; c. …

华为智能企业上网行为管理安全解决方案(1)

华为智能企业上网行为管理安全解决方案&#xff08;1&#xff09; 课程地址方案背景需求分析企业上网行为概述企业上网行为安全风险分析企业上网行为管理需求分析 方案设计组网架构设备选型设备简介行为管理要点分析方案功能概述 课程地址 本方案相关课程资源已在华为O3社区发…

MQTT协议知识梳理,看完你就懂了!

一.MQTT简介 MQTT是基于TCP/IP协议栈构建的异步通信消息协议&#xff0c;是一种轻量级的发布/订阅信息传输协议。MQTT在时间和空间上&#xff0c;将消息发送者与接受者分离&#xff0c;可以在不可靠的网络环境中进行扩展。 适用于设备硬件存储空间有限或网络带宽有限的场景。物…