如果您使用的是除 Hibernate 之外的其他 JPA 实现,则必须查看该实现的文档以了解如何创建自定义类型。

属性转换器不会做

第一个想法可能是使用简单的值对象和属性转换器。不幸的是,这是不可能的,因为 JPA 不支持对@Id字段使用属性转换器。您可以做出妥协并为您的@Id字段和简单的值对象使用“原始”ID 以从其他聚合中引用它们,但我个人不喜欢这种方法,因为您必须在值对象及其包装的原始对象之间来回移动ID,使编写查询更加困难。更好、更一致的方法是创建自定义 Hibernate 类型。

创建自定义休眠类型

当您为您的 ID 值对象创建自定义 Hibernate 类型时,它们可以在您的整个持久性上下文中使用,而无需任何额外的注释。这涉及以下步骤:

  1. 决定要在值对象中使用哪种原始 ID 类型:UUIDStringLong
  2. 为您的值对象创建类型描述符。该描述符知道如何将另一个值转换为值对象的实例(包装),反之亦然(展开)。
  3. 创建一个自定义类型,将您的类型描述符与您想要用于您的 ID 的 JDBC 列类型联系在一起。
  4. 使用 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.
}
  1. 您必须实现Serializable接口,因为Persistable假定 ID 类型是可持久的。我有时会创建一个新的标记接口DomainObjectId,称为扩展ValueObjectSerializable
  2. 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>
}
  1. AbstractTypeDescriptor是驻留在org.hibernate.type.descriptor.java包中的 Hibernate 基类。
  2. 此方法将我们的值对象转换为字符串。我们使用 Hibernate 内置UUIDTypeDescriptor(也来自org.hibernate.type.descriptor.java包)的帮助类来执行转换。
  3. 此方法从字符串构造值对象。同样,我们使用来自UUIDTypeDescriptor.
  4. 此方法将值对象转换为UUID、字符串或字节数组。同样,我们使用来自UUIDTypeDescriptor.
  5. 此方法将一个UUID、一个字符串或一个字节数组转换为一个值对象。这里也使用了辅助类。
  6. 我们可以将此类型描述符作为单例访问,因为它不包含任何可变状态。

到目前为止,我们只处理了 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>
    }
}
  1. AbstractSingleColumnStandardBasicType是驻留在org.hibernate.type包中的 Hibernate 基类。
  2. 为了使自定义类型在@Id字段中正常工作,我们必须从org.hibernate.id包中实现这个额外的接口。
  3. 这里我们传入 SQL 类型描述符(在本例中为二进制,因为我们将 UUID 存储在 16 字节二进制列中)和我们的 Java 类型描述符。
  4. 在这里,我们从 JDBC 结果集中检索 ID 作为字节数组…
  5. …并CustomerId使用我们的 Java 类型描述符将其转换为 a。
  6. 自定义类型需要一个名称,因此我们使用 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;
  1. 这个 Hibernate 注释告诉 HibernateCustomerIdType在遇到CustomerId.
  2. 请注意,导入是在文件中的注释之后进行的,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();
    }
}
  1. 我们不再扩展AbstractPersistable,但我们确实实现了Persistable接口。
  2. 此方法来自Persistable接口,必须由子类实现。
  3. 这个方法也来自Persistable接口。
  4. 由于我们不再扩展AbstractPersistable,我们必须重写toString自己以返回有用的东西。我有时还包括对象身份哈希码,以明确我们是否正在处理同一实体的不同实例。
  5. 我们还必须覆盖equals. 请记住,具有相同 ID 的相同类型的两个实体被视为相同的实体。
  6. ProxyUtils是一个 Spring 实用程序类,对于 JPA 实现对实体类进行字节码更改的情况很有用,导致getClass()不一定返回您认为它可能返回的内容。
  7. 既然我们已经覆盖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;
    }
}
  1. 我们 extend BaseAggregateRoot,这反过来又扩展了我们重构的BaseEntity类。
  2. 我们在常量中声明 ID 生成器的名称。当我们向 Hibernate 注册我们的自定义生成器时,我们将使用它。
  3. 现在我们不再拘泥于映射超类中使用的任何注释。
  4. 我们从 中实现抽象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>
    }
}
  1. IdentifierGenerator是驻留在org.hibernate.id包中的接口。
  2. 由于新生成器是如何在 Hibernate 中注册的,我们需要类的全名作为字符串。我们将它存储在一个常量中,以使未来的重构更容易 - 并最大限度地减少由拼写错误引起的错误风险。
  3. 在此示例中,我们使用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;
  1. 这个注解告诉 HibernateCustomerIdGenerator在遇到名为customer-id-generator.

双呸!现在,我们的域模型应该可以按照我们的预期工作,使用自动生成的值对象作为 ID。

关于复合键的说明

在我们离开 ID 的主题之前,我只想提一件事。通过将 ID 字段从映射的超BaseEntity类(例如,您可能遇到组合键由另一个聚合根的 ID 和枚举常量组成的情况。Customer@EmbeddedId@IdClass