DDD-七、在 Hibernate 中使用值对象作为聚合标识符
如果您使用的是除 Hibernate 之外的其他 JPA 实现,则必须查看该实现的文档以了解如何创建自定义类型。
属性转换器不会做
第一个想法可能是使用简单的值对象和属性转换器。不幸的是,这是不可能的,因为 JPA 不支持对@Id
字段使用属性转换器。您可以做出妥协并为您的@Id
字段和简单的值对象使用“原始”ID 以从其他聚合中引用它们,但我个人不喜欢这种方法,因为您必须在值对象及其包装的原始对象之间来回移动ID,使编写查询更加困难。更好、更一致的方法是创建自定义 Hibernate 类型。
创建自定义休眠类型
当您为您的 ID 值对象创建自定义 Hibernate 类型时,它们可以在您的整个持久性上下文中使用,而无需任何额外的注释。这涉及以下步骤:
- 决定要在值对象中使用哪种原始 ID 类型:
UUID
、String
或Long
- 为您的值对象创建类型描述符。该描述符知道如何将另一个值转换为值对象的实例(包装),反之亦然(展开)。
- 创建一个自定义类型,将您的类型描述符与您想要用于您的 ID 的 JDBC 列类型联系在一起。
- 使用 Hibernate 注册您的自定义类型。
让我们看一个代码示例来更好地说明这一点。我们将创建一个名为的值对象 ID CustomerId
,它包含一个UUID
. 值对象如下所示:
package foo.bar.domain.model;
// Imports omitted
public class CustomerId implements ValueObject, Serializable { // <1>
private final UUID uuid;
public CustomerId(@NotNull UUID uuid) {
this.uuid = Objects.requireNonNull(uuid);
}
public @NotNull UUID unwrap() { // <2>
return uuid;
}
// Implementation of equals() and hashCode() omitted.
}
- 您必须实现
Serializable
接口,因为Persistable
假定 ID 类型是可持久的。我有时会创建一个新的标记接口DomainObjectId
,称为扩展ValueObject
和Serializable
。 UUID
当您实现类型描述符时,您需要一种获取底层的方法。
接下来,我们将创建类型描述符。我通常将它放在一个子包.hibernate
中,以保持域模型本身的美观和整洁。
package foo.bar.domain.model.hibernate;
// Imports omitted
public class CustomerIdTypeDescriptor extends AbstractTypeDescriptor<CustomerId> { // <1>
public CustomerIdTypeDescriptor() {
super(CustomerId.class);
}
@Override
public String toString(CustomerId value) { // <2>
return UUIDTypeDescriptor.ToStringTransformer.INSTANCE.transform(value.unwrap());
}
@Override
public ID fromString(String string) { // <3>
return new CustomerId(UUIDTypeDescriptor.ToStringTransformer.INSTANCE.parse(string));
}
@Override
@SuppressWarnings("unchecked")
public <X> X unwrap(CustomerId value, Class<X> type, WrapperOptions options) { // <4>
if (value == null) {
return null;
}
if (getJavaType().isAssignableFrom(type)) {
return (X) value;
}
if (UUID.class.isAssignableFrom(type)) {
return (X) UUIDTypeDescriptor.PassThroughTransformer.INSTANCE.transform(value.unwrap());
}
if (String.class.isAssignableFrom(type)) {
return (X) UUIDTypeDescriptor.ToStringTransformer.INSTANCE.transform(value.unwrap());
}
if (byte[].class.isAssignableFrom(type)) {
return (X) UUIDTypeDescriptor.ToBytesTransformer.INSTANCE.transform(value.unwrap());
}
throw unknownUnwrap(type);
}
@Override
public <X> CustomerId wrap(X value, WrapperOptions options) { // <5>
if (value == null) {
return null;
}
if (getJavaType().isInstance(value)) {
return getJavaType().cast(value);
}
if (value instanceof UUID) {
return new CustomerId(UUIDTypeDescriptor.PassThroughTransformer.INSTANCE.parse(value));
}
if (value instanceof String) {
return new CustomerId(UUIDTypeDescriptor.ToStringTransformer.INSTANCE.parse(value));
}
if (value instanceof byte[]) {
return new CustomerId(UUIDTypeDescriptor.ToBytesTransformer.INSTANCE.parse(value));
}
throw unknownWrap(value.getClass());
}
public static final CustomerIdTypeDescriptor INSTANCE = new CustomerIdTypeDescriptor(); // <6>
}
AbstractTypeDescriptor
是驻留在org.hibernate.type.descriptor.java
包中的 Hibernate 基类。- 此方法将我们的值对象转换为字符串。我们使用 Hibernate 内置
UUIDTypeDescriptor
(也来自org.hibernate.type.descriptor.java
包)的帮助类来执行转换。 - 此方法从字符串构造值对象。同样,我们使用来自
UUIDTypeDescriptor
. - 此方法将值对象转换为
UUID
、字符串或字节数组。同样,我们使用来自UUIDTypeDescriptor
. - 此方法将一个
UUID
、一个字符串或一个字节数组转换为一个值对象。这里也使用了辅助类。 - 我们可以将此类型描述符作为单例访问,因为它不包含任何可变状态。
到目前为止,我们只处理了 Java 类型。现在是时候将 SQL 和 JDBC 结合起来并创建我们的自定义类型了:
package foo.bar.domain.model.hibernate;
// Imports omitted
public class CustomerIdType extends AbstractSingleColumnStandardBasicType<CustomerId> // <1>
implements ResultSetIdentifierConsumer { // <2>
public CustomerIdType() {
super(BinaryTypeDescriptor.INSTANCE, CustomerIdTypeDescriptor.INSTANCE); // <3>
}
@Override
public Serializable consumeIdentifier(ResultSet resultSet) {
try {
var id = resultSet.getBytes(1); // <4>
return getJavaTypeDescriptor().wrap(id, null); // <5>
} catch (SQLException ex) {
throw new IllegalStateException("Could not extract ID from ResultSet", ex);
}
}
@Override
public String getName() {
return getJavaTypeDescriptor().getJavaType().getSimpleName(); // <6>
}
}
AbstractSingleColumnStandardBasicType
是驻留在org.hibernate.type
包中的 Hibernate 基类。- 为了使自定义类型在
@Id
字段中正常工作,我们必须从org.hibernate.id
包中实现这个额外的接口。 - 这里我们传入 SQL 类型描述符(在本例中为二进制,因为我们将 UUID 存储在 16 字节二进制列中)和我们的 Java 类型描述符。
- 在这里,我们从 JDBC 结果集中检索 ID 作为字节数组…
- …并
CustomerId
使用我们的 Java 类型描述符将其转换为 a。 - 自定义类型需要一个名称,因此我们使用 Java 类型的名称。
最后,我们只需要在 Hibernate 中注册我们的新类型。我们将在与我们的类package-info.java
位于同一包中的文件中执行此操作:CustomerId
@TypeDef(defaultForType = CustomerId.class, typeClass = CustomerIdType.class) // <1>
package foo.bar.domain.model;
import org.hibernate.annotations.TypeDef; // <2>
import foo.bar.domain.model.hibernate.CustomerIdType;
- 这个 Hibernate 注释告诉 Hibernate
CustomerIdType
在遇到CustomerId
. - 请注意,导入是在文件中的注释之后进行的,
package-info.java
而不是在类文件中之前。
呸!现在我们可以使用CustomerId
两者来识别Customer
聚合并从其他聚合中引用它们。但是请记住,如果您让 Hibernate 为您生成 SQL 模式并且您使用 ID 来引用聚合而不是@ManyToOne
关联,那么 Hibernate 将不会创建外键约束。您必须自己执行此操作,例如使用Flyway。
如果您有许多不同的 ID 值对象类型,您将需要为您的类型描述符和自定义类型创建抽象基类,以避免重复自己。我将把这个作为练习留给读者。
但是等等,我们是不是忘记了什么?CustomerID
当我们持久化新创建的Customer
聚合根时,我们实际上将如何生成新实例?让我们来了解一下。
生成值对象 ID
准备好 ID 值对象和自定义类型后,您需要一种生成新 ID 的方法。您可以在持久化实体之前创建您的 ID 并手动分配它们(如果您使用 UUID,这真的很容易),或者您可以将 Hibernate 配置为在需要时自动为您生成 ID。后一种方法设置起来更困难,但一旦完成就更容易使用,所以让我们来看看。
重构你的基类
JPA 支持不同的 ID 生成器。如果查看@GeneratedValue
注释,则可以指定generator
要使用的名称。在这里,我们遇到了第一个警告。如果您在映射的超类(例如AbstractPersistable
)中声明您的 ID 字段,则您无法覆盖该@GeneratedValue
字段的注释。换句话说,对于扩展此基类的所有聚合根和实体,您都无法使用相同的 ID 生成器。如果您发现自己处于这种情况,您必须从基类中删除您的 ID 字段,并让每个聚合根和实体声明其自己的 ID 字段。
因此,BaseEntity
类(我们最初在这里定义了这个类)变成了这样的:
@MappedSuperclass
public abstract class BaseEntity<Id extends Serializable> implements Persistable<Id> { // <1>
@Version
private Long version;
@Override
@Transient
public abstract @Nullable ID getId(); // <2>
@Override
@Transient
public boolean isNew() { // <3>
return getId() == null;
}
public @NotNull Optional<Long> getVersion() {
return Optional.ofNullable(version);
}
protected void setVersion(@Nullable version) {
this.version = version;
}
@Override
public String toString() { // <4>
return String.format("%s{id=%s}", getClass().getSimpleName(), getId());
}
@Override
public boolean equals(Object obj) { // <5>
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!getClass().equals(ProxyUtils.getUserClass(obj))) { // <6>
return false;
}
var that = (BaseEntity<?>) obj;
var id = getId();
return id != null && id.equals(that.getId());
}
@Override
public int hashCode() { // <7>
var id = getId();
return id == null ? super.hashCode() : id.hashCode();
}
}
- 我们不再扩展
AbstractPersistable
,但我们确实实现了Persistable
接口。 - 此方法来自
Persistable
接口,必须由子类实现。 - 这个方法也来自
Persistable
接口。 - 由于我们不再扩展
AbstractPersistable
,我们必须重写toString
自己以返回有用的东西。我有时还包括对象身份哈希码,以明确我们是否正在处理同一实体的不同实例。 - 我们还必须覆盖
equals
. 请记住,具有相同 ID 的相同类型的两个实体被视为相同的实体。 ProxyUtils
是一个 Spring 实用程序类,对于 JPA 实现对实体类进行字节码更改的情况很有用,导致getClass()
不一定返回您认为它可能返回的内容。- 既然我们已经覆盖
equals
了,我们也必须以hashCode
同样的方式覆盖。
现在,当我们对 进行必要的更改后BaseEntity
,我们可以将 ID 字段添加到聚合根:
@Entity
public class Customer extends BaseAggregateRoot<CustomerId> { // <1>
public static final String ID_GENERATOR_NAME = "customer-id-generator"; // <2>
@Id
@GeneratedValue(generator = ID_GENERATOR_NAME) // <3>
private CustomerId id;
@Override
public @Nullable CustomerId getId() { // <4>
return id;
}
}
- 我们 extend
BaseAggregateRoot
,这反过来又扩展了我们重构的BaseEntity
类。 - 我们在常量中声明 ID 生成器的名称。当我们向 Hibernate 注册我们的自定义生成器时,我们将使用它。
- 现在我们不再拘泥于映射超类中使用的任何注释。
- 我们从 中实现抽象
getId()
方法Persistable
。
实现 ID 生成器
接下来,我们必须实现我们的自定义 ID 生成器。由于我们使用的是 UUID,这几乎是微不足道的。对于其他 ID 生成策略,我建议您选择现有的 Hibernate 生成器并在此基础上进行构建(开始查看此处)。ID 生成器将如下所示:
package foo.bar.domain.model.hibernate;
public class CustomerIdGenerator implements IdentifierGenerator { // <1>
public static final String STRATEGY = "foo.bar.domain.model.hibernate.CustomerIdGenerator"; // <2>
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return new CustomerId(UUID.randomUUID()); // <3>
}
}
IdentifierGenerator
是驻留在org.hibernate.id
包中的接口。- 由于新生成器是如何在 Hibernate 中注册的,我们需要类的全名作为字符串。我们将它存储在一个常量中,以使未来的重构更容易 - 并最大限度地减少由拼写错误引起的错误风险。
- 在此示例中,我们使用
UUID.randomUUID()
创建新的 UUID。请注意,如果您需要执行更高级的操作,例如从数据库序列中检索数值,您可以访问 Hibernate 会话。
最后,我们必须向 Hibernate 注册我们的新 ID 生成器。与自定义类型一样,这发生在 中package-info.java
,变为:
@TypeDef(defaultForType = CustomerId.class, typeClass = CustomerIdType.class)
@GenericGenerator(name = Customer.ID_GENERATOR_NAME, strategy = CustomerIdGenerator.STRATEGY) // <1>
package foo.bar.domain.model;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.TypeDef;
import foo.bar.domain.model.hibernate.CustomerIdType;
import foo.bar.domain.model.hibernate.CustomerIdGenerator;
- 这个注解告诉 Hibernate
CustomerIdGenerator
在遇到名为customer-id-generator
.
双呸!现在,我们的域模型应该可以按照我们的预期工作,使用自动生成的值对象作为 ID。
关于复合键的说明
在我们离开 ID 的主题之前,我只想提一件事。通过将 ID 字段从映射的超BaseEntity
类(例如,您可能遇到组合键由另一个聚合根的 ID 和枚举常量组成的情况。Customer
@EmbeddedId
@IdClass
- 原文作者:知识铺
- 原文链接:https://geek.zshipu.com/post/DDD-%E4%B8%83%E5%9C%A8-Hibernate-%E4%B8%AD%E4%BD%BF%E7%94%A8%E5%80%BC%E5%AF%B9%E8%B1%A1%E4%BD%9C%E4%B8%BA%E8%81%9A%E5%90%88%E6%A0%87%E8%AF%86%E7%AC%A6/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com