实践DDD的时候,如何使用hibernate呢?
1、分离领域模型与映射类的方案
DDD能给项目带来不少好处,我们必须得采用。在用代码实现模型的时候,遇到了如何在DDD中使用hibernate等ORM框架的问题。Hibernate的Entity,实际上是数据库表的映射类,如果直接就用hibernate的Entity来实现领域模型中的entity,有一些坏处。 一来会让开发人员有时会更观注于数据库,而不是模型。二来,hibernate的entity对类有一些限制,比如hibernate映射到关联关系的类时,可能会生成错误的子类(详见 这篇文章)。三来,从本质上说,一个类既代表了数据库表结构,又代表了领域对象,职责不够分离。最后,从分层的角度说,数据库表结构的类应该放到领域层下面的基础设施层才对。 因此,在项目开始的时候,我先和同事们尝试了将领域模型类与hibernate的entity分开。具体的方案参看《 分层结构实践》,简单说来,就是单独建一个类映射到数据库,然后在领域模型的类中引用这个类,以避免大量的属性复制。这样做职责划分的比较清晰,层状结构也比较清楚,但是仓储实现起来比较困难,因为要手动维护对象之间的关系。实践起来大概就以下这几种方式:
1.1 领域模型引用映射类
这一方案里,让领域模型类有一个域是到映射类的引用,这样对领域对象的操作,会被领域对象转化成它所引用的映射对象的数据操作。持久化时在仓储内针对映射对象做操作。这一方案有一个不足之处,为了能在保存某个对象后,立即取得它的主键,hibernate的延迟写入的策略必须得失效了。
1.1.1 领域模型外同步到数据库
这个子方案是让领域模型对象不去负责管理映射对象之间的关系,而是把职责交给外部来实现。这一方案对外部系统有一些使用上的约束。代码看起来就像这个样子:
//一、一个对像对应一张表,没有关联
public class ATable {
}
public class A {
private ATable table;
}
aRepository.save(a);
//二、两个对象对应一张表,有一个关联
public class ATable {
}
public class B {
}
public class A {
private B b;
private ATable table;
public B getB() {
B b = new B();
//从table中取数据,构建b
return b;
}
public void setB(B b) {
//把b 里面的数据写到table中
}
}
aRepository.save(a); //保存a的时候,自然就保存了b
B b=a.getB();
//... 对b做一系统操作导致b发生变化
a.setB(b); //把对b的改动写回到a的table上
aRepository.update(a); //更新a的时候,自然就更新了b
// 三、两个对象对应两张表,有一个关联
public class ATable {
}
public class BTable {
}
public class B {
private BTable bTable;
}
public class A {
private B b;
private ATable table;
private BRepository bRepository;
//通过repository获得b,并且在这个类里做了缓存
public B getB() {
if (b == null) {
b = bRepository.findById(table.getBId());
}
return b;
}
public void setB(B b) {
this.b = b; //只是修改了对象引用
}
}
public class ARepository {
public void save(A a) {
if (a.getB() != null) { //保存a之前设置a到b的外键
a.getTable().setBId(a.getB().getId());
}
aDao.save(a);
}
}
//先保存b, 获得外键,再保存a
bRepository.save(b);
aRepository.save(a);
B b = a.getB();
//... //对b做一系统操作导致b发生变化
// 对b的改动记录到了b的table上,不需要a.setB(b),
// 但要分别更新b和a
bRepository.update(b);
aRepository.update(a);
//四、两个对象对应两张表,有一个多重关联
public class ATable {
}
public class BTable {
}
public class B {
private BTable bTable;
public void setAId(long aId) {
this.table.setAId(aId);
}
}
public class A {
private List<B> bs;
private ATable table;
private BRepository bRepository; //通过repository获得b,并且在这个类里做了缓存
public List<B> getBs() {
if (s == null) {
bs = bRepository.findById(table.getBId());
}
return bs;
}
public void setBs(List<B> bs) { //只是更新了内存中的集合,并没有更新数据库 //中对象之间的关联关系
this.bs = bs;
for (B b : bs) {
b.setAId(a.getId());
}
}
}
public class ARepository {
public void save(A a) {
aDao.save(a);
for (B b : bs) {
b.setAId(a.getId());
}
}
public void update(A a) {
aDao.update(a);
//把a里面的所有的b同步到数据库,对数据库里面
// 的b做相应的添加删除
}
}
public class BRepository {
public void save(B b) {
//b的table上的外键已经被ARepository更新了 bDao.save(b);
}
}
//先保存a, 获得aTable的主键,再保存b
aRepository.save(a);
for(B b:a.getBs()) {
bRepository.save(b);
}
//只改变b,不改变b和a的关系
B b = a.getBs().get(0);
//... // 对b做一系统操作导致b发生变化
// 对b的改动记录到了b的table上
bRepository.update(b);
// 改变了b和a的关系
List<B> bs = a.getBs();
bs.remove();
bs.add();
//...
// a和b的关系发生了改变,要把bs设置回a上,
// 已使关系改动生效
a.setBs(bs);
aRepository.update(a);
//五、两个对象对应三张表(多对多的中间表),有一个多重关联
public class ATable {
}
public class BTable {
}
public class ABTable {
};
public class B {
private BTable bTable;
}
public class A {
private List<B> bs;
private ATable table;
private BRepository bRepository; //通过repository获得b,并且在这个类里做了缓存
public List<B> getBs() {
if (s == null) {
bs = aRepository.findBById(table.getId());
}
return bs;
}
public void setBs(List<B> bs) {
//只是更新了内存中的集合,
// 并没有更新数据库中对象之间的关联关系
this.bs = bs;
}
}
public class ARepository {
public void save(A a) {
aDao.save(a);
//把a里面的所有和b的关联关系同步到中间表
}
public void update(A a) {
aDao.update(a); //把a里面的所有和b的关联关系同步到中间表
}
}
//先保存b, 获得bTable的主键,再保存a
for (B b : a.getBs()) {
bRepository.save(b);
}
aRepository.save(a);
//只改变b,不改变b和a的关系
B b=a.getBs().get(0);
//...
// 对b做一系统操作导致b发生变化
// 对b的改动记录到了b的table上
bRepository.update(b);
//改变了b和a的关系
List<B> bs = a.getBs();
bs.remove();
bs.add();
//...
// 必须先保证b都存储好了,有了id,才能更新a和b的关系,
// 否则中间表不知道外键
for (B b : bs) {
if (b.getId() == null) {
bRepository.save(b);
}
}
//a和b的关系发生了改变,要把bs设置回a上,已使关系改动生效
a.setBs(bs);
aRepository.update(a);
在实践中发现,这个方案不太好,编程还是比较复杂,而且容易出错。如果使用这个模型的人,没有按照正确地顺序存储,关联关系很有可能会出错。
1.1.2 领域模型内同步到数据库
这一方案,可以说是对上面的改进,当两个领域对要建立关系时,直接在领域对象内就调用仓储来建立映射类之间的关联关系。代码如下:
//一、两个对象对应两张表,单向关联
public class B {
private BTable table;
}
public class A {
private ATable table;
private B b;
private ARepository aRepository;
public void setB(B b) {
this.b = b;
this.table.setBId(b.getId());
this.aRepository.update(this);
}
}
//新建A和B的场景
A a = new A();
aRepository.save(a);
B b = new B();
bRepository.save(b);
a.setB(b);
//二、两个对象对应两张表,有一个单向多重关联的情况
public class B {
private BTable table;
}
public class A {
private ATable table;
private List<B> bs;
private ARepository aRepository;
private BRepository bRepository;
public List<B> getBs() {
if (bs == null) {
this.bs = bRepository.findByAId(this.getId());
}
return bs;
}
public void addB(B b) {
this.bs.add(b);
b.setAId(this.getId());
this.bRepository.update(b);
}
public void deleteB(B b) {
this.bs.remove(b);
b.setAid(null);
this.bRepository.update(b);
}
}
//新建A和B的场景
A a = new A();
aRepository.save(a);
B b=new B();
//建立关系
a.addB(b);
//解除关系
a.deleteB(b);
//三、两个对象对应三张表,多对多,有一个单向 //多重关联的情况
public class B {
private BTable table;
}
public class A {
private ATable table;
private List<B> bs;
private ARepository aRepository;
public List<B> getBs() {
if (bs == null) {
this.bs = aRepository.findBByAId(this.getId());
}
return bs;
}
public void addB(B b) {
this.bs.add(b);
this.aRepository.addB(this, b);
}
public void deleteB(B b) {
this.bs.remove(b);
this.aRepository.deleteB(this, b);
}
}
public class ARepository {
public void addB(A a, B b) {
ABTable abTable = abTableDao.findById(a.getId(), b.getId());
if (abTable == null) {
abTableDao.save(new ABTable(a.getId(), b.getId());
}
}
public void deleteB(A a, B b) {
ABTable abTable = abTableDao.findById(a.getId(), b.getId());
if (abTable == null) {
abTableDao.delete(abTable);
}
}
}
//新建A和B的场景
A a = new A();
aRepository.save(a);
B b = new B();
//建立关系
a.addB(b);
//解除关系
a.deleteB(b);
1.2 领域模型与映射类相互复制状态
这一方案就是彻底分开领域模型与映射类,两者都互相不引用,只在仓储内实现两者的转换。比如,新建一个模型对象,要把它存储起来时,就在仓储内新建映射对象,然后把领域模型里面的状态数据复制到映射对象上。这个操作既繁琐,又容易出错,尤其是当有复杂的关联关系的时候,这个转化过程是非常的痛苦的。代码就不举例了。
1.3 父类实现领域模型,子类实现映射
我的一个同事提出了另一种方案,就是领域模型类都是抽像类,里面的getter和setter都不实现。映射类继承领域模型类,并实现那些getter, setter,并在getter上加上hibernate的映射用的annotation。这样做得到的代码不太符合常人的思维。这样做只是把职责分离到了两个文件中,实际上还由同一个对象负责,领域模型和映射类非常强的偶合,还不如不分的好。
2、领域模型兼作映射类
通过上面的实践,我们发现,如果分离了领域模型与映射类的话,那么hibernate的作用非常小了。如果不用hibernate, 而是用jdbc直接访问数据库,方案1.1.2 是一个非常好的方案。看来,为了不管理那么多映射类和对象间的关联关系,在使用hibernate的时候,最好还是让领域模型兼作映射类。现在,我们在实践中也是以这种方式为主。但这并不意味着回到老路,实践中为了保证模型的完整,我们这样做:
2.1 把annotation打在private域上而不是getter上
这样hibernate可以通过反射管理对象的状态,而又不必到处建setter和getter。
2.2 尽量减少领域对象上的setter
如果所有属性都有setter,都是public的,那么这个类的封装就失去意义了,因为任何地方都可以修改它。实践中,我们一般把setter设成包内可访问或protected的,这样同一个包的仓储和服务,就可以去直接修改对象,而包外的对象则不能。
2.3 尽量使用有参数的构造方法或工厂类
把无参的构造方法设置成protected,留给hibernate用,其它的代码尽量使用有参数的构造方法,以尽量实现构建出的对象就是可用的。如果构造过程参数太多,或太复杂,就把这一职责交给工厂,别的类必须通过工厂来创建领域对象。
2.4 通过AOP等向领域模型中注入一些依赖
如果不能给领域模型中的实体和值对象注入依赖,有时一些本来该由它们担负的职责就只好交给服务来完成。不知不觉又会走向所谓的贫血模型。
3 总结
如果不采用ORM,把领域模型与映射类分开是一个非常好的方案,当然代码量不小。如果使用ORM,一般情况下还是让领域模型兼任映射类吧,对于那些领域模型无法通过ORM映射的情况,还是采用分离方案吧,毕竟保证领域模型的完美更重要。采用ORM了,也还要尽量保证领域模型的完美,不要到处是public的setter和无参构建方法。