我意识到诸如 Morphia 和 Hibernate 之类的持久性框架依赖于域对象上的注释来发挥它们的魔力。在某种程度上,在我看来,这是将持久性问题插入到域层中,这是我们应该努力避免的。
这是我应该尝试通过使用外部配置文件或将 DTO 与域模型分开来避免的事情吗?或者,持久层和域层之间的这种小泄漏通常被认为是可以接受的吗?
我意识到诸如 Morphia 和 Hibernate 之类的持久性框架依赖于域对象上的注释来发挥它们的魔力。在某种程度上,在我看来,这是将持久性问题插入到域层中,这是我们应该努力避免的。
这是我应该尝试通过使用外部配置文件或将 DTO 与域模型分开来避免的事情吗?或者,持久层和域层之间的这种小泄漏通常被认为是可以接受的吗?
在我对使用 Spring 和 Hibernate 的现有系统的最新迭代中,我已经开始处理类似的问题。在第一次实现 Hibernate 模型时,我努力通过数据访问对象将服务类中的应用程序逻辑与持久性逻辑分开。去年构建一个新系统时,我允许大多数持久性对象充当域对象,因为这是权宜之计。
然而,我正在根据不断变化的业务需求重新设计相同的系统,并且我再次倾向于分离这些关注点。我只接触了新设计几天,但我已经发现自己更喜欢有一个代表内存中关注对象的对象,而一个单独的基于持久性的对象用于将其状态更改存储到数据库中。例如,我有一个持久性和一个跨事务Lead
的并行。ActiveLead
我还不相信这是最好的方法,但它在直觉层面上是有道理的。我一直渴望拥有一组与持久性无关——不,持久性——无知的对象集,它们在数据库事务中保持内存驻留,而不考虑标准的 CRUD 简化。然而我知道最终所有的数据库操作都是作为 CRUD 实现的。两个世界必须碰撞,诀窍在于让它们和谐共舞。
域对象上的休眠注释?在我看来,这是易于实施与易于维护之间的良好折衷。
我最近在一个相当复杂的系统上工作,它有一个单独的持久层,这是一个巨大的痛苦,对可维护性非常不利。你基本上是在看 YAGNI 和单一职责原则之间的冲突。在我看来,YAGNI 是更重要的一个(唉,也是最常被忽略的一个)。
我想说,在绝大多数情况下,如果您使用 ORM,直接持久化域对象会更好,除非您有具体要求强制持久化实体以不同的结构(如果它们具有完全相同的结构,除了象牙塔论点外,没有理由将它们分开)。
可以肯定的是:始终在单独的服务/DAO 层中执行实际的持久性工作(调用 ORM 函数)!这样,如果您发现需要它,以后很容易引入持久层。
域对象中的持久性注释是一种不好的做法吗?
是的。随着 NoSQL 的兴起,你不能依赖单一的持久化策略。
例如,今天我将我的域对象(比如说使用 morphia)持久化到MongoDB中。如果明天我想将域对象持久化到Neo4j怎么办?
或者,您可能希望将域对象持久保存到所有三种数据库,如关系数据库(Postgres / MySQL)、MongoDB(文档存储)和 Neo4J(图形数据库),仅用于评估。
在所有这些情况下,最好有单独的持久性策略,而不是仅仅依赖域对象
最佳实践:将持久策略作为策略模式传递可能会有所帮助。但是在设计你的类/对象时必须小心。
简短的回答:我喜欢持久的、丰富的域对象。
长答案:
近 10 年来,我使用 Spring 和 Hibernate 开发了一个相当大的系统 ~ 500k LOC。一开始,我们从“事务脚本”(参见 Fowler)方法开始,部分原因是我们不太信任 Hibernate。然而,在很短的时间内,我们开始信任 Hibernate,并且由于我更早地接受过相当纯粹的 OO 培训,我成为了将瞬态持久性与域驱动设计方法相结合的忠实信徒。我们基本上认为我们的系统是由 ODBMS 支持的(有很多小漏洞:-))。
我称我们的架构为“域内核”,因为 DDD 的书还没有写完。这是在 Hibernate 的早期,所以领域模型没有被注释污染。持久性的独立关注点在 XML 映射中保持独立。
同样,随着时间的推移,我们在将行为下推到领域层方面做得更好。我们有一个非常传统的控制器-->服务-->dao-->域分层方案,它通过编译时依赖项强制执行。随着时间的推移,我观察到这个模型非常适合我们的系统,它代表了相当复杂的 401(k) 计划管理领域的各个方面,包括计划设置、交易、会计、合规性测试、销售、品牌推广等。具有(相对)透明的“神奇”持久性的富域模型是我们能够根据域模型中的现有特性构建新特性的关键。
我们的服务层仅编排技术服务之间的交互(例如电子邮件、文件 I/O、排队等),并在必要时帮助跨域包。服务层还定义了事务边界(通过 Spring)。服务仅接收或发出 DTO 或原语。很多人讨厌这样,作为 DRY 的一个突破,但我们发现它在定义服务接口和使用它们的代码时让我们保持诚实。这也使得以后远程操作变得非常容易。
这种方法使我们能够用一个非常小的团队(我们是一个 Scrum 团队)构建高质量的软件。
因此,请考虑我是持久域对象的信徒。不知道我的故事是否有帮助,但我想分享。
如果我已经决定使用我要使用的持久性框架,我相信我会在我的域上使用注释,但是,如果您遵循Hexagonal架构和 TDD,XML 会更方便。如果您预先使用特定框架注释您的域,您将与持久性集成相结合,并且无法以与技术/框架无关的目的测试核心功能。
在我看来,无需复制域对象即可将它们与持久层分离。它可以处理重复的代码,并且通过使用与 DTO 相同的对象完全可行。如有必要,您可以随时使用单独的课程,但我不会将其作为经验法则,这会花费您的时间,而且我们都知道时间很宝贵。
我更喜欢上面有注释的富域对象。甚至 Evans 在他的示例应用程序中也使用了这种方法。他使用 XMl 而不是 Annotations,但他仍然保留相同的对象。
也许将域和持久性分开会更干净,但不要只是为了能够在未来选择不同的数据库技术。这是通往复杂地狱的道路,YAGNI 先生会咬你的。
在 DDD 社区发现的东西
由 Chris Richardson 发表 *如果您想将 JPA 排除在域模型之外,请使用 XML 而不是注释(我从来都不是 ORM 注释的粉丝,因为恕我直言,它混合了关注点)
就我个人而言,我非常喜欢使用注释,XML 对我来说总是容易出错,字段名称的一个小改动,你也需要手动更改 XML。如果你想重构你的域的一个类,你最终可能会更改几个文件而不是自动处理它。但最近,我一直在重新考虑这一点,因为我希望能够在一个项目中使用多个持久选项。我不想要任何与我的域中的持久性相关的东西,所以 XML 是一种选择。尽管如此,有几次我到达了没有直接映射的地步,或者我仍然想使用注释,因为它们很容易更改并且可以直接在代码中看到。我最近一直在做的事情是将我的业务域类创建为抽象类,并使用另一个以持久扩展它。像这样的东西:
public abstract class Persona {
private Set<State>states;
public boolean inState(State state){
return states.contains(state);
}
}
如果由于某种原因有一个数据库,其中状态已经定义为单个列,并且不可能进行直接映射,我可以扩展业务类并将其用作持久性实体。
@Entity
public class PersonaSql extends Persona {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String statesDefinition;
@PrePersist
void prePersist(){
this.statesDefinition = mapStatesSetToString();
}
@PostPersist
void postPersists(){
this.states = mapStatesStringToSet();
}
}
当然,这是一个微不足道的例子。还有其他方法可以解决这个问题,我的观点是:通过使用继承,您可以利用使用注释的巨大优势,并使您的业务模型不了解特定的持久性代码。
不使用继承的另一个选择是将持久性实体转换为业务模型,反之亦然,但我不建议采用这种方法(即使使用自动映射器之类的东西),除非您的域很简单并且您确定它会保持简单。例如,如果您正在创建微服务,那么您的域应该足够简单,并且它应该是简单的。