hello,everyone。又到了周末了,没有出去玩,继续肝。从评论与粉丝私下的联系来看,大家对于DDD架构的热情都比较高。但是因为抽象化的概念较多,因此理解上就很困难。
昨天媳妇儿生病了在医院,她挂点滴的时候,我也没闲下来,抓紧时间做出了DDD的第一版demo,就冲这点,
这个项目我会持续维护,针对读者提出的issue与相关功能点的增加,我都会持续的补充。
DDD系列博客
本文将给大家介绍的同样是DDD中的一个比较好理解与落地的知识点-仓储。
本系列为MVC框架迁移至DDD,考虑到国内各大公司内还是以mybatis作为主流进行业务开发。因此,demo中的迁移与本文的相关实例均以mybatis进行演示。至于应用仓储选型是mybatis还是jpa,文中会进行分析,请各位仔细阅读本文。 我的第一本掘金小册《深入浅出DDD》已经在掘金上线,欢迎大家试读~
DDD的微信群我也已经建好了,由于文章内不能放二维码,大家可以加我微信**baiyan_lou
**,备注DDD交流,我拉你进群,欢迎交流共同进步。
原著《领域驱动设计:软件核心复杂性应对之道》 中对仓储的有关解释:
为每种需要全局访问的对象类型创建一个对象,这个对象就相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体标准来挑选对象的方法,并返回属性值满足查询标准的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的Aggregate提供Repository。让客户始终聚焦于型,而将所有对象存储和访问操作交给Repository来完成。
上文通俗的讲,当领域模型一旦建立之后,你不应该关心领域模型的存取方式。仓储就相当于一个功能强大的仓库,你告诉他唯一标识:例如订单id
,它就能把所有你想要数据按照设置的领域模型一口气组装返回给你。存储时也一样,你把整块订单数据给他,至于它怎么拆分,放到什么存储介质【DB,Redis,ES等等】
,这都不是你业务应该关心的事。你完全信任它能帮助你完成数据管理工作。
先说贫血模型
的缺点:
有小伙伴之前提出过不知道贫血模型的定义,这里做一下解释。贫血模型:PO,DTO,VO这种常见的业务POJO,都是数据java里面的数据载体,内部没有任何的业务逻辑。所有业务逻辑都被定义在各种service里面,service做了各种模型之间的各种逻辑处理,臃肿且逻辑不清晰。充血模型:建立领域模型形成聚合根,在聚合根即表示业务,在聚合内部定义当前领域内的业务处理方法与逻辑。将散落的逻辑进行收紧。
虽然贫血模型有很大的缺陷,但是在我们日常的代码中,我见过的99%的代码都是基于贫血模型,为什么呢?
写业务逻辑
转变为了写数据库逻辑
,也就是我们经常说的在写CRUD代码
。脚本
或胶水代码
,也就是流程式代码
。脚本代码的好处就是比较容易理解,但长久来看缺乏健壮性,维护成本会越来越高。但是可能最核心的原因在于,实际上我们在日常开发中,混淆了两个概念:
所以,解决这个问题的根本方案,就是要在代码里区分Data Model和Domain Model,具体的规范会在后文详细描述。在真实代码结构中,Data Model和 Domain Model实际上会分别在不同的层里,Data Model只存在于数据层,而Domain Model在领域层,而链接了这两层的关键对象,就是Repository。
能够隔离我们的软件(业务逻辑)和固件/硬件(DAO、DB),让我们的软件变得更加健壮,而这个就是Repository的核心价值。
DTO Assembler: 在Application层 【应用服务层】 ,Entity
到DTO
的转化器有一个标准的名称叫DTO Assembler
【汇编器】 。
DTO Assembler的核心作用就是将1个或多个相关联的Entity转化为1个或多个DTO。
Data Converter: 在Infrastructure层 【基础设施层】 ,Entity
到DO
的转化器没有一个标准名称,但是为了区分Data Mapper,我们叫这种转化器Data Converter
。这里要注意Data Mapper通常情况下指的是DAO,比如Mybatis的Mapper。
首先聚合
和仓储
之间是一一对应
的关系。仓储
只是一种持久化的手段,不应该包含任何业务操作。
仓储内部仅能依赖mapper,es,redis这种存储介质包装框架的工具类。save动作,仅对传入的聚合根进行解析放入不同的存储介质,你想放入redis,数据库还是es,由converter来完成聚合根的转换解析。同样,从不同的存储介质中查询得到的数据,交给converter来组装。
回顾一下这张图,可以发现增删改数据模型走了DDD模型。而查询则从应用服务层直接穿透到了基础设施层。
这就是CQRS模型,从数据角度来看,增删改数据非幂等操作,任何一个动作都能对数据进行改动,称为危险行为
。而查询,不会因为你查询次数的改变,而去修改到数据,称为安全行为
。而往往功能迭代过程中,数据修改的逻辑还是复杂的,因此建模也都是针对于增删改数据而言的。
那么查询数据有什么原则吗?
目前主流使用的orm框架就是mybatis与jpa。国内使用mybatis多,国外使用jpa多。两者框架上的比较本文不做展开,不清楚两个框架实现差异的,可以自行百度。
那么我们如果做DDD建模的话到底选择哪一种orm框架更好呢?
mybatis是一个半自动框架(当然现在有mybatis-plus的存在,mybatis也可以说是跻身到全自动框架里面了)
,国内使用它作为orm框架是主流。为什么它是主流,因为它足够简单,设计完表结构之后,映射好字段就可以进行开发了,业务逻辑可以用胶水
一个个粘起来。而且在架构支持上,mybatis不支持实体嵌套实体,这个在领域模型建模结束后的应用上就优于mybatis。
当然我们今天讨论的是架构,任何时候,技术选型不是决定我们技术架构的关键性因素。
jpa天生就具备做DDD的优势。但是这并不意味着mybatis就做不了DDD了,我们完全可以将领域模型的定义与orm框架的应用分离,单独定义converter去实现领域模型与数据模型之间的转换,demo中我也是这么给大家演示的。
当然,如果是新系统或者迁移时间足够多,我还是推荐使用JPA的,红红火火恍恍惚惚~
需求描述,用户领域有四个业务场景
核心实现演示,不贴全部代码,完整demo可从文章开头的github仓库获取
/**
* 用户聚合根
*
* @author baiyan
*/
@Getter
@NoArgsConstructor
public class User extends BaseUuidEntity implements AggregateRoot {
/**
* 用户名
*/
private String userName;
/**
* 用户真实名称
*/
private String realName;
/**
* 用户手机号
*/
private String phone;
/**
* 用户密码
*/
private String password;
/**
* 用户地址
*/
private Address address;
/**
* 用户单位
*/
private Unit unit;
/**
* 角色
*/
private List<Role> roles;
/**
* 新建用户
*
* @param command 新建用户指令
*/
public User(CreateUserCommand command){
this.userName = command.getUserName();
this.realName = command.getRealName();
this.phone = command.getPhone();
this.password = command.getPassword();
this.setAddress(command.getProvince(),command.getCity(),command.getCounty());
this.relativeRoleByRoleId(command.getRoles());
}
/**
* 修改用户
*
* @param command 修改用户指令
*/
public User(UpdateUserCommand command){
this.setId(command.getUserId());
this.userName = command.getUserName();
this.realName = command.getRealName();
this.phone = command.getPhone();
this.setAddress(command.getProvince(),command.getCity(),command.getCounty());
this.relativeRoleByRoleId(command.getRoles());
}
/**
* 组装聚合
*
* @param userPO
* @param roles
*/
public User(UserPO userPO, List<RolePO> roles){
this.setId(userPO.getId());
this.setDeleted(userPO.getDeleted());
this.setGmtCreate(userPO.getGmtCreate());
this.setGmtModified(userPO.getGmtModified());
this.userName = userPO.getUserName();
this.realName = userPO.getRealName();
this.phone = userPO.getPhone();
this.password = userPO.getPassword();
this.setAddress(userPO.getProvince(),userPO.getCity(),userPO.getCounty());
this.relativeRoleByRolePO(roles);
this.setUnit(userPO.getUnitId(),userPO.getUnitName());
}
/**
* 根据角色id设置角色信息
*
* @param roleIds 角色id
*/
public void relativeRoleByRoleId(List<Long> roleIds){
this.roles = roleIds.stream()
.map(roleId->new Role(roleId,null,null))
.collect(Collectors.toList());
}
/**
* 设置角色信息
*
* @param roles
*/
public void relativeRoleByRolePO(List<RolePO> roles){
if(CollUtil.isEmpty(roles)){
return;
}
this.roles = roles.stream()
.map(e->new Role(e.getId(),e.getCode(),e.getName()))
.collect(Collectors.toList());
}
/**
* 设置用户地址信息
*
* @param province 省
* @param city 市
* @param county 区
*/
public void setAddress(String province,String city,String county){
this.address = new Address(province,city,county);
}
/**
* 设置用户单位信息
*
* @param unitId
* @param unitName
*/
public void setUnit(Long unitId,String unitName){
this.unit = new Unit(unitId,unitName);
}
}
/**
*
* 用户领域仓储
*
* @author baiyan
*/
@Repository
public class UserRepositoryImpl implements UserRepository {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Override
public void delete(Long id){
userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,id));
userMapper.deleteById(id);
}
@Override
public User byId(Long id){
UserPO user = userMapper.selectById(id);
if(Objects.isNull(user)){
return null;
}
List<UserRolePO> userRoles = userRoleMapper.selectList(Wrappers.<UserRolePO>lambdaQuery()
.eq(UserRolePO::getUserId, id).select(UserRolePO::getRoleId));
List<Long> roleIds = CollUtil.isEmpty(userRoles) ? new ArrayList<>() : userRoles.stream()
.map(UserRolePO::getRoleId)
.collect(Collectors.toList());
List<RolePO> roles = roleMapper.selectBatchIds(roleIds);
return UserConverter.deserialize(user,roles);
}
@Override
public User save(User user){
UserPO userPo = UserConverter.serializeUser(user);
if(Objects.isNull(user.getId())){
userMapper.insert(userPo);
user.setId(userPo.getId());
}else {
userMapper.updateById(userPo);
userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,user.getId()));
}
List<UserRolePO> userRolePos = UserConverter.serializeRole(user);
userRolePos.forEach(userRoleMapper::insert);
return this.byId(user.getId());
}
}
/**
*
* 用户信息查询仓储
*
* @author baiyan
*/
@Repository
public class UserQueryRepositoryImpl implements UserQueryRepository {
@Autowired
private UserMapper userMapper;
@Override
public Page<UserPageDTO> userPage(KeywordQuery query){
Page<UserPO> userPos = userMapper.userPage(query);
return UserConverter.serializeUserPage(userPos);
}
}
以OrderDO与OrderDAO的业务场景为例