Hibernate中值对象与旧数据的向后兼容性问题探讨
我认为Value Object模式非常实用,《Secure by Design》《Implementing Domain-Driven Design》《Domain Modeling Made Functional》等诸多书籍都倡导使用该模式来让领域类型更清晰、更具类型安全性。
例如,假设我有一个包含名称的Order实体,我可以将name字段标记为String类型,但Value Object模式建议创建一个独立的OrderName类,其构造函数可包含订单名称的验证逻辑,代码示例如下:
@Entity class Order { ... @Embedded private OrderName name; } @Data @Setter(PRIVATE) @Embeddedable class OrderName extends SelfValidated { // 自定义Bean验证注解 @OrderNameValid private String value; public OrderName(String value) { // 名称长度不能超过50个字符 this.value = value; // 手动调用Bean验证器 validateSelf(); } protected OrderName() { // 由Hibernate调用 // Hibernate会自动扫描Bean验证注解 // 并在值对象持久化/加载时调用验证器 } }
乍一看一切正常,若传入的输入违反既定业务规则(如长度超过50字符),则无法创建OrderName实例。但如果需求发生变化呢?假设业务决定订单名称的最大长度改为30字符,这意味着我无法处理旧订单——因为若要修改旧订单,必须从数据库读取数据并创建Order实体,而旧数据会触发验证失败。
我长期思考这个问题,想到了4种解决方案,但认为没有一种是完全合理的,因此想听听大家的看法。以下是我总结的解决思路:
更新数据库中的旧数据以匹配新业务规则
这种方法很直接,若数据无效则修改使其符合规则,开发者通常会将此类更新作为Flyway或Liquibase迁移脚本。但该方案并不完美,原因如下:
- 部分数据可能存储为复杂结构(如jsonb对象),更新操作会非常繁琐复杂;
- 这类更新迁移脚本难以测试,若使用Testcontainers启动数据库实例后立即执行迁移,脚本会在空数据集上运行,无法验证效果,需单独编写测试用例;
- 有时无法更新旧数据,只能直接处理现有数据。
将值对象仅用作输入参数
我自己想到了这个方案,甚至在会议上做过相关分享。思路很简单,代码示例如下:
@Entity class Order { ... private String name; // 传入值对象以更新Order状态 public void changeName(OrderName name) { // 解包后存储原始值 this.name = name.getValue(); } }
可以看到,Hibernate实体将name存储为原始String类型,但修改时,公共方法接收OrderName值对象并进行解包。从数据库读取数据时,Hibernate通过无参构造函数创建实体实例,并通过Java Reflection API设置字段值。该方案有以下优势:
- 值对象仍作为领域层的公共API部分;
- 若接收值对象作为参数,可确保其符合当前业务规则;
- 需求变更不兼容时,不会阻碍读取旧数据。
但该方法也存在缺点:
- 代码变得更复杂且不直观,难以理解为何存储原始类型却接收值对象作为输入;
- 若Order类的其他方法需要使用名称进行后续操作,无法从存储值构造值对象,否则可能因旧数据无效抛出异常,因此领域层内仍需处理旧数据;
- 获取订单名称时,也无法将其包装为值对象(原因同上)。
这种情况下值对象的用法缺乏自解释性,因此有了下一个方案。
仅在公共构造函数中验证值对象
这种方案中,仅当直接在代码中创建值对象时才会进行验证,若Hibernate通过Java Reflection API实例化值对象,则不进行验证,直接设置值。代码示例如下:
@Data @Setter(PRIVATE) @Embeddedable class OrderName { private String value; public OrderName(String value) { // 名称长度不能超过50个字符 this.value = validateValue(value); } protected OrderName() { // 由Hibernate调用 // 此处不进行验证 } }
向后兼容性问题不再存在,但值对象的核心思想被破坏了——值对象的本质是无法用无效数据实例化,而现在Hibernate可通过调用protected构造函数创建包含无效value的OrderName。
若采用该方案,代码的健壮性会下降,例如:
@Entity class Order { ... private String name; public void changeName(OrderName name) { // 该对象是否有效? this.name = name; } }
若传入的OrderName是由Hibernate而非客户端创建的,则无法保证其符合当前验证规则,因此代码需修改为:
@Entity class Order { ... private String name; public void changeName(OrderName name) { if (name.isValid()) { this.name = name; } else { throw new OrderNameNotValidException(...); } } }
若允许值对象处于有效或无效状态,使用值对象将毫无优势,反而会让代码更复杂、不直观。
完全不使用值对象
既然Value Object模式带来这么多障碍,或许应该彻底弃用?此外,一些IT专家如Allen Holub声称Value Object从根本上是一种反模式,我对此并不确定。
我很乐意了解大家对这个问题的看法,尤其是同为Hibernate使用者的朋友,非常感谢任何有价值的建议。
内容的提问来源于stack exchange,提问作者Semyon Kirekov

