Custom ID Generator in Hibernate 6: Use existing ID or fall back to auto-increment

I’m upgrading my application to Java 21 and Spring Boot 3.5.x, which brought in Hibernate 6 as a dependency. As part of this upgrade, my existing custom ID generator broke and I need to rewrite it to be compatible with Hibernate 6.

  • If the entity already has an ID set → use it

  • If the ID is null → let the database auto-generate one (like IDENTITY / AUTO_INCREMENT)

public class UseExistingOrGenerateIdGenerator extends IdentityGenerator 
                                               implements IdentifierGenerator {
    @Override
    public Serializable generate(SharedSessionContractImplementor session, 
                                 Object object) throws HibernateException {
        Serializable id = session.getEntityPersister(null, object)
                .getClassMetadata().getIdentifier(object, session);
        return id != null ? id : super.generate(session, object);
    }
}

What I need:

A Hibernate 6 equivalent that:

  1. Reads the current ID from the entity

  2. If not null → use it

  3. If null → delegate to the database AUTO_INCREMENT (not a Hibernate-managed sequence)

Environment:

  • Hibernate 6.x

  • Spring Boot 3.5.14

  • MySQL with AUTO_INCREMENT columns

Why I cannot use SequenceStyleGenerator as fallback:

My tables already have existing data with non-sequential IDs. For example:

users table:       max id 300
products table:    max id 50 000
orders table:      max id 120 000

If I use a shared sequence, it would start all tables at 120 000+, which is wrong. Each table needs its own AUTO_INCREMENT to continue naturally from its own max ID.

If you use a sequence generator, you wouldn’t have a single sequence, but rather a sequence per table/entity, for which you can configure the next id value. So there is no need to use a single number space for all tables.

You can also use a generator like this though if you prefer to stick to auto_increment:

public static class IdentityOrAssignedGenerator extends IdentityGenerator implements IdentifierGenerator {
    @Override
    public Object generate(SharedSessionContractImplementor session, Object object) {
       final EntityPersister entityPersister = session.getEntityPersister( null, object );
       return entityPersister.getIdentifier( object, session );
    }

    @Override
    public boolean generatedOnExecution() {
       return true;
    }

    @Override
    public boolean generatedOnExecution(Object owner, SharedSessionContractImplementor session) {
       return generate( session, owner, null, null ) == null;
    }

    @Override
    public boolean allowAssignedIdentifiers() {
       return true;
    }

    @Override
    public EnumSet<EventType> getEventTypes() {
       return INSERT_ONLY;
    }

    @Override
    public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) {
    }
}

Thanks for the reply. The challenge I’m facing is that both super.generate(session, object) and getClassMetadata() have been removed in Hibernate 6. My previous implementation relied on extending IdentityGenerator and delegating to super.generate(...) when the entity ID was null.

Do you have any recommendations on how to implement a custom generator in Hibernate 6 that supports the following behavior with an auto-increment (IDENTITY) column?

  • Use the existing ID if one is already assigned to the entity.
  • Otherwise, let Hibernate/database generate the next auto-increment value.

I also couldn’t find any guidance on this specific migration path in the Hibernate 6 migration guides or documentation. If there is a recommended replacement pattern or example for this use case, could you please point me to it?

Any guidance would be greatly appreciated.

I didn’t try this myself, but the code is an excerpt from a test we have on the 6.6 branch: hibernate-orm/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/MixedTimingGeneratorsTest.java at 6.6 · hibernate/hibernate-orm · GitHub

I don’t know why you need getClassMetadata, but the generate(SharedSessionContractImplementor, Object) method was not removed.

Did you try the generator I posted? I’m sure it will solve your problem. If not, please post what you tried and what error you faced.

public class CustomIdentityGenerator extends IdentityGenerator implements BeforeExecutionGenerator {
    @Override
    public Object generate(
            SharedSessionContractImplementor session,
            Object entity,
            Object currentValue,
            EventType eventType) {
        return getId(entity, session);
    }

    private static Object getId(Object entity, SharedSessionContractImplementor session) {
        return session.getEntityPersister(null, entity)
                .getIdentifier(entity, session);
    }

    @Override
    public boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) {
        return getId(entity, session) == null;
    }

    @Override
    public boolean generatedOnExecution() {
        return true;
    }

    @Override
    public boolean allowAssignedIdentifiers() {
        return true;
    }

    @Override
    public EnumSet<EventType> getEventTypes() {
        return INSERT_ONLY;
    }
}

This works correctly when the ID is not set and Hibernate uses the database auto-increment value.

However, when I explicitly set an ID before saving the entity, the insert succeeds but Hibernate throws the following exception:

org.hibernate.HibernateException:
The database returned no natively generated values

It looks like Hibernate is still expecting generated keys from the database even though an identifier was already assigned.

All of my entities extend the following base class:

@MappedSuperclass
public abstract class Persistent implements Serializable {

    @Version
    @Column(name = "VERSION", nullable = false)
    protected Long version;

    public Long getVersion() {
        return version;
    }

    public void setVersion(Long version) {
        this.version = version;
    }
}

I am wondering whether the @Version field could be related to the issue.Could Hibernate be treating the entity differently because the version is null? Is there any interaction between @Version and the custom identifier generator that could cause Hibernate to still expect a generated value from the database?

That’s maybe a bug, yeah. The presence of a non-null version make Hibernate ORM assume the entity is detached, whereas it being null will make it assume the entity is new. Maybe there is a bug when optimistic locked entities are involved.
Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

Yes, I believe this is a bug. I tried both suggested approaches, but I still encounter the following exception:
org.hibernate.PropertyValueException: Detached entity with generated id '9' has an uninitialized version value 'null'
The issue appears to be related to entities that use optimistic locking (@Version), where Hibernate treats the entity as detached .
I would like to create a bug ticket, but unfortunately I am unable to access Jira. The link you provided does not seem to be working for me. Is there an alternative way to report the issue, such as creating a GitHub issue? If needed, I can also provide a minimal reproducer.

The problems with Jira were reported a couple of times before. Can you please try what is described here: 7.2.1: schema validation fails for not-null database field mapped with @CreationTimestamp/@UpdateTimestamp - #6 by az264