你好,我是钟敬。
[上节课]我们完成了添加员工 的功能,并且实现了关于技能 和工作经验 的不变规则 。今天我们重点要做两件事。第一,是继续完成修改员工 的功能。
另外,假如不考虑并发的情况,上节课的逻辑已经足以保证不变规则了。但是正如我们在[第14节课]讲聚合概念的时候讨论的,在并发环境下,这些规则仍然可能被破坏。所以今天的第二件事就是用事务来解决这一问题。
修改聚合对象 上节课,我们在员工 实体(Emp)里只实现了添加技能【addSkill()】的方法。如果要修改员工聚合,我们还要编写修改技能 和删除技能 的方法。对于工作经验 和岗位 也是一样的。
我们先看看在领域层实现这些逻辑的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package chapter17.unjuanable.domain.orgmng.emp;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); } public Emp updateExperience (LocalDate startDate, LocalDate endDate, String company, Long userId) { } public Emp deleteExperience (LocalDate startDate, LocalDate endDate) { } public Emp addEmpPost (String postCode, Long userId) { } public Emp deleteEmpPost (String postCode, Long useId) { } }
我们看一下 updateSkill() 方法。之前说过,我们把技能类型ID (SkillTypeId)当作技能 的局部标识,所以程序里先通过这个ID找到相应的技能 。
然后,我们会比较当前技能 和输入参数中的各个属性值。如果都相同,证明事实上不需要改变,所以什么都不需要做。只有当至少一个值不同时,才对技能 对象进行修改。修改属性值后,要用上节课写的 toUpdate() 方法来改变修改状态 (ChangingStatus)。
用于修改聚合的应用服务 修改完领域对象,我们来完成应用服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package chapter17.unjuanable.application.orgmng.empservice;@Service public class EmpService { private final EmpRepository empRepository; private final EmpAssembler assembler; private final EmpUpdator updator; @Transactional public EmpResponse updateEmp (Long empId, UpdateEmpRequest request , User user) { Emp emp = empRepository.findById(request.getTenantId(), empId) .orElseThrow(() -> new BusinessException ( "Emp id(" + empId + ") 不正确!" )); updator.update(emp, request, user); empRepository.save(emp); return assembler.toResponse(emp); } }
在应用服务里,我们增加了updateEmp()方法,用来修改员工 聚合。这个方法本身比较简单。首先从数据库中查出当前要修改的员工 (Emp), 然后调用updator来对聚合进行更新,最后调用仓库(empRepository)把聚合保存到数据库。
Updator是我们新写的一个类,在地位上和Assembler是类似的,都是应用服务的Helper。本来Updator的逻辑也可以写在Assembler里,但这样 Assembler就过于庞大了,所以基于关注点分离的原则,我们单独写一个 Updator来完成修改功能。
下面看看 Updator 的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package chapter17.unjuanable.application.orgmng.empservice;@Component public class EmpUpdator { public void update (Emp emp, UpdateEmpRequest request, User user) { emp.setNum(request.getNum()) .setIdNum(request.getIdNum()) .setDob(request.getDob()) .setGender(Gender.ofCode(request.getGenderCode())) .setLastUpdatedAt(LocalDateTime.now()) .setLastUpdatedBy(user.getId()) .toUpdate(); updateSkills(emp, request, user.getId()); updateExperiences(emp, request, user.getId()); } private void updateSkills (Emp emp, UpdateEmpRequest request , Long userId) { deleteAbsentSkills(emp, request); operatePresentSkills(emp, request, userId); } private void deleteAbsentSkills (Emp emp, UpdateEmpRequest request) { emp.getSkills().forEach(presentSkill -> { if (request.isSkillAbsent(presentSkill)) { emp.deleteSkill(presentSkill.getSkillTypeId()); } }); } private void operatePresentSkills (Emp emp , UpdateEmpRequest request, Long userId) { for (SkillDto skill : request.getSkills()) { Optional<Skill> skillMaybe = emp.getSkill( skill.getSkillTypeId()); if (skillMaybe.isPresent()) { emp.updateSkill(skill.getSkillTypeId() , SkillLevel.ofCode(skill.getLevelCode()) , skill.getDuration() , userId); } else { emp.addSkill(skill.getSkillTypeId() , SkillLevel.ofCode(skill.getLevelCode()) , skill.getDuration() , userId); } } } private void updateExperiences (Emp emp, UpdateEmpRequest request , Long userId) { } }
这里,程序逻辑的起点是update() 方法。它首先修改员工对象的值,并调用 toUpdate()方法设置**修改状态,**然后分别调用另外两个私有方法updateSkills()和updateExperiences()来修改技能和工作经验。我们假定按照业务需求,更改员工的岗位是单独的服务,所以这里没有修改岗位。
updateSkills() 方法用于修改技能,它包括两步。
**首先是调用 deleteAbsentSkills() 来删除不存在的技能。**逻辑是,比较请求参数(request)和当前员工聚合里的技能。
如果当前聚合有某个技能,但请求参数里没有,就认为用户希望删除这条技能,所以会调用 emp.deleteSkill() 方法来删除。这时并没有真的在内存里删除,只是修改了技能的修改状态,以便在持久化时在数据库里删除。对于技能是否存在,我们也是通过局部ID (skillTypeId)来判断的。
**第二步,调用operatePresentSkills() 方法来处理请求参数里存在的技能。**如果请求参数里的技能在当前聚合里存在,就更改,否则就增加。由于既可能是更改,也可能是增加,所以方法名用了 operate (操作)。
对于工作经验 的修改是类似的,你可以参考前面的讲解自己试试。
聚合的查询 接下来我们来完成持久层。在EmpService里,有两处调用empRepository和持久层交互。一处是调用empRepository.findById() 根据租户和员工ID查找要修改的员工,另一处是调用empRepository.save()来保存员工聚合。
咱们先看查询。由于聚合在逻辑上是一个整体,并且我们采用了在聚合内部用对象导航的策略,所以我们会把员工 实体和从属于它的技能 、工作经验 和岗位 都一次性取到内存。
乍一看,应该不太复杂,但这里会遇到一个问题。从数据库重建员工 (Emp)聚合的过程中,当我们调用Emp的一些方法赋值的时候,会触发业务规则的校验。比如说,调用addSkill()增加技能的时候,会触发“技能类型不允许重复”的校验。
那么重建聚合的时候,是否应该进行这种校验呢?
这取决于数据的“干净程度”。如果数据库中的数据比较“脏”,也就是说数据库里很多数据已经违反了业务规则,那么,可能在重建聚合时再校验一遍业务规则是可取的,这样可以找出脏数据错误。
不过多数情况下,数据库是比较干净的。这时候,如果每次从数据库取数据都要校验一遍,就会无谓地影响性能。
那么怎样绕过这些规则呢?有多种方法。我们的例子里采用这样的技巧:先把 Emp 中的属性都改成 protected 的,然后写一个 Emp 的子类,这个子类中的方法也可以设置 Emp 的值,但是不调用业务规则,这样就达到了绕过业务规则的目的。
下面是这个子类的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package chapter17.unjuanable.adapter.driven.persistence.orgmng;public class RebuiltEmp extends Emp { RebuiltEmp(Long tenantId, Long id, LocalDateTime create_at, long created_by) { super (tenantId, id, create_at, created_by); this .changingStatus = ChangingStatus.UNCHANGED; } RebuiltEmp resetOrgId (Long orgId) { this .orgId = orgId; return this ; } RebuiltEmp resetNum (String num) { this .num = num; return this ; } RebuiltEmp resetIdNum (String idNum) { this .idNum = idNum; return this ; } RebuiltEmp resetName (String name) { this .name = name; return this ; } RebuiltEmp resetGender (Gender gender) { this .gender = gender; return this ; } RebuiltEmp resetDob (LocalDate dob) { this .dob = dob; return this ; } RebuiltEmp resetStatus (EmpStatus status) { this .status = status; return this ; } public RebuiltEmp reAddSkill (Long id, Long skillTypeId, SkillLevel level, int duration, Long createdBy) { RebuiltSkill newSkill = new RebuiltSkill (tenantId, id, skillTypeId, createdBy) .resetLevel(level) .resetDuration(duration); skills.add(newSkill); return this ; } public RebuiltEmp reAddExperience (LocalDate startDate, LocalDate endDate, String company, Long userId) { } public RebuiltEmp reAddEmpPost (String postCode, Long userId) { } }
首先,这个子类和员工仓库 的实现(EmpRepositoryJdbc)放在同一个包,类中的方法都是包级私有的,也就是说,只有员工仓库 的实现类可以访问,从而避免了这个包外部的其他类绕过业务规则。
这个类的名字是RebuiltEmp,也就是“重建的”员工 。对应于父类(Emp)里的 setXxx() 方法,这里我们setter用resetXxx() 来命名,以示区别。类似地,我们也用reAddXxx()来增加技能 、工作经验 和岗位 。另外,这些方法都返回 RebuildEmp 对象本身,以便对这个对象进行链式操作。
有了这个子类,我们就可以实现仓库了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 package chapter17.unjuanable.adapter.driven.persistence.orgmng;@Repository public class EmpRepositoryJdbc implements EmpRepository { @Override public Optional<Emp> findById (Long tenantId, Long id) { Optional<RebuiltEmp> empMaybe = retrieveEmp(tenantId, id); if (empMaybe.isPresent()) { RebuiltEmp emp = empMaybe.get(); retrieveSkills(emp); retrieveExperiences(emp); retrievePosts(emp); return Optional.of(emp); } else { return Optional.empty(); } } private Optional<RebuiltEmp> retrieveEmp (Long tenantId, Long id) { String sql = " select org_id, num, id_num, name " + " , gender_code, dob, status_code " + " from emp " + " where id = ? and tenant_id = ? " ; RebuiltEmp emp = jdbc.queryForObject(sql, (rs, rowNum) -> { RebuiltEmp newEmp = new RebuiltEmp (tenantId , id , rs.getTimestamp("create_at" ).toLocalDateTime() , rs.getLong("created_by" )); newEmp.resetOrgId(rs.getLong("org_id" )) .resetNum(rs.getString("num" )) .resetIdNum(rs.getString("id_num" )) .resetName(rs.getString("name" )) .resetGender(Gender.ofCode( rs.getString("gender_code" ))) .resetDob(rs.getDate("dob" ).toLocalDate()) .resetStatus(EmpStatus.ofCode( rs.getString("status_code" ))); return newEmp; }, id, tenantId); return Optional.ofNullable(emp); } private void retrieveSkills (RebuiltEmp emp) { String sql = " select id, tenant_id, skill_type_id, level, duration " + " from skill " + " where tenant_id = ? and emp_id = ? " ; List<Map<String, Object>> skills = jdbc.queryForList( sql, emp.getTenantId(), emp.getId()); skills.forEach(skill -> emp.reAddSkill( (Long) skill.get("id" ) , (Long) skill.get("skill_type_id" ) , SkillLevel.ofCode((String) skill.get("level_code" )) , (Integer) skill.get("duration" ) , (Long) skill.get("created_by" ) }); } private void retrieveExperiences (RebuiltEmp emp) { } private void retrievePosts (RebuiltEmp emp) { } }
FindById() 方法首先会从数据库重建 Emp 对象本身,然后分别重建技能 、工作经验 和岗位 。与数据库直接打交道的方法,用 retrieveXxx() 来命名,以便和更上层的 FindByXxx() 相区别。
对修改的聚合进行持久化 完成了查询功能,我们来看怎样把修改后的聚合存入数据库。无论新增还是修改聚合,我们都可以用同一个empRepository.save()方法 ,所以我们要修改之前课程中的这个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 package chapter17.unjuanable.adapter.driven.persistence.orgmng;@Repository public class EmpRepositoryJdbc implements EmpRepository { final JdbcTemplate jdbc; final SimpleJdbcInsert empInsert; final SimpleJdbcInsert skillInsert; final SimpleJdbcInsert WorkExperienceInsert; final SimpleJdbcInsert empPostInsert; @Autowired public EmpRepositoryJdbc (JdbcTemplate jdbc) { this .jdbc = jdbc; this .empInsert = new SimpleJdbcInsert (jdbc) .withTableName("emp" ) .usingGeneratedKeyColumns("id" ); } @Override public void save (Emp emp) { saveEmp(emp); emp.getSkills().forEach(s -> saveSkill(emp, s)); emp.getExperiences().forEach(e -> saveWorkExperience(emp, e)); emp.getEmpPosts().forEach(p -> saveEmpPost(emp, p)); } private void saveEmp (Emp emp) { switch (emp.getChangingStatus()) { case NEW: insertEmpRecord(emp); break ; case UPDATED: updateEmpRecord(emp); break ; } } private void insertEmpRecord (Emp emp) { Map<String, Object> parms = Map.of( "tenant_id" , emp.getTenantId() , "org_id" , emp.getOrgId() , "num" , emp.getNum() , "id_num" , emp.getIdNum() , "name" , emp.getName() , "gender" , emp.getGender().code() , "dob" , emp.getDob() , "status" , emp.getStatus().code() , "created_at" , emp.getCreatedAt() , "created_by" , emp.getCreatedBy() ); Number createdId = empInsert.executeAndReturnKey(parms); forceSet(emp, "id" , createdId.longValue()); } private void updateEmpRecord (Emp emp) { String sql = "update emp " + " set org_id = ?" + ", num = ?" + ", id_num =? " + ", name = ?" + ", gender =?" + ", dob = ?" + ", status =?" + ", last_updated_at =?" + ", last_updated_by =? " + " where tenant_id = ? and id = ? " ; this .jdbc.update(sql , emp.getOrgId() , emp.getNum() , emp.getIdNum() , emp.getName() , emp.getGender().code() , emp.getDob() , emp.getStatus() , emp.getLastUpdatedAt() , emp.getLastUpdatedBy() , emp.getTenantId() , emp.getId()); } private void saveSkill (Emp emp, Skill skill) { switch (skill.getChangingStatus()) { case NEW: insertSkillRecord(skill, emp.getId()); break ; case UPDATED: updateSkillRecord(skill); break ; case DELETED: deleteSkillRecord(skill); break ; } } private void insertSkillRecord (Skill skill, Long empId) { Map<String, Object> parms = Map.of( "emp_id" , empId, "tenant_id" , skill.getTenantId(), "skill_type_id" , skill.getSkillTypeId(), "level_code" , skill.getLevel().code(), "duration" , skill.getDuration(), "created_at" , skill.getCreatedAt(), "created_by" , skill.getCreatedBy() ); Number createdId = skillInsert.executeAndReturnKey(parms); forceSet(skill, "id" , createdId.longValue()); } private void updateSkillRecord (Skill skill) { String sql = "update skill " + " set level_code = ?" + ", duration = ?" + ", last_updated_at = ?" + ", last_updated_by = ?" + " where tenant_id = ? and id = ? " ; this .jdbc.update(sql , skill.getSkillTypeId() , skill.getDuration() , skill.getLastUpdatedAt() , skill.getLastUpdatedBy() , skill.getTenantId() , skill.getId()); } private void deleteSkillRecord (Skill skill) { this .jdbc.update("delete from skll where tenant_id = ? " + " and id = ?" , skill.getTenantId() , skill.getId()); } private void saveWorkExperience (Emp emp, WorkExperience e) { } private void saveEmpPostRecord (Emp emp, EmpPost p) { } }
save()方法先调用saveEmp()方法,根据员工 对象的修改状态(changingStatus),来插入或更新emp表,然后用同样的逻辑循环处理技能 、工作经验 和岗位 。
我们假定将来会写专门的removeEmp()方法删除整个聚合,所以目前的 saveEmp()中没有处理删除的情况。另外,对于直接操作数据库的类,我们用 insertXxxRecord()的方式的命名,与更上一层的saveXxx()方法相区别。
用事务保证固定规则 完成了修改聚合的基本功能后,我们来考虑避免并发情况下破坏不变规则的问题。我们在第14节课已经讲过,需要把对聚合的修改封装到一个事务中去,这样,一个人修改完以后,另一个人才能修改,从而避免并发修改的问题。那么具体怎么做呢?
首先,我们要考虑一个问题,仅仅靠数据库事务,是无法完成这一任务的,需要自己编写一些代码来完成。这种比数据库事务“高一级”的事务,我们可以称为“业务事务”(Business Transaction)。业务事务一般要使用乐观锁或者悲观锁的机制。
悲观锁指的是,只要一个人开始修改操作,就为数据加锁,其他人根本不可能同步修改。乐观锁指的是,两个人可以同时操作,但最后保存到数据库的时候,先保存的那个人成功,后保存的那个人失败,只能重新进行操作。
我们这里选择乐观锁。对于聚合的情况而言,实际上是通过锁聚合根,来把整个聚合锁住。我们一步一步地看一看做法。
第一步,要在聚合根的代码和数据表里增加一个版本 (version)字段,类型可以是长整型。由于多数聚合都要考虑加锁,所以我们为聚合根写一个父类,这个类又是AuditableEntity的子类。后面是具体代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package chapter17.unjuanable.common.framework.domain; import java.time.LocalDateTime; public class AggregateRoot extends AuditableEntity { protected Long version; public AggregateRoot(LocalDateTime createdAt, Long createdBy) { super(createdAt, createdBy); } public Long getVersion() { return version; } }
Emp原来继承的是AuditableEntity, 现在改为继承AggregateRoot,其他部分不需要修改。这样,Emp就有了version属性。
1 2 3 public class Emp extends AggregateRoot { //... }
第二步,修改EmpRepository中的 findById() 方法,在取数据的时候,把Emp 的verion值也取出来。逻辑比较简单,这里就不列代码了。
第三步,是在update Emp表的时候,修改SQL语句,这一步是最关键的,我们先看代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package chapter17.unjuanable.adapter.driven.persistence.orgmng; // imports ... @Repository public class EmpRepositoryJdbc implements EmpRepository { // 声明 JdbcTemplate, SimpleJdbcInsert empInsert ... // 构造器,其他方法不变 ... @Override public boolean save(Emp emp) { if (saveEmp(emp)) { emp.getSkills().forEach(s -> saveSkill(emp, s)); emp.getExperiences().forEach(e -> saveWorkExperience(emp, e)); emp.getEmpPosts().forEach(p -> saveEmpPost(emp, p)); return true; } else { return false; } } private boolean saveEmp(Emp emp) { switch (emp.getChangingStatus()) { case NEW: insertEmpRecord(emp); break; case UPDATED: if(!updateEmpRecord(emp)) { return false; } break; } return true; } private void insertEmpRecord(Emp emp) { // 代码不变 ... } // 注意:SQL语句中增加了两处关于 version 的修改 private boolean updateEmpRecord(Emp emp) { String sql = "update emp " + " set version = version + 1 " + ", org_id = ?" + ", num = ?" + ", id_num =? " + ", name = ?" + ", gender =?" + ", dob = ?" + ", status =?" + ", last_updated_at =?" + ", last_updated_by =? " + " where tenant_id = ? and id = ? and version = ?"; int affected = this.jdbc.update(sql , emp.getOrgId() , emp.getNum() , emp.getIdNum() , emp.getName() , emp.getGender().code() , emp.getDob() , emp.getStatus() , emp.getLastUpdatedAt() , emp.getLastUpdatedBy() , emp.getTenantId() , emp.getId() , emp.getVersion()); return affected == 1 ? true : false; } // 其他方法不变 ... }
这里重点是updateEmpRecord() 方法里SQL语句的变化。SQL语句里增加了两处关于version的修改,其他部分不变。
1 2 3 update emp set version = version + 1 ... where version = <当前Emp中的version值>
也就是说,根据当前Emp里的version值,找到记录,然后把version值加 1 。
我们想象一下,两个人几乎同时修改员工 ,但最后 update 语句的执行总有一个先后。
先update的人是可以根据原来的version值取到记录的,因为这时 version 值还没变。而后update的人,由于数据库里的version值已经被刚才的人加1了,所以无法通过原来的version找到记录,会导致更新失败,也就不会破坏业务规则。这就是乐观锁的诀窍。
我们再看回updateEmpRecord()方法,它的返回值由原来的void改成了 boolean,表示修改是否成功。update语句执行后,会返回被update的记录数量。如果返回为1,证明修改成功,则这个方法返回true;如果返回0 ,说明修改失败,也就是已经被别人抢先修改了,这时返回false。
调用updateEmpRecord()的saveEmp()和再上层的save()的返回值也都改成了 boolean。updateEmpRecord()的成功状态经由saveEmp()返回给save() 。save()方法只有在保存员工 成功的时候才进一步保存技能 、工作经验 和岗位 ,否则,不会继续操作,而是返回false。
而save()方法又是由应用服务EmpService()调用的。EmpService()的代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package chapter17.unjuanable.application.orgmng.empservice; // imports ... @Service public class EmpService { // 依赖注入、构造器、其他方法 ... @Transactional public EmpResponse updateEmp(Long empId, UpdateEmpRequest request , User user) { Emp emp = empRepository.findById(request.getTenantId(), empId) .orElseThrow(() -> new BusinessException( "Emp id(" + empId + ") 不正确!")); updator.update(emp, request, user); // 这里增加了判断 if(!empRepository.save(emp)) { throw new BusinessException( "这个员工已经被其他人同时修改了,请重新修改!"); }; return assembler.toResponse(emp); } }
EmpService 的updateEmp() 方法会判断保存是否成功,如果不成功,则可推断出是其他人抢先修改了,于是抛出异常,提示当前用户重新修改。
单实体聚合 现在,我们已经完成了聚合代码的编写。最后再讨论一个问题:有些实体,既不是聚合根,也不从属于任何聚合,例如上个迭代讲过的组织(Org)实体,对于这些实体该怎么处理呢?
我们建议,把这种“游离”的实体看做一种“退化”的聚合,也就是说,它们也是聚合,只不过只有聚合根,没有“儿子”,可以称为“单实体聚合”。
比如说,组织 实体就构成了一个单实体聚合,它本身就是聚合根,在代码层面可以和普通聚合一样处理。也就是说,这些实体也在自己单独的包内,这个包里面通常包括仓库的接口,有时还包括工厂和领域服务。事实上,上个迭代对组织 的处理,就是这么做的。
但是在领域模型图里,如果把每个单实体聚合外面都套一个“包”的话,模型图就显得太凌乱了,所以在模型图上就没有必要为单独的实体加上包了。这时,模型和代码稍微有些不一致,算是一种妥协吧。
总结 好,这节课的主要内容就讲完了,下面我们来总结一下。今天主要解决的是聚合的修改,以及在并发环境下保护聚合不变规则的问题。
对于聚合的修改,有以下要点。
第一,在修改之前,要把聚合从数据库里取出来。为了这个目的,仓库要把聚合的数据整体装入内存,并重建聚合。这里我们还用了一个技巧,在仓库包里建立了聚合根的一个子类,从而绕过校验规则,避免不必要的性能损耗。
第二,要在领域层的聚合根里增加对技能、工作经验和岗位的更改和删除代码,并为这些对象设置合适的修改状态,从而把非聚合根对象的修改逻辑封装起来。
第三,在应用层把当前聚合与请求参数进行对比,确定对聚合里的各个对象应该进行增、删、改,还是保持不变。然后,调用聚合根来进行相应的操作。
最后,为了把聚合存入数据库,仓库要遍历聚合中的各个对象,根据对象的更改状态进行合适的数据库操作。
完成了聚合的修改以后,我们展示了怎样用乐观锁保护聚合的事务边界,避免并发操作对不变规则的破坏。此外,我们还讨论了单实体聚合的处理。
在介绍聚合概念的那节课里,我们讲了聚合的两大特征:一个是概念上的整体性 ;另一个是维护不变规则的要求 。在这三节课,你应该能体会到怎样从代码层面实现这些聚合的特征了吧。
还有一点要注意,尽管我们目前选择的是偏过程式的编码风格,但是也会尽量实现封装、继承等面向对象编程的特征,这一点也是要着重体会的。
思考题 1.我们在重建聚合时,采用了编写聚合子类的方式绕过业务规则的校验,你还能想到其他方法吗?
2.如果用悲观锁的话,应该怎样实现?
好,今天的课程结束了,有什么问题欢迎在评论区留言,下节课,我们开始讲解值对象和其他一些建模技巧。