Hybrid ID generation in Hibernate 6.2 is difficult

In Hibernate 6.2, it seems my ID generator can no longer work.

My generator uses an identity column/post-insert-id by default, but will use an explicitly supplied ID if entity.getExplicitId() returns non-null. This is handy when data is in the process of being migrated and the service can both insert new IDs as well as consume inserts (using supplied IDs) from another source.

The problem is that in 6.2, the way to choose between generation before execution (supplied ID) and generation after execution (post-insert) is via org.hibernate.generator.Generator.generatedOnExecution(). This method does not take the entity object as an argument, so the generator cannot decide at runtime which strategy to use. Is there another way to do it?

For reference, here is my generator implementation which was working in previous Hibernate versions:

public class ExplicitOrAutoGenerator extends IdentityGenerator {
    @Override
    public Serializable generate(SharedSessionContractImplementor s, Object obj) {
        if (obj instanceof ExplicitId entity && entity.getExplicitId() != null) {
            return entity.getExplicitId();
        }

        return IdentifierGeneratorHelper.POST_INSERT_INDICATOR;
    }
}

If I use it as-is (by implementing the new IdentifierGenerator interface), an insert with a post-insert ID just fails because it tries to call the entity ID setter with POST_INSERT_INDICATOR (which is not a Long or Integer obviously).

You can create a JIRA issue for this. Shouldnā€™t be too hard to fix this.

It affects me too.I created an account just to write this comment.

Did you create jira request? Could you attach link here?

I am affected as well, usecase:
When an entitiys id is present ā†’ use it when storing the entity.
If not: fall back to the databases auto_increment

I did it like this in the past (hibernate 6.1):

public class CustomIdentityGenerator extends IdentityGenerator {
    @Override
    public Object generate(SharedSessionContractImplementor sharedSessionContractImplementor, Object o) throws HibernateException {
        if (o instanceof TariffEntity) {
            TariffEntity t = (TariffEntity) o;
            if (t.getTariffId() != null && t.getTariffId() > 0) {
                return t.getTariffId();
            }
        }
        return super.generate(sharedSessionContractImplementor, o);
    }
}

Is there an alternate solution I could try? I tried implementing IdentifierGenerator, but that got me nowhere since indicating the fallback to the database is the problem (like above using IdentifierGeneratorHelper.POST_INSERT_INDICATOR)

This happened after upgrading Spring Boot 3.0 to 3.1, which translates to hibernate 6.1.7 to 6.2.2. The guide at 6.2 Migration Guide doesnt mention it, at least it is not obvious to me.

See [HHH-16692] - Hibernate JIRA

Ah ok, so a jira was indeed created, but untouched since may.

Also, please note that I am NOT using ā€œIdentifierGeneratorHelper.POST_INSERT_INDICATORā€, but calling super to invoke the database default id behavior.

If anyone else comes upon this issue like I did:
For me(!!!) it is enough to force spring boot 3.1 to use the old hibernate version that was shipped with spring boot 3.0 by using the follwing property in the projects pom:

<properties>
   <hibernate.version>6.1.7.Final</hibernate.version>
</properties>

This is not a fix!
This is barely a workaround!
Just because it works for me, does not mean it works for any other usecase.

I found a solution.

With

<properties>
    <hibernate.version>6.4.4.Final</hibernate.version>
</properties>

I can use

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);
    }

    /**
     * @implNote Method {@link #generate(SharedSessionContractImplementor, Object, Object, EventType)}
     * is called if this method returns false
     */
    @Override
    public boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) {
        Object id = getId(entity, session);
        return isNull(id);
    }

    @Override
    public boolean generatedOnExecution() {
        // This method is called to configure a context (using this Generator) without knowledge of a specific Entity.
        // The choice for the real Entity must be made in the this.generatedOnExecution(entity, session) method.
        // For example, find out comment "support mixed-timing generators" in IdentifierGeneratorUtil.class (hibernate-core):
        // true is required, if ID sometimes should be generated by RDBMS (for example by AUTO_INCREMENT)
        return true;
    }
}

This class completly replaces old CustomIdentityGenerator above, the rest of the code can remain the same. This class works for AUTO_INCREMENT / IDENTITY db columns.

If you want use TABLE, SEQUENCE, UUID id generation types you can use general class Iā€™ve described here. Then you can wrap required ā€œbeforeā€ and ā€œonā€ sql statement execution id generator implementations

class AssignedOrIdentityGenerator extends BeforeOrOnExecutionGenerator {
    public AssignedOrIdentityGenerator() {
        super(new Assigned(), new IdentityGenerator());
    }
}

With hibernate 6.4.4 you can also create simple annotation

@Retention(RUNTIME)
@Target({METHOD, FIELD})
@IdGeneratorType(AssignedOrIdentityGenerator.class)
public @interface AssignedOrGeneratedValue {
}

So you can replace this code snippet

@Id
@GeneratedValue(generator = "assigned-or-generated")
@GenericGenerator(name = "assigned-or-generated", type = AssignedOrIdentityGenerator.class)
@Column(name = "id")
private Integer id;

with a simpler one

@Id
@AssignedOrGeneratedValue
@Column(name = "id")
private Integer id;
1 Like

Please beware that allowing assigned values can lead to unique constraint violation errors i.e. when the underlying sequence is not updated correctly after an insert with assigned values, which is why we do not recommend this and donā€™t want to allow this out of the box.

IMO this is a broken mapping and will most probably bite you in production if youā€™re not careful. I can see the appeal to allow assigning fixed ids for test data, but then you should only configure this for your tests, not for your production app.

1 Like

Thank you vananiev, I tested your solution and it works great, but only for IDs which are Auto-incremented. I have plenty of such tables, so it is most appreciated.
Unfortunately for me, I have some cases where IDs are char(24) and get generated by database by pre-insert trigger which calls generate_24c_id function, which does only
RETURN SUBSTRING(SHA1(UUID()), 1, 24);
I found solution, but it is a bit involved, so iā€™m not going to spam here with all the required code, just explain what I did to make it work with MySQL 8.3.

  • created a table to hold last_id, with fields for table name and last_id, using memory engine
  • created post-insert trigger which writes NEW.id and table_name into this table
  • created stored procedure which takes all record fields as IN parameters and returns id read from the last_id table as OUT parameter
  • created interface to the repository with only ā€˜customSave()ā€™ method and add it to the default repository interface with ā€˜implementsā€™
  • created implementation of this CustomRepository, which calls database procedure using StoredProcedureQuery from autowired entity Manager, registers and sets all query parameters, executes the query and returns entity instance with Id set by getOutputParameterValue()

    This now works as i would expect @GeneratedValue(strategy = GenerationType.IDENTITY) would work.

1 Like

I backup DB data to JSON file to restore it later. Foregn keys uses Autoincrement IDs. If I donā€™t restore the original ID from JSON, the integrity of the data will be compromised. How do you recommend working with Hibernate in this case? Do you recommend creating another uniq column (second row identifier) to link rows between tables? Wouldnā€™t this be redundant?

You should rather use the backup and restore tool of the database for this task. That will also take care of the autoincrement sequence value to be correct.

1 Like