This library provides base classes and tooling to create typed IDs. The original goal was to allow easy and safe creation of Value Objects (VOs) backed by UUIDv7 for Hibernate ORM entities. Contributions for other ORMs or alternative JPA implementations are welcome.
Blog: Specialized Value Objects for entity identifiers
For seamless type support in Hibernate ORM, you should pick one of the following variants:
Hibernate Version | Artifact |
---|---|
6.6, 6.5, 6.4, and 6.3 | org.framefork:typed-ids-hibernate-63 |
6.2 | org.framefork:typed-ids-hibernate-62 |
6.1 and 6.0 | TBD |
5.6 and 5.5 | TBD |
Find the latest version in this project's GitHub releases or on Maven Central.
If you want just the plain classes, you can install only the org.framefork:typed-ids.
The library tries to make sure your data is stored with the types best fit for the job.
In some cases, that means changing the default DDL that Hibernate uses for the SqlTypes.UUID
jdbc type.
Database | Database Type for UUID |
---|---|
PostgreSQL | uuid |
MySQL | binary(16) |
MariaDB 10.7+ | uuid |
MariaDB before 10.7 | binary(16) |
other | whatever Hibernate uses as the dialect default |
The library only sets the type if there is no JDBC type for SqlTypes.UUID
already set,
which means that if you want to use something different you should be able to do so using a custom org.hibernate.boot.model.TypeContributor
.
One of the goals of this library is to enable generated typed IDs in application code - specifically in entity constructors. Being able to generate identifiers in app code solves many problems around application design and architecture by getting rid of the dependency of entity on the database. The classic approach is to let the database generate the identifiers, which is perfectly fine if you prefer that, but it breaks entity state because until you persist them they're invalid and incomplete. But when you generate the ID at construction time, the entity is valid from the first moment.
The only way to do this reliably is to generate random identifiers so that you don't get conflicts when persisting the entities, but using perfectly random values has its problems...
The ObjectUuid.randomUUID()
generates UUIDv7 instead of Java's default UUIDv4.
The UUIDv4 is not well suited to be used in indexes and primary keys due to performance reasons.
The UUIDv7, while still larger than plain long
, does not suffer from the performance problems that UUIDv4 has, and can be safely used for primary keys.
In case you don't like the default generator, you can opt to replace it using ObjectUuid.Generators.setFactory(UuidGenerator.Factory)
.
It might come in handy in tests where you might want to use a deterministic generator.
Some additional resources:
- Illustrating Primary Key models in InnoDB and their impact on disk usage - Percona Blog
- GUID/UUID Performance Breakthrough - Rick James
- MySQL InnoDB Primary Key Choice: GUID/UUID vs Integer Insert Performance - KCCoder
- Unreasonable Defaults: Primary Key as Clustering Key - Use The Index, Luke
- SQL server full-text index and its stop words - Jiangong Sun
- MySQL UUIDs – Bad For Performance - Percona Blog
- Choose the right primary key to save a large amount of disk I/O - Too Many Afterthoughts
- UUID vs Bigint Battle! - Scaling Postgres 302
The base type is designed to wrap a native UUID, and allows you to expose any utility functions you may need. The following snippet is the standard boilerplate, but you may opt to skip some of the methods, or add a few custom ones.
public record User(Id id)
{
public static final class Id extends ObjectUuid<Id>
{
private Id(final UUID inner)
{
super(inner);
}
public static Id random()
{
return ObjectUuid.randomUUID(Id::new);
}
public static Id fromString(final String value)
{
return ObjectUuid.fromString(Id::new, value);
}
public static Id fromUuid(final UUID value)
{
return ObjectUuid.fromUuid(Id::new, value);
}
}
}
// ...
var user = new User(Id.random());
With Kotlin, the standard boilerplate should look like the following snippet
data class User(id: Id) {
class Id private constructor(id: UUID) : ObjectUuid<Id>(id) {
companion object {
fun random() = randomUUID(::Id)
fun from(value: String) = fromString(::Id, value)
fun from(value: UUID) = fromUuid(::Id, value)
}
}
}
Install the newest version of org.atteo.classindex:classindex, and register it as an annotation processor.
The classes of this library are annotated with @IndexSubclasses
, so once the classindex library is correctly installed,
you should see META-INF/services/org.framefork.typedIds.uuid.ObjectUuid
being populated somewhere in your build output directory.
The ObjectUuidTypesContributor
should then read it, and register the types automatically when Hibernate ORM is initialized.
When used with a Kotlin project, you might want to explicitly add also a kapt("org.atteo.classindex:classindex")
dependency.
Without the automatic registration, the field has to be annotated like this
@Entity
public class User
{
@jakarta.persistence.Id
@Column(nullable = false)
@Type(ObjectUuidType.class)
private Id id;
// ...
public static final class Id extends ObjectUuid<Id>
{
// ...
But with the class indexer installed, The system will know that the User.Id
should be handled by ObjectUuidType
and the @Type(...)
can be dropped.
This also simplifies usage on every other place, where Hibernate might need to resolve a type for the Id
instance, like queries.