JPA DDD 实践


充血、涨血模型的讨论不在此篇文章的讨论,DDD具体如何拆分业务也不在此讨论范围,我只是说一下我用JPA是如何进行DDD编码的。
首先需要明确一点,你到底需不需要PO(Persistent Object)?
我这里的实践是将PO和Domain合并了,拆分PO有好处有坏处,好处就是职责清晰,相对编码逻辑清楚点,坏处就是多了一个莫名其妙的PO,你会感到有一点点的不爽。
其次,你需不需要Service层?我建议你最好加个Service层,如果连Service层都没有,你这玩意儿跨度有点大,我个人不是很建议。
我个人的架构的习惯就是:接入层(例如Controller)负责参数的接受、校验以及返回结果,Service层负责事务,Domain层负责细节逻辑处理,Repository层负责持久化的抽象实现。
废话不多说,直接看领域模型,我这里用账号举例。
@Slf4j
@Entity
@Table(name = "uaa_account", uniqueConstraints = {
@UniqueConstraint(name = "UK_username", columnNames = {"username"})
@SQLDelete(sql = "UPDATE uaa_account SET deleted = true WHERE id=?")
@Where(clause = "deleted=false")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {
/* 状态 */
@GeneratedValue(generator = "generator")
@GenericGenerator(name = "generator",
strategy = "com.esteel.uaa.manager.IdGenerator")
@Getter
private Long id;
@Getter
private String username;
@Getter
private String password;
@Getter
private String name;
@Getter
private boolean locked = false;
@Getter
private boolean enabled = true;
@Getter
private boolean expired = false;
@ManyToMany
@JoinTable(name = "uaa_rel_account_role",
joinColumns = @JoinColumn(name = "account_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private final Set<Role> roles = new HashSet<>();
public Set<Role> getRoles() {
return Collections.unmodifiableSet(roles);
public void lock() {
this.locked = true;
public void unlock() {
this.locked = false;
public void enable() {
this.enabled = true;
public void disable() {
this.enabled = false;
public void expire() {
this.expired = true;
* 为账号添加角色,该角色必须已经被持久化,否则会抛出 {@link AccountAddRoleException}
* @param role 角色
public void addRole(Role role) {
if (role.getId() == null) {
throw new AccountAddRoleException();
this.roles.add(role);
* 移除一个角色
* @param role 角色
public void removeRole(Role role) {
this.roles.remove(role);
* 修改用户名,如果用户名已经存在,将会抛出 {@link AccountUsernameExistsException}
* 如果用户名不符合要求,则会抛出{@link AccountUsernameInvalidException}
* @param username 用户名
public void changeUsername(String username) {
if (repository.existsByUsername(username)) {
throw new AccountUsernameExistsException();
if (!StringUtils.hasText(username)) {
throw new AccountUsernameInvalidException();
this.username = username;
* 修改密码
* @param password 新密码
public void resetPassword(String password) {
this.password = passwordEncoder.encode(password);
* 修改姓名
* @param name 姓名
public void changeName(String name) {
this.name = name;
public void save() {
repository.save(this);
public void destroy() {
repository.delete(this);
// ===============构造方法区================
@Transient
private PasswordEncoder passwordEncoder;
@Transient
private AccountRepository repository;
protected Account(String username, String password) {
this.init();
this.username = username;
this.password = passwordEncoder.encode(password);
@PostLoad
private void init() {
passwordEncoder = ApplicationContextProvider.getBean(PasswordEncoder.class);
repository = ApplicationContextProvider.getBean(AccountRepository.class);
log.info(repository + "");
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Objects.equals(id, account.id);
@Override
public int hashCode() {
return Objects.hash(id);
首先,我们一段代码一段代码去看。
@Slf4j
@Entity
@Table(name = "uaa_account", uniqueConstraints = {
@UniqueConstraint(name = "UK_username", columnNames = {"username"})
@SQLDelete(sql = "UPDATE uaa_account SET deleted = true WHERE id=?")
@Where(clause = "deleted=false")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
声明式的编程,JPA避免不了,这里简单做了几件事情:
- 声明这是个Entity
- 这个Entity指向的表
- 逻辑删除
- 一个受保护的构造方法给JPA反序列化使用
这里其实是有问题的,受保护的构造方法其实是对JPA的妥协,理论上不应该有,按照代码的设计来讲,应该是一个私有无参的构造方法来保证必填参数的逻辑校验。
接着,我们看第二段代码:
@Id
@GeneratedValue(generator = "generator")
@GenericGenerator(name = "generator",
strategy = "com.esteel.uaa.manager.IdGenerator")
@Getter
private Long id;
@Getter
private String username;
@Getter
private String password;
@Getter
private String name;
@Getter
private boolean locked = false;
@Getter
private boolean enabled = true;
@Getter
private boolean expired = false;
@ManyToMany
@JoinTable(name = "uaa_rel_account_role",
joinColumns = @JoinColumn(name = "account_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private final Set<Role> roles = new HashSet<>();
public Set<Role> getRoles() {
return Collections.unmodifiableSet(roles);
}
这里是状态代码区,里面声明了一些状态,全是只读状态,因为我这边对Account的定义就是一旦被创建,就不允许通过简单的Setter方法来修改状态,我是需要进行逻辑控制的。
包括多对多的一个集合Roles,我这边给出的是一个不可变的集合,意味着我只允许你读,但是不允许你 随便 写,想写可以,必须通过我提供的函数进行操作。
第三段是对象行为,我这边其实比较简单,看上去与普通的setter没啥区别:
public void lock() {
this.locked = true;
public void unlock() {
this.locked = false;
public void enable() {
this.enabled = true;
public void disable() {
this.enabled = false;
public void expire() {
this.expired = true;
* 为账号添加角色,该角色必须已经被持久化,否则会抛出 {@link AccountAddRoleException}
* @param role 角色
public void addRole(Role role) {
if (role.getId() == null) {
throw new AccountAddRoleException();
this.roles.add(role);
* 移除一个角色
* @param role 角色
public void removeRole(Role role) {
this.roles.remove(role);
* 修改用户名,如果用户名已经存在,将会抛出 {@link AccountUsernameExistsException}
* 如果用户名不符合要求,则会抛出{@link AccountUsernameInvalidException}
* @param username 用户名
public void changeUsername(String username) {
if (repository.existsByUsername(username)) {
throw new AccountUsernameExistsException();
if (!StringUtils.hasText(username)) {
throw new AccountUsernameInvalidException();
this.username = username;
* 修改密码
* @param password 新密码
public void resetPassword(String password) {
this.password = passwordEncoder.encode(password);
* 修改姓名
* @param name 姓名
public void changeName(String name) {
this.name = name;
public void save() {
repository.save(this);
public void destroy() {
repository.delete(this);
}
这段代码看似平淡无奇,但是有两点需要注意:
第一点是:我在改变状态的时候加了逻辑处理,例如:
/**
* 修改用户名,如果用户名已经存在,将会抛出 {@link AccountUsernameExistsException}
* 如果用户名不符合要求,则会抛出{@link AccountUsernameInvalidException}
* @param username 用户名
public void changeUsername(String username) {
if (repository.existsByUsername(username)) {
throw new AccountUsernameExistsException();
if (!StringUtils.hasText(username)) {
throw new AccountUsernameInvalidException();
this.username = username;
}
第二点是:我依赖了持久化层Repository和外部工具PasswordEncounter,例如:
repository.save(this);
以及
this.password = passwordEncoder.encode(password);
这里,就引申出一个问题,这俩货到底是怎么被注入进来的?
我们接着看下面的代码:
// ===============构造方法区================
@Transient
private PasswordEncoder passwordEncoder;
@Transient
private AccountRepository repository;
protected Account(String username, String password) {
this.init();
this.username = username;
this.password = passwordEncoder.encode(password);
@PostLoad
private void init() {
passwordEncoder = ApplicationContextProvider.getBean(PasswordEncoder.class);
repository = ApplicationContextProvider.getBean(AccountRepository.class);
log.info(repository + "");
}
我们在构造这个类的时候,在构造函数中做了一个动作,就是
@PostLoad
所标记的方法,我在这里初始化了这两依赖。
@PostLoad
大家都知道,就是JPA从数据库中加载这个对象的时候,会执行这个代码。
这样,我就可以为这个对象赋予了更多 实际行为。
什么叫 实际行为 ?
我们对于DDD的理解,往往都是只关注实现而不关注如何落地。
这也是争论比较多的地方。
如果只关注如何实现的话,Domain层不应当依赖任何持久化或外部工具,应当独立的完成状态变更,由外部控制器来决定如何落地。
这样有好处,即领域脱离框架。
但是也有坏处,就是过于理想主义,因为毕竟你的最终目标是如何落地,就如同我这里所讲的是如何基于JPA进行DDD开发一样。
不过落地有两种方式。
第一种方式就是状态改变即save:
public void resetPassword(String password) {
this.password = passwordEncoder.encode(password);
this.save();
}
即对象状态发生改变的时候,就立刻进行save,这样就不需要在控制器中显示的调用save,其实JPA管理的很好,你就算立刻进行sava,它也是在事务执行完毕之后再提交。
但是如果不再事务控制中,就容易出殡。
所以,第二种方式就是将save方法暴露出去,由外部控制器来决定何时进行save。
如果我是一个人写代码,我会选择直接save,如果是多人配合,我还是习惯将这个save操作暴露给操作者让他自己选择。
到这里,领域的定义已经完成, 基于JPA做DDD唯一的缺点就是,JPA的Entity【 建议】 你要有一个无参且最低级别为Protected的构造函数。
如果能够定义private,那就完美了,Protected的构造函数还是容易出殡,但是没办法。
后记:
如果你使用aspectj做高级AOP,利用
@Configurable
做注入我是非常不建议的,因为这玩意儿过于
高级
,多团队配合的时候容易出殡。
而
ApplicationContextProvidder
是一个套路玩法,具体代码如下:
@Slf4j
@Component
public final class ApplicationContextProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
private static void setContext(ApplicationContext ctx) {
ApplicationContextProvider.applicationContext = ctx;
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
setContext(ctx);