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的处理类似...
}
}
我们看一下 updateSkill() 方法。之前说过,我们把 技能类型ID(SkillTypeId)当作 技能 的局部标识,所以程序里先通过这个ID找到相应的 技能。
然后,我们会比较当前 技能 和输入参数中的各个属性值。如果都相同,证明事实上不需要改变,所以什么都不需要做。只有当至少一个值不同时,才对 技能 对象进行修改。修改属性值后,要用上节课写的 toUpdate() 方法来改变 修改状态(ChangingStatus)。
用于修改聚合的应用服务
修改完领域对象,我们来完成应用服务。
package chapter17.unjuanable.application.orgmng.empservice;
// imports ...
@Service
public class EmpService {
private final EmpRepository empRepository;
private final EmpAssembler assembler;
private final EmpUpdator updator; //用于修改Emp聚合
// 构造器、其他方法...
@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 的代码。
package chapter17.unjuanable.application.orgmng.empservice;
// imports ...
@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) {
// 与updateSkilL()类似...
}
}
这里,程序逻辑的起点是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 的值,但是不调用业务规则,这样就达到了绕过业务规则的目的。
下面是这个子类的代码。
//这个类位于适配器包
package chapter17.unjuanable.adapter.driven.persistence.orgmng;
//imports...
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;
}
//包级权限,并且用 resetXxx 命名
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;
}
// 用 reAddXxx 命名
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 对象本身,以便对这个对象进行链式操作。
有了这个子类,我们就可以实现仓库了。
package chapter17.unjuanable.adapter.driven.persistence.orgmng;
// imports...
@Repository
public class EmpRepositoryJdbc implements EmpRepository {
//声明 JdbcTemplate 和各个 SimpleJdbcInsert ...
// 构造器、其他方法 ...
@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) {
//与retrieveSkill 类似 ...
}
private void retrievePosts(RebuiltEmp emp) {
//与retrieveSkill 类似 ...
}
}
FindById() 方法首先会从数据库重建 Emp 对象本身,然后分别重建 技能、 工作经验 和 岗位。与数据库直接打交道的方法,用 retrieveXxx() 来命名,以便和更上层的 FindByXxx() 相区别。
对修改的聚合进行持久化
完成了查询功能,我们来看怎样把修改后的聚合存入数据库。无论新增还是修改聚合,我们都可以用同一个empRepository.save()方法 ,所以我们要修改之前课程中的这个方法。
package chapter17.unjuanable.adapter.driven.persistence.orgmng;
// imports ...
@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");
//初始化其他 SimpleJdbcInsert ...
}
@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) {
// 与 saveSkill( ) 类似...
}
private void saveEmpPostRecord(Emp emp, EmpPost p) {
// 与 saveSkill( ) 类似...
}
}
save()方法先调用saveEmp()方法,根据 员工 对象的修改状态(changingStatus),来插入或更新emp表,然后用同样的逻辑循环处理 技能、 工作经验 和 岗位。
我们假定将来会写专门的removeEmp()方法删除整个聚合,所以目前的 saveEmp()中没有处理删除的情况。另外,对于直接操作数据库的类,我们用 insertXxxRecord()的方式的命名,与更上一层的saveXxx()方法相区别。
用事务保证固定规则
完成了修改聚合的基本功能后,我们来考虑避免并发情况下破坏不变规则的问题。我们在第14节课已经讲过,需要把对聚合的修改封装到一个事务中去,这样,一个人修改完以后,另一个人才能修改,从而避免并发修改的问题。那么具体怎么做呢?
首先,我们要考虑一个问题,仅仅靠数据库事务,是无法完成这一任务的,需要自己编写一些代码来完成。这种比数据库事务“高一级”的事务,我们可以称为“业务事务”(Business Transaction)。业务事务一般要使用乐观锁或者悲观锁的机制。
悲观锁指的是,只要一个人开始修改操作,就为数据加锁,其他人根本不可能同步修改。乐观锁指的是,两个人可以同时操作,但最后保存到数据库的时候,先保存的那个人成功,后保存的那个人失败,只能重新进行操作。
我们这里选择乐观锁。对于聚合的情况而言,实际上是通过锁聚合根,来把整个聚合锁住。我们一步一步地看一看做法。
第一步,要在聚合根的代码和数据表里增加一个 版本(version)字段,类型可以是长整型。由于多数聚合都要考虑加锁,所以我们为聚合根写一个父类,这个类又是AuditableEntity的子类。后面是具体代码。
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属性。
第二步,修改EmpRepository中的 findById() 方法,在取数据的时候,把Emp 的verion值也取出来。逻辑比较简单,这里就不列代码了。
第三步,是在update Emp表的时候,修改SQL语句,这一步是最关键的,我们先看代码。
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的修改,其他部分不变。
也就是说,根据当前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()的代码如下。
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.如果用悲观锁的话,应该怎样实现?
好,今天的课程结束了,有什么问题欢迎在评论区留言,下节课,我们开始讲解值对象和其他一些建模技巧。