Using Value Objects as Aggregate Identifiers with Hibernate
Tags: domaindrivendesign, jpa, hibernate, ddd
Published: 2021-04-28 05:33:21

In [Tactical Domain-Driven Design](https://dev.to/peholmst/tactical-domain-driven-design-17dp), we learned that that we should refer to other aggregates by ID and use value objects to distinguish between different aggregate types (second aggregate design guideline).

This is perfectly possible to do with JPA and [Hibernate](http://hibernate.org/), but requires some additional work as it goes against the design principles of JPA where you should not have to care about the IDs at all and just work with your entity objects directly. Let's have a look at how. We will be building on the code and principles laid out in my posts about [value objects](https://dev.to/peholmst/using-value-objects-with-jpa-27mi) and [aggregates](https://dev.to/peholmst/building-aggregates-with-spring-data-2iig) so please read those first if you haven't already.

If you are using another JPA implementation than Hibernate, you have to check that implementation's documentation for how to create custom types.

# Attribute Converters Won't Do

The first thought may be to use a simple value object and an attribute converter. Unfortunately this is not possible as JPA does not support using attribute converters for `@Id` fields. You can make a compromise and use "raw" IDs for your `@Id` fields and simple value objects to refer to them from other aggregates, but I do not personally like this approach as you have to move back and forth between the value objects and their wrapped raw IDs, making it more difficult to write queries. A better, more consistent approach is to create custom Hibernate types.

# Creating a Custom Hibernate Type

When you create custom Hibernate types for your ID value objects, they become available for use inside your entire persistence context without any additional annotations anywhere. This involves the following steps:

1. Decide what kind of raw ID type you are going to use inside your value object: `UUID`, `String`, or `Long`
2. Create a type descriptor for your value object. This descriptor knows how to convert another value into an instance of your value object (wrap) and vice versa (unwrap).
3. Create a custom type that ties together your type descriptor with the JDBC column type you want to use for your ID.
4. Register your custom type with Hibernate.

Let's have a look at a code example to better illustrate this. We are going to create a value object ID called `CustomerId` that wraps a `UUID`. The value object looks like this:

```java
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. You have to implement the `Serializable` interface because `Persistable` assumes the ID type is persistable. I sometimes create a new marker interface called `DomainObjectId` that extends `ValueObject` and `Serializable`.
2. You need a way of getting the underlying `UUID` when you implement the type descriptor.

Next, we will create the type descriptor. I typically place this in a subpackage called `.hibernate` to keep the domain model itself nice and clean.

```java
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` is a Hibernate base class that resides in the `org.hibernate.type.descriptor.java` package.
2. This method converts our value object to a string. We use a helper class from Hibernate's built-in `UUIDTypeDescriptor` (also from the `org.hibernate.type.descriptor.java` package) to perform the conversion.
3. This method constructs a value object from a string. Again, we use a helper class from `UUIDTypeDescriptor`.
4. This method converts a value object into a `UUID`, a string or a byte array. Again, we use helper classes from `UUIDTypeDescriptor`.
5. This method converts a `UUID`, a string or a byte array into a value object. The helper classes are used here as well.
6. We can access this type descriptor as a singleton since it does not contain any changeable state.

So far we have only dealt with Java types. Now it is time to bring SQL and JDBC into the mix and create our custom type:

```java
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` is a Hibernate base class that resides in the `org.hibernate.type` package.
2. In order for the custom type to work properly in `@Id` fields, we have to implement this extra interface from the `org.hibernate.id` package.
3. Here we pass in the SQL type descriptor (in this case binary as we are going to store the UUID in a 16 byte binary column) and our Java type descriptor.
4. Here, we retrieve the ID from a JDBC result set as a byte array...
5. ... and convert it to a `CustomerId` using our Java type descriptor.
6. A custom type needs a name so we use the name of the Java Type.

Finally, we just have to register our new type with Hibernate. We will do this inside a `package-info.java` file that resides in the same package as our `CustomerId` class:

```java
@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. This Hibernate annotation tells Hibernate to use `CustomerIdType` whenever it encounters a `CustomerId`.
2. Note that the imports are coming after the annotations in a `package-info.java` file and not before as they do in a class file.

Phew! Now we can use `CustomerId` both to identify `Customer` aggregates and to refer to them from other aggregates. Please keep in mind, though, that if you let Hibernate generate your SQL schema for you and you use IDs to refer to aggregates instead of `@ManyToOne` associations, Hibernate will not create foreign key constraints. You will have to do that yourself, for example using [Flyway](https://flywaydb.org/).

If you have many different ID value object types, you will want to create abstract base classes for your type descriptors and custom types to avoid having to repeat yourself. I'm going to leave this as an exercise to the reader.

But wait, haven't we forgotten something? How are we actually going to generate new `CustomerID` instances when we persist newly created `Customer` aggregate roots? Let's find out.

# Generating Value Object IDs

Once you have your ID value objects and custom types in place, you need a way of generating new IDs. You can create your IDs and assign them manually before persisting your entities (this is really easy if you use UUIDs) or you can configure Hibernate to automatically generate IDs for you when they are needed. The latter approach is more difficult to set up but easier to work with once it is done so let's have a look at that.

## Refactoring Your Base Classes

JPA has support for different ID generators. If you look at the `@GeneratedValue` annotation, you can specify the name of the `generator` to use. Here we run into the first caveat. If you declare your ID field inside a mapped superclass (such as `AbstractPersistable`), there is no way for you to override the `@GeneratedValue` annotation for that field. In other words, you are stuck using the same ID generator for all of your aggregate roots and entities that extend this base class. If you find yourself in a situation like this, you have to remove your ID field from the base class and have every aggregate root and entity declare its own ID field. 

Thus, the `BaseEntity` class (we originally defined this class [here](https://dev.to/peholmst/building-aggregates-with-spring-data-2iig)) changes to something like this:

```java
@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. We no longer extend `AbstractPersistable` but we do implement the `Persistable` interface.
2. This method comes from the `Persistable` interface and will have to be implemented by the subclasses.
3. This method also comes from the `Persistable` interface.
4. Since we no longer extend `AbstractPersistable` we have to override `toString` ourselves to return something useful. I sometimes also include the object identity hash code to make it clear whether we are dealing with different instances of the same entity.
5. We also have to override `equals`. Remember that two entities of the same type with the same ID are considered the same entity.
6. `ProxyUtils` is a Spring utility class that is useful for cases where the JPA implementation has made bytecode changes to the entity class, resulting in `getClass()` not necessarily returning what you think it may return.
7. Since we have overridden `equals`, we also have to override `hashCode` in the same manner.

Now when we have made the necessary changes to `BaseEntity`, we can add the ID field to our aggregate root:

```java
@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. We extend `BaseAggregateRoot`, which in turn extends our refactored `BaseEntity` class.
2. We declare the name of the ID generator in a constant. We will be using this when we register our custom generator with Hibernate.
3. Now we are no longer stuck with whatever annotation was used in the mapped superclass.
4. We implement the abstract `getId()` method from `Persistable`.

## Implementing the ID Generator

Next, we have to implement our custom ID generator. Since we are using UUIDs this is going to be almost trivial. For other ID generation strategies, I suggest you pick an existing Hibernate generator and build on that (start looking [here](https://github.com/hibernate/hibernate-orm/tree/main/hibernate-core/src/main/java/org/hibernate/id)). The ID generator will look something like this:

```java
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` is an interface that resides in the `org.hibernate.id` package.
2. Because of how new generators are registered with Hibernate, we need the full name of the class as a string. We store it in a constant to make future refactoring easier - and minimize the risk of bugs caused by typos.
3. In this example we use `UUID.randomUUID()` to create new UUIDs. Please note that you have access to the Hibernate session if you need to do something more advanced, like retrieving a numeric value from a database sequence.

Finally, we have to register our new ID generator with Hibernate. Like with the custom type, this happens in `package-info.java`, which becomes:

```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. This annotation tells Hibernate to use `CustomerIdGenerator` whenever it encounters a generator named `customer-id-generator`.

Double-phew! Now our domain model should just work as we expect it to, with auto-generated value objects as IDs. 

# A Note on Composite Keys

Before we leave the subject of IDs, I just want to mention one thing. By moving the ID field from the mapped superclass (`BaseEntity`) to the concrete entity class (`Customer` in the example above), we also opened up the possibility to use composite keys in our entities (either using `@EmbeddedId` or `@IdClass`). You may for example have a situation where the composite key consists of the ID of another aggregate root and an enum constant.