DDD该怎么去落地实现(4)多对多关系

多对多关系的设计实现

如题,DDD该如何落地呢?前面我通过三期的内容,讲解了DDD落地的关键在于“关系”,也就是通过前面我们对业务的理解先形成领域模型,然后将领域模型的原貌,形成程序代码中的服务、实体、值对象。在这个过程中,将实体与值对象形成领域对象的同时,还要保留领域模型中对象之间的关系。这种关系,除了有“一对一”、“多对一”、“一对多”、“多对多”关系以外,还可以有“继承”关系。在DDD落地编码时,要非常准确地将这五种关系,通过程序表达出来。因此,在编码过程中,仅仅依靠领域对象是不足够的,还需要通过DSL辅助表达。譬如,订单与客户、地址、明细之间的关系,先编写一个“订单”对象:

@Data
@EqualsAndHashCode(callSuper = true)
public class Order extends Entity<Long> {private Long id;private Long customerId;private Long addressId;private Double amount;private Date orderTime;private Date modifyTime;private String status;private Customer customer;private Address address;private Payment payment;private List<OrderItem> orderItems;...
}

在这样的基础上,再为订单对象编写DSL,对它们之间的关系进行补充说明:

<do class="com.edev.trade.order.entity.Order" tableName="t_order"><property name="id" column="id" isPrimaryKey="true"/><property name="customerId" column="customer_id"/><property name="addressId" column="address_id"/><property name="amount" column="amount"/><property name="orderTime" column="order_time"/><property name="modifyTime" column="modify_time"/><property name="status" column="status"/><join name="customer" joinKey="customerId" joinType="manyToOne"class="com.edev.trade.order.entity.Customer"/><join name="address" joinKey="addressId" joinType="manyToOne"class="com.edev.trade.order.entity.Address"/><join name="payment" joinType="oneToOne" isAggregation="true"class="com.edev.trade.order.entity.Payment"/><join name="orderItems" joinKey="orderId" joinType="oneToMany"isAggregation="true" class="com.edev.trade.order.entity.OrderItem"/>
</do>

除了这五种关系以外,它们之间还可能存在聚合关系,也在DSL中进行说明,譬如订单与明细之间就有聚合关系。只有“一对一”与“一对多”关系才可能出现聚合关系。有了DSL的详细描述,后面的所有增删改与查询操作,都遵循以上这些关系进行底层数据库的操作。对于业务开发人员来说,只要根据领域模型完成了领域对象、DSL与领域服务的开发,所有底层数据库的操作都不必操心了,开发就得到了简化,从而使得DDD落地变得容易。DDD落地实现的思路就在于此。

然而,对于业务开发人员来说,不必关注底层数据库的操作,对于底层平台开发的人员却必须要关注。前面,我已经讲解了“一对一”、“多对一”和“一对多”这三个关系的设计实现,即通过开发一个DDD的通用平台,实现通用的仓库与工厂。所有的Service只要注入了这个通用仓库就可以完成底层数据库的持久化。那么,除了它们,“多对多”和“继承”的关系又该如何实现呢?今天我们先来看看“多对多”关系。

在现实世界中,多对多关系其实并不常见,但也还是有的。比较典型的例子就是“用户”与“权限”的关系。一个用户可以申请多个权限,同时一个权限也可以分配给多个用户,它们之间就形成了“多对多”关系。然而,要将这个多对多关系落地实现会比较困难,因此通常会在它们之间增加一个关联类。有了这个关联类,就可以将这个多对多关系转变成两个多对一关系,或者两个一对多关系。这样,对于多对多关系的设计实现就有两种,我们先来看两个多对一关系的实现。

如上图,我们在“用户”与“权限”之间增加了一个关联类:用户-权限关联类。这样的设计就将多对多关系变成了两个多对一关系:关联类与用户是多对一关系、关联类与权限也是多对一关系。有了这三个类,它们就分别对应三个数据库的表:用户表、权限表、用户-权限关联表。如下图,可以看到,用户-权限关联表的主键,是由用户ID与权限ID组成的联合主键,或者先设计一个无意义的自动生成主键,然后由它俩形成一个唯一键。

按照这样的思路,就可以分别编写这三个领域对象及其对应的DSL,并创建相应的表。用户表存储用户信息,并通过用户Service进行增删改查的操作;权限表存储权限信息,并通过权限Service进行增删改查的操作;关联表存储用户授权的信息,并通过用户授权Service进行增删改查的操作,就可以完成多对多的设计实现,思路也并不复杂。

然而,基于以上的设计,一个用户要查询它的所有权限,或者一个权限要查询它分配给哪些用户,查询起来就比较麻烦。首先,要获取该用户的ID,去关联表中查找它的所有授权。然后,还要对所有这些授权的ID,到授权表中查询它们的详细信息。最后,将这个授权集合,写入用户对象中的“授权”属性中。以上这些操作都必须由业务开发人员来完成,开发工作量就会比较大。

除了以上的设计思路以外,另一个思路就是将多对多关系变成两个一对多关系,即用户与关联类是一对多关系,权限与关联类是一对多关系(如上图)。这样的设计,表结构不变,业务开发人员只需要在用户对象中增加一个“授权”的集合属性,在授权对象中增加一个“用户”的集合属性:

@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Entity<Long> {private Long id;private String username;private String password;private int accountExpired;private int accountLocked;private int credentialsExpired;private int disabled;private String userType;private Collection<Authority> authorities = new ArrayList<>();public void addAuthority(Authority authority) {this.authorities.add(authority);}
}

然后在DSL中进行如下配置,就可以实现多对多关系:

<do class="com.edev.emall.authority.entity.User" tableName="t_user" subclassType="joined"><property name="id" column="id" isPrimaryKey="true"/><property name="username" column="username"/><property name="password" column="password"/><property name="accountExpired" column="account_expired"/><property name="accountLocked" column="account_locked"/><property name="credentialsExpired" column="credentials_expired"/><property name="disabled" column="disabled"/><property name="userType" column="user_type" isDiscriminator="true"/><join name="authorities" joinKey="userId" joinType="manyToMany" joinClassKey="authorityId"joinClass="com.edev.emall.authority.entity.UserGrantedAuthority"class="com.edev.emall.authority.entity.Authority"/>
</do>

可以看到,在DSL中配置多对多关系时,joinClass配置的就是那个关联类,joinKey是用户与关联类进行关联的字段,joinClassKey是关联类与权限进行关联的字段。通过这样的配置,业务开发人员很容易就可以完成多对多关系的开发。

当然,除了编写用户和授权的领域对象与DSL,还要编写关联类的领域对象与DSL:

@Data
@EqualsAndHashCode(callSuper = true)
public class UserGrantedAuthority extends Entity<Long> {private Long id;private String available;private Long userId;private Long authorityId;public Boolean getAvailable() {return "Y".equals(available);}public void setAvailable(Boolean available) {this.available = (available!=null&&available ? "Y" : "N");}
}

显然,我们不需要编写对关联类和关联表操作的代码,对它们的操作都封装在了底层平台中了。更详细的编码,可以查看我的示例:

DSL文件

测试代码

有了以上的设计,当需要为用户授权时,授权信息是作为用户对象中的一个属性,由前端进行提交:

{"id": 1,"username": "Mooodo","password": "{noop}4321","accountExpired": false,"accountLocked": false,"credentialsExpired": false,"disabled": false,"userType": "administrator","authorities": [{"id": 999},{"id": 998}]
}

紧接着,后台在完成对用户信息的增删改的同时,就可以完成对该用户的授权。比如,在添加新用户时添加授权的信息,就可以同时对该用户进行授权。这时,该用户通过用户Service创建用户时,后台的仓库就会同时插入用户表和关联表,完成用户添加与授权的操作。当该用户已经创建好了,现在要对他的授权进行增删改操作时,只需要在用户对象中对“授权”这个集合属性进行增删改,然后更新该用户,那么后台的仓库就会比对“授权”属性是否存在变更。如果有变更,就会完成对关联表的增删改操作,从而完成对授权的变更。通过这样的设计,业务开发人员只需要按照领域模型的要求设计领域对象,将DSL配置成多对多关系,然后在Service中直接操作相应的领域对象就可以了,而不必再关心数据库的持久化,使设计得到了简化。

除了增删改以外,多对多关系的查询该怎么做呢?在以上案例中,只要完成了对用户与权限的对象编写与DSL配置,查询用户时就可以自动带出该用户的所有授权。对于业务开发人员来说,这个操作非常简单,但DDD的底层平台却需要做很多事情。底层平台首先通过读取DSL配置文件获取它们的多对多关系,然后根据用户对象去查找对应的关联对象,然后对所有的关联对象去查找对应的授权对象。最后,将这个授权对象的列表放到用户对象的“权限”属性中,完成整个查询的过程。

这时,如果查找的是一个用户列表,是否会存在性能的问题呢?比如现在要制作一个用户查询的功能,如上一期所讲的,先编写一个MyBatis的mapper对用户表进行查询,然后配置一个AutofillQueryServiceImpl来补填用户对象的关联信息。现在通过一个条件查询了100个用户,但通常不会往前端直接返回这100个用户,而是通过分页只返回这一页的20个用户。那么,对这20个用户,先获得它们的用户ID列表,通过这个列表在关联表中一次性查询这20个用户的所有授权,然而对这些授权ID的列表,在权限表中一次性查询出对应的权限信息。最后,再由通用工厂对所有这些信息进行拼装,将每个用户的权限放进该用户的“权限”属性中。底层通过这样的设计,既可以完成对所有用户对象的补填,又可以最大程度保证性能。特别是,在补填的过程中还可以增加Redis缓存,进一步提升查询性能。

总之,多对多关系的设计实现需要中间增加一个关联类,从而形成了两种设计方案。第一个方案:转变成两个多对一关系,设计比较简单,但在查询用户的授权信息时比较麻烦,业务开发人员的工作量就会比较大;第二个方案:转变成两个一对多关系,需要比较强大的DDD底层平台的支持,但上层业务开发就会变得比较简单。两个方案都各有各自的优缺点,大家可以根据各自的情况权衡利弊,进行选择。下一期我们将探讨五种关系中设计最难、最复杂的关系:继承关系的实现。

相关的文章:

《DDD该怎么去落地实现(1)关键是“关系”》

《DDD该怎么去落地实现(2)难度是“聚合”》

《DDD该怎么去落地实现(3)通用的仓库和工厂》

(待续)

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

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

相关文章

【补阙拾遗】排序之冒泡、插入、选择排序

炉烟爇尽寒灰重&#xff0c;剔出真金一寸明 冒泡排序1. 轻量化情境导入 &#x1f30c;2. 边界明确的目标声明 &#x1f3af;3. 模块化知识呈现 &#x1f9e9;&#x1f4ca; 双循环结构对比表★★★⚠️ 代码关键点注释 4. 嵌入式应用示范 &#x1f6e0;️5. 敏捷化巩固反馈 ✅ …

前端面试题---小程序跟vue的声明周期的区别

1. 小程序生命周期 小程序的生命周期主要分为 页面生命周期 和 应用生命周期。每个页面和应用都有自己独立的生命周期函数。 应用生命周期 小程序的应用生命周期函数与全局应用相关&#xff0c;通常包括以下几个钩子&#xff1a; onLaunch(options)&#xff1a;应用初始化时触…

【芯片设计】NPU芯片前端设计工程师面试记录·20250227

应聘公司 某NPU/CPU方向芯片设计公司。 小声吐槽两句,前面我问了hr需不需要带简历,hr不用公司给打好了,然后我就没带空手去的。结果hr小姐姐去开会了,手机静音( Ĭ ^ Ĭ )面试官、我、另外的hr小姐姐都联系不上,结果就变成了两个面试官和我一共三个人在会议室里一人拿出…

让Word插上AI的翅膀:如何把DeepSeek装进Word

在日常办公中&#xff0c;微软的Word无疑是我们最常用的文字处理工具。无论是撰写报告、编辑文档&#xff0c;还是整理笔记&#xff0c;Word都能胜任。然而&#xff0c;随着AI技术的飞速发展&#xff0c;尤其是DeepSeek的出现&#xff0c;我们的文字编辑方式正在发生革命性的变…

点击修改按钮图片显示有问题

问题可能出在表单数据的初始化上。在 ave-form.vue 中&#xff0c;我们需要处理一下从后端返回的图片数据&#xff0c;因为它们可能是 JSON 字符串格式。 vue:src/views/tools/fake-strategy/components/ave-form.vue// ... existing code ...Watch(value)watchValue(v: any) …

vue深拷贝:1、使用JSON.parse()和JSON.stringify();2、使用Lodash库;3、使用深拷贝函数(采用递归的方式)

文章目录 引言三种方法的优缺点在Vue中,实现数组的深拷贝I JSON.stringify和 JSON.parse的小技巧深拷贝步骤缺点:案例1:向后端请求路由数据案例2: 表单数据处理时复制用户输入的数据II 使用Lodash库步骤适用于复杂数据结构和需要处理循环引用的场景III 自定义的深拷贝函数(…

线性模型 - 支持向量机

支持向量机&#xff08;SVM&#xff09;是一种用于分类&#xff08;和回归&#xff09;的监督学习算法&#xff0c;其主要目标是找到一个最佳决策超平面&#xff0c;将数据点分为不同的类别&#xff0c;并且使得分类边界与最近的数据点之间的间隔&#xff08;margin&#xff09…

记录一次解决springboot需要重新启动项目才能在前端界面展示静态资源的问题--------使用热部署解决

问题 使用sprinbootthymeleaf&#xff0c;前后端不分离&#xff0c;一个功能是用户可以上传图片&#xff0c;之后可以在网页展示。用户上传的图片能在对应的静态资源目录中找到&#xff0c;但是在target目录没有&#xff0c;导致无法显示在前端界面 解决 配置热部署 <depe…

【Python pro】函数

1、函数的定义及调用 1.1 为什么需要函数 提高代码复用性——封装将复杂问题分而治之——模块化利于代码的维护和管理 1.1.1 顺序式 n 5 res 1 for i in range(1, n1):res * i print(res) # 输出&#xff1a;1201.1.2 抽象成函数 def factorial(n):res 1for i in range(1…

[Web 信息收集] Web 信息收集 — 手动收集 IP 信息

关注这个专栏的其他相关笔记&#xff1a;[Web 安全] Web 安全攻防 - 学习手册-CSDN博客 0x01&#xff1a;通过 DNS 服务获取域名对应 IP DNS 即域名系统&#xff0c;用于将域名与 IP 地址相互映射&#xff0c;方便用户访问互联网。对于域名到 IP 的转换过程则可以参考下面这篇…

大语言模型的评测

大语言模型评测是评估这些模型在各种任务和场景下的性能和能力的过程。 能力 1. 基准测试&#xff08;Benchmarking&#xff09; GLUE&#xff08;General Language Understanding Evaluation&#xff09;&#xff1a;包含多个自然语言处理任务&#xff0c;如文本分类、情感分…

Node.js与MySQL的深入探讨

Node.js与MySQL的深入探讨 引言 Node.js,一个基于Chrome V8引擎的JavaScript运行时环境,以其非阻塞、事件驱动的方式在服务器端应用中占据了一席之地。MySQL,作为一款广泛使用的开源关系型数据库管理系统,凭借其稳定性和高效性,成为了许多应用的数据库选择。本文将深入探…

STM32--SPI通信讲解

前言 嘿&#xff0c;小伙伴们&#xff01;今天咱们来聊聊STM32的SPI通信。SPI&#xff08;Serial Peripheral Interface&#xff09;是一种超常用的串行通信协议&#xff0c;特别适合微控制器和各种外设&#xff08;比如传感器、存储器、显示屏&#xff09;之间的通信。如果你…

基于定制开发开源AI大模型S2B2C商城小程序的商品选品策略研究

摘要&#xff1a;随着电子商务的蓬勃发展和技术的不断进步&#xff0c;商品选品在电商领域中的重要性日益凸显。特别是在定制开发开源AI大模型S2B2C商城小程序的环境下&#xff0c;如何精准、高效地选择推广商品&#xff0c;成为商家面临的一大挑战。本文首先分析了商品选品的基…

C#异步编程之async与await

一&#xff1a;需求起因 在 C# 中使用异步编程&#xff08;特别是使用 async 和 await 关键字&#xff09;通常是为了提高应用程序的响应性和性能&#xff0c;特别是在需要进行 I/O 操作或执行长时间运行的任务时。 常见应用场景如下&#xff1a; 1. 网络请求 HTTP 请求&…

PMP项目管理—整合管理篇—7.结束项目或阶段

文章目录 基本信息过程4W1HITTO输入工具与技术输出 收尾过程组项目收尾&#xff08;结束项目或阶段&#xff09;行政收尾/管理收尾 合同收尾&#xff08;结束采购&#xff09; 最终报告 基本信息 项目无论何因何时终止&#xff0c;都必须用结束项目或阶段过程来正式关闭。通过…

labview中VISA串口出现异常的解决方案

前两天在做项目时发现&#xff0c;当用VISA串口读取指令时出现了回复异常的情况&#xff0c;不管发什么东西就一直乱回&#xff0c;针对这个情况&#xff0c;后面在VISA串口中加了一个VISA寄存器清零的函数。加了之后果然好多了&#xff0c;不会出现乱回的情况&#xff0c;但是…

【GO】学习笔记

目录 学习链接 开发环境 开发工具 GVM - GO多版本部署 GOPATH 与 go.mod go常用命令 环境初始化 编译与运行 GDB -- GNU 调试器 基本语法与字符类型 关键字与标识符 格式化占位符 基本语法 初始值&零值&默认值 变量声明与赋值 _ 下划线的用法 字…

staruml绘制时序图和用例图

文章目录 1.文章介绍2.绘制用例图3.绘制时序图 1.文章介绍 之前&#xff0c;我们初步介绍了这个staruml软件的安装和如何使用这个软件对于uml类图进行绘制&#xff0c;当时我们是绘制了这个user类&#xff0c;实现了相关的接口&#xff0c;表示他们之间的关系&#xff0c;在今…

开放标准(RFC 7519):JSON Web Token (JWT)

开放标准&#xff1a;JSON Web Token 前言基本使用整合Shiro登录自定义JWT认证过滤器配置Config自定义凭证匹配规则接口验证权限控制禁用session缓存的使用登录退出单用户登录Token刷新双Token方案单Token方案 前言 JSON Web Token &#xff08;JWT&#xff09; 是一种开放标准…