在领域对象中使用持久化注解是一种不好的做法吗?

36

我意识到像Morphia和Hibernate这样的持久化框架依赖于领域对象上的注释来实现它们的魔力。从某种程度上看,我觉得这是将持久化关注点插入到领域层中,这是我们应该尽力避免的。我应该通过使用外部配置文件或者将DTO与领域模型分离来避免这种情况吗?还是这种持久化和领域层之间的小漏洞通常被视为可以接受的?


2
请查看此演示文稿:http://www.infoq.com/presentations/Clean-Model-Tim-McCarthy - MikeSW
谢谢,这看起来是一份很棒的演示!正在观看中... - HolySamosa
8个回答

16
在我最近使用Spring和Hibernate对现有系统进行迭代时,我开始采用类似的方式。在最初实现Hibernate模型时,我努力通过数据访问对象将应用程序逻辑与持久化逻辑分离。去年构建新系统时,我允许大多数持久化对象作为域对象,因为那是一个便捷的解决方案。
然而,鉴于业务需求的变化,我正在重新设计同一系统,并再次倾向于分离这些关注点。我只是开始几天,但已经发现自己更喜欢拥有一个表示内存中关注点对象的对象,同时使用单独的基于持久性的对象来存储其状态更改到数据库中。例如,我有一个Lead用于持久性和一个并行的ActiveLead,跨事务存在。
我还没有完全相信这是最佳方法,但从直觉上说这是有道理的。我一直想要一个持久性不可知的对象集合,它们保留在内存中,在数据库事务之间不考虑标准的CRUD简化。然而,我理解最终所有的数据库操作都是作为CRUD实现的。这两个世界必须发生碰撞,诀窍在于使它们在和谐中共舞。
Hibernate的注释用于域对象?在我看来,这是易于实现与易于维护之间的良好折衷方案。

2
这也为您提供了更严格/更宽松的机会,以确定持久性对象与内存中对象的状态。我尝试非常注意接口的输入和输出,并以这种方式分离这些关注点可以实现这一点。 - alexwen

12

最近我参与了一个相当复杂的系统开发,该系统有一个单独的持久化层,但这对于可维护性来说非常糟糕且非常麻烦。你基本上是在YAGNI原则和单一职责原则之间寻求平衡。在我看来,YAGNI是更重要的(不幸的是,也经常被忽略)。

我认为,在绝大多数情况下,如果你使用ORM,直接将领域对象持久化会更好,除非你有具体的要求需要持久化实体以不同的结构进行组织(如果它们具有完全相同的结构,则没有理由分离它们,除了纯理论争论)。

确保:总是在单独的服务/DAO层中执行实际的持久化操作(调用ORM函数)!这样,如果你发现需要它,很容易引入持久化层。


1
有一个情况,当我使用同一对象时会遇到问题,那就是在多个线程/服务器上进行乐观锁定。例如,如果您尝试保存对 firstName 的更改,并发现其他人已更改了该对象,则需要重新加载对象以进行更改。如何将新加载和修改的对象插入回域对象树中? - David Harkness
3
被投票否决,我不同意。有太多注释涉及过多的问题,很多时候会把你的类结构引向不同的方向,再加上行为,使得你的类难以阅读和维护,这是不好的。 - dalvarezmartinez1
@dalvarezmartinez - 如果注释不仅用于数据持久性问题,还用于领域问题怎么办?例如:将领域对象上的“Name”属性标记为“[Required]”可以清楚地表明该字段是必需的,但也通知验证层和数据库层不应接受空值。 - Sam
2
@Sam 这可能是一个有效的情况,我并不是说你永远不应该有任何注释,在领域中绝对不是这样。只是在我的一点小而贫乏的经验中,单一职责比琐碎的YAGNI更重要(琐碎是因为在大多数情况下映射相当容易)。有些人,包括我们,强制领域对象具有行为(需要某些数据字段以某种方式结构化),同时这也用作DB数据结构(需要以另一种方式结构化的数据字段),看到了对象关系阻抗不匹配和一些XML注释,对我来说太多了。 - dalvarezmartinez1

9

在域对象中使用持久性注释是一种不好的做法吗?

是的。随着NoSQL的兴起,你不能依赖单一的持久化策略。

例如,今天我正在将我的域对象(假设使用Morphia)持久化到MongoDB。如果明天我想要将域对象持久化到Neo4j怎么办?

或者,有人想要将域对象持久化到所有三种类型的数据库,如关系型(Postgres/MySQL)、MongoDB(文档存储)和Neo4J(图形数据库),只是为了评估。

在所有这些情况下,最好有单独的持久化策略,而不仅仅依赖于域对象。

最佳实践:将持久化策略作为策略模式传递可能会有所帮助。但在设计类/对象时必须小心。


1
我认为有太多的“如果……”问题。我还没有看到过任何一个项目改变数据库或在单个模块中使用三种不同的数据库技术,这可能会牺牲维护额外策略/映射器等的成本。根据我的经验,采用这种方法开始的项目比简单地注释域层要困难得多。 - thilko
@thilko 我认为你的观点很中肯,从实际角度来看。不过,这让我得出结论:需要更容易地将持久性框架与存储库模式结合使用,特别是在DDD和/或TDD方面。 - Timo

5

如果我已经决定使用持久化框架,我会在我的领域上使用注解;然而,如果你遵循六边形架构和TDD,XML会更加方便。如果你提前使用特定框架对领域进行注释,那么你将与持久化集成耦合在一起,无法测试核心功能,也无法做到技术/框架无关。


域名实际上并没有与特定技术耦合,如果使用注释的话。是的,编译器需要注释接口,但执行的代码不会受到影响,因为注释只添加元数据。即使没有JPA实现,测试域也不是问题,甚至可以使用完全不同的持久化技术,如MongoDB,然后忽略注释。 - deamon

5
简短回答:我喜欢持久化、丰富的领域对象。
长远来看,我在一个相当大的系统上工作了近10年,使用了Spring和Hibernate,大约50万行代码。最初,我们采用了“事务脚本”(见Fowler)方法,部分原因是我们不太信任Hibernate。但是,在短时间内,我们开始信任Hibernate,并且由于我早期在OO方面接受了相当纯粹的训练,因此我成为了一位瞬态持久性与领域驱动设计方法的坚定支持者。我们基本上认为我们的系统是由ODBMS(带有很多小泄漏)支持的。
我将我们的架构称为“领域内核”,因为当时还没有写《领域驱动设计》这本书。这是Hibernate的早期阶段,因此领域模型没有被注释污染。持久性的单独关注点在XML映射中保持单独。
同样地,在一段时间后,我们变得更加擅长将行为压缩到领域层中。我们有一个相当传统的控制器->服务->dao->领域层分层方案,它通过编译时依赖强制执行。我随着时间的推移观察到,这个模型非常适合我们的系统,它代表了401(k)计划管理的相当复杂的领域的每个方面,包括计划设置、交易、会计、合规性测试、销售、品牌等。一个丰富的领域模型和(相对)透明的“神奇”持久化是我们能够以现有领域模型的特征构建新功能的关键所在。
我们的服务层只编排技术服务之间的交互(例如电子邮件、文件I/O、排队等),并在必要时帮助跨越领域包。服务层还通过Spring定义了事务边界。服务只接收或发出DTO或基本类型。很多人讨厌这个,因为它打破了DRY(不重复原则),但我们发现这让我们在定义服务接口和使用它们的代码时保持诚实。它也使以后的远程处理变得非常容易。
这种方法使我们能够用相当小的团队(我们是Scrum团队)构建高质量的软件。
所以,请把我看作是一位持久化领域对象的支持者。我不知道我的故事是否有帮助,但我想分享。

4

在我看来,为了将领域对象与持久层分离,没有必要复制这些对象。这样做会增加代码冗余,而使用这些对象作为DTO是完全可行的。如果有必要,您总是可以使用单独的类,但我不会把这视为铁则,这会浪费时间,而时间是宝贵的。


3
我不同意。领域对象本质上与持久化模型对象不同,它们有不同的目的。如果有时候领域并没有真正成为主导,并且持久化实体可以直接使用,那只是一个简单的巧合,一种实现细节。这个想法是你开始对领域进行建模,完全忽略持久性。最近我在博客中就这种情况发表了文章:http://www.sapiensworks.com/blog/post/2012/04/07/Just-Stop-It!-The-Domain-Model-Is-Not-The-Persistence-Model.aspx - MikeSW
根据我的经验,领域对象和表格并没有太大的区别。出现的差异应该很容易通过ORM的自定义选项进行翻译。在我看来,一个好的ORM应该帮助简化这个过程。 - Timo
完全同意@MikeSW及其博客文章。例如,我不想被强制使用丑陋的getter/setter在我的领域模型中,只是为了满足ORM的需求。我希望我的领域模型能够自由地存在,没有来自持久化世界的任何限制。 - Mik378

2
我更喜欢拥有注释的丰富领域对象。即使是Evans在他的示例应用程序中也使用了这种方法。他使用XML而不是注释,但仍然保持相同的对象。

也许将领域和持久性分离更加清晰,但不要只是为了以后可能选择不同的数据库技术而这样做。这是走向复杂度地狱的途径,YAGNI先生会咬你一口。

1

在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();
  }
}

当然,这只是一个微不足道的例子。解决此问题的其他方法也有,我的观点是:通过使用继承,您可以利用注释的巨大优势,并使业务模型忽略特定的持久性代码。
另一种不使用继承的选项是将持久性实体转换为业务模型,反之亦然,但我不建议走这条路(即使使用像automapper这样的工具),除非您的域很简单并且您确定它会保持简单。例如,如果您正在创建微服务,则您的领域应该足够简单,并且预计它将保持简单。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接