手把手教你落地 DDD
钟敬
Thoughtworks 首席咨询师、数字化转型与运营团队 DDD 负责人
19697 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 45 讲
AIGC特别策划 (2讲)
结束语&结课测试 (2讲)
手把手教你落地 DDD
15
15
1.0x
00:00/00:00
登录|注册

17|聚合的实现(下):怎样用事务保护聚合?

你好,我是钟敬。
上节课我们完成了添加员工的功能,并且实现了关于技能工作经验不变规则。今天我们重点要做两件事。第一,是继续完成修改员工的功能。
另外,假如不考虑并发的情况,上节课的逻辑已经足以保证不变规则了。但是正如我们在第 14 节课讲聚合概念的时候讨论的,在并发环境下,这些规则仍然可能被破坏。所以今天的第二件事就是用事务来解决这一问题。

修改聚合对象

上节课,我们在员工实体(Emp)里只实现了添加技能【addSkill()】的方法。如果要修改员工聚合,我们还要编写修改技能删除技能的方法。对于工作经验岗位也是一样的。
我们先看看在领域层实现这些逻辑的代码。
package chapter17.unjuanable.domain.orgmng.emp;
// imports
public class Emp extends AuditableEntity {
//属性、构造器、其他方法 ...
public Optional<Skill> getSkill(Long skillTypeId) {
return skills.stream()
.filter(s -> s.getSkillTypeId() == skillTypeId)
.findAny();
}
public void addSkill(Long skillTypeId, SkillLevel level
, int duration, Long userId) {
// 上节课已经实现...
}
public Emp updateSkill(Long skillTypeId, SkillLevel level
, int duration, Long userId) {
Skill theSkill = this.getSkill(skillTypeId)
.orElseThrow(() ->
new BusinessException("不存在要修改的skillTypeId!"));
if (theSkill.getLevel() != level
|| theSkill.getDuration() != duration) {
theSkill.setLevel(level)
.setDuration(duration)
.setLastUpdatedBy(userId)
.setLastUpdatedAt(LocalDateTime.now())
.toUpdate(); //设置修改状态
}
return this;
}
public Emp deleteSkill(Long skillTypeId) {
this.getSkill(skillTypeId)
.orElseThrow(() -> new BusinessException(
"不存在要删除的skillTypeId!"))
.toDelete(); //设置修改状态
return this;
}
public void addExperience(LocalDate startDate, LocalDate endDate, String company, Long userId) {
durationShouldNotOverlap(startDate, endDate);
// 与Skill的处理类似...
}
public Emp updateExperience(LocalDate startDate, LocalDate endDate, String company, Long userId) {
// 与Skill的处理类似...
}
public Emp deleteExperience(LocalDate startDate, LocalDate endDate) {
// 与Skill的处理类似...
}
public Emp addEmpPost(String postCode, Long userId) {
// 与Skill的处理类似...
}
public Emp deleteEmpPost(String postCode, Long useId) {
// 与Skill的处理类似...
}
}
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了在软件开发中使用事务来保护聚合对象的实现过程。首先介绍了在领域层实现修改员工聚合对象的逻辑,包括添加、修改和删除技能、工作经验等方法的实现。然后详细讲解了用于修改聚合的应用服务,包括对员工聚合进行更新并保存到数据库的过程。接着,文章详细讲解了用于修改聚合的应用服务中的updator类的代码实现,包括对技能的增删改操作。最后,文章提到了对工作经验的修改类似于对技能的修改,但没有具体展开。整体来说,本文通过具体的代码示例和逻辑讲解,深入浅出地介绍了如何用事务来保护聚合对象,对于开发人员来说具有很高的参考价值。文章还讨论了乐观锁和单实体聚合的处理,为读者提供了更多的技术思考和解决问题的方法。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《手把手教你落地 DDD》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(19)

  • 最新
  • 精选
  • Hesher
    回答下课后问题: 1. 业务校验可以加一个标志位判断是否是重建,重建就跳过校验; 2. 悲观锁就是分布式锁或者数据库select for update,建议用分布式锁。

    作者回复: 1、是个办法。此外还可以做一个专用于重建的Builder,或者用反射, 2、这个没错

    2023-04-14归属地:北京
    4
  • 老师,组织Org里面包含了多个员工Emp,为什么不可以理解为组合和员工构成一个聚合,组织是聚合根,而员工是实体呢?

    作者回复: 员工和组织不存在“强”的整体部分关系。比如,一个员工,可能属于多个组织,一个组织删除了,不代表下面的员工也会被删除。相反,订单和订单项是聚合。你可以体会一下。

    2023-04-03归属地:广东
    3
    4
  • 张逃逃
    有个疑问想请教老师,为什么EmpRepository在查找Emp的时候不把对应Emp的所有状态(包括技能,工作经验...)全部查出来,然后通过Emp的构造参数来实例化对象,而是先实例化对象再调用addSkill()等方法来初始化,如果用构造方法来实例化对象,好像就不需要RebuiltEmp了。

    作者回复: 你说的也是一种可行的做法。Evans认为,如果构造器太复杂,就掩盖了对象的主要职责,所以这时候倾向于把构造的职责抽出来。

    2023-01-12归属地:北京
    3
  • 远天
    老师您好,这里的查询是只查询一个员工,如果分页查询多个员工,先查询出员工,再组装每个员工的技能和经验吗?还有一种极端情况,假如员工的技能有很多,成百上千个,也要一次性查出吗,是否有性能问题?

    作者回复: 好问题。 关于分页查询,参考迭代三的CQRS。 关于成百上千个技能,这时候建议不把员工和技能作为同一个聚合,而是把每个技能作为单独的聚合。

    2023-03-22归属地:浙江
    2
    2
  • Felix
    saveEmp(emp); emp.getSkills().forEach(s -> saveSkill(emp, s)); emp.getExperiences().forEach(e -> saveWorkExperience(emp, e)); emp.getEmpPosts().forEach(p -> saveEmpPost(emp, p)); 有个疑问,这几个save执行的sql都在一个数据库事务里的吗?没看见有显式声明,不清楚有没有

    作者回复: 在 application service 层有 @Transactional 注释,所以是在同一个事务。难道我在代码中漏了?

    2023-08-25归属地:广东
    1
  • 苏籍
    老师好,有几个困惑,想请教一下 1. 关于聚合中有多个实体,比如Emp 中有 skill 和 经验, 在实际场景中,业务场景上只需要更新 skill,在操作数据库时候,有必要Emp也更新吗(我看示例代码上 写的 保存完Emp 再去保存skill),我只更新skill 是否可行呢 或者我能够在领域层提供一个修改skill的领域服务。 2. 我看前面UpdateEmp方法执行之前,进行变更Emp和skill 属性的操作的 EmpUpdator 是放在应用层的,我理解是不是应该放在领域层呢,首先因为实体属性的变更 本身应该是某个业务规则触发的,在某个业务规则下才能修改某些属性以及联动修改skill 这种应该属于领域逻辑吧。 另外聚合本身后续可能会拆解成微服务,如果这种写到应用层,不利于后续拆分

    作者回复: 1 之所以总是更新Emp,是因为乐观锁(version字段)放在了 Emp 上。如果不想更新Emp,可以另外找一个地方放乐观锁。 2. 目前EmpUpdator用到了DTO,如果直接放到领域层,分层架构的依赖关系就错了,如果一定要放到领域层,那么可以在领域层再定义一层DTO。另外,EmpUpdator 并不是典型的领域逻辑,课程里应该说过,需要和领域专家聊的逻辑才是领域逻辑。

    2023-07-03归属地:浙江
    1
  • iam593
    继承于AuditableEntity的对象,在数据库中对应的表都有创建者、创建时间、修改者、修改时间等字段?从数据库层面看,这样会不会有点繁琐?

    作者回复: 是的,都有这些字段。至于要不要这么做,取决于你的权衡。

    2023-01-24归属地:湖南
    1
  • 南山
    1.能直接从数据库中查询值构造聚合对象,不做任何检查或者校验可行吗? 2.查询emp就加写锁,语句使用forUpdate PS:这种方式的修改聚合很有启发性

    作者回复: 第一点,关键是从数据库查到值以后,怎么构建领域对象。第二点,确实是悲观锁的可行做法。

    2023-01-12归属地:江苏
    1
  • Spoon
    select for update不是一种很好的悲观锁方式,当A事务执行时,其他事务都在等待,占用数据库链接,数据库链接是一个很宝贵的资源,而且等待对于用户来说也是一种很不好的体验,还可能会有死锁的风险

    作者回复: 有道理

    2024-02-22归属地:浙江
  • Johar
    1. 我们在重建聚合时,采用了编写聚合子类的方式绕过业务规则的校验,你还能想到其他方法吗? 直接在mybatis sql中将关联的实体查询出来,就不需要再单独实现了 2. 如果用悲观锁的话,应该怎样实现? 一般场景使用select *** for update,若是微服务要考虑使用分布式锁。 3.请教一下老师,目前在更新技能,工作经验,员工信息都在一起,要是更新场景频繁,是不是可以拆开单独更新,减小锁的范围?此外更新逻辑里面没有检验更新人的权限

    作者回复: 3. 可以拆开。目前实际上共享了Emp中的一把锁(version字段),如果拆开,需要用多把锁。

    2023-07-12归属地:重庆
收起评论
显示
设置
留言
19
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部