Jon Tirsen
Jon Tirsen

Reputation: 4990

Manually set auto generated id property

In Hibernate is there a way to skip generating the value of a @GeneratedValue property by manually setting it before calling save on the session?

I am building an import/export facility and I would like to preserve the IDs from a previously made export.

Upvotes: 2

Views: 6860

Answers (5)

Alexander Tumin
Alexander Tumin

Reputation: 1591

With hibernate 6.5+ several things are required:

  • A custom org.hibernate.generator.Generator implementation.
  • A custom generator annotation, annotated in turn with org.hibernate.annotations.IdGeneratorType for @Id fields to indicate which generator you want to use.
  • A custom org.hibernate.Interceptor implementation to provide a hint to hibernate to use insert instead of update even when identifier is non-null.
  • When spring-data repositories are used, all entities which can have manually set ids must also implement org.springframework.data.domain.Persistable<ID> interface to provide a hint to spring-data to use em.persist(entity) instead of em.merge(entity) even when identifier is non-null.

Examples will assume java 14+.

Generator implementation

In recent hibernate versions there are two major kinds of ID generators:

  • BeforeExecutionGenerator -- one that provides values before insert query is executed, e.g. random UUID generator
  • OnExecutionGenerator -- one that provides values after insert query is executed, e.g. trigger/default value generated on the database side and exposed via returns * or similar mechanism.

Every generator must implement EnumSet<EventType> getEventTypes() which will define when id generator will be used, in this scenario INSERT_ONLY is most suitable. If you're extending UuidGenerator, IdentityGenerator, or other predefined generators, it may already be provided.

BeforeExecutionGenerator

BeforeExecutionGenerator family requires implementing/overriding

Object generate(
    SharedSessionContractImplementor session,
    Object owner,
    Object currentValue,
    EventType eventType
);

returning manually set or generated id value for owner (aka entity). In case existing implementation is overridden, you can simply defer to super.generate(..) when manually-set id is not detected.

An example based on UuidGenerator, assuming java.util.UUID entity fields:

public class MyUuidGenerator extends UuidGenerator {
    public MyUuidGenerator() {
        super(UUID.class);
    }

    @Override
    public Object generate(
        SharedSessionContractImplementor session,
        Object owner,
        Object currentValue,
        EventType eventType
    ) {
        Object id = session.getEntityPersister(null, owner)
            .getIdentifierMapping()
            .getIdentifier(owner);

        if (id != null) {
            return id;
        }

        return super.generate(session, owner, currentValue, eventType);
    }
}

OnExecutionGenerator

OnExecutionGenerator is more complicated, since you actually must also implement BeforeExecutionGenerator in order to provide manually-set id and a bunch of methods:

  • boolean referenceColumnsInSql(Dialect dialect) -- returning true to ensure id column is always present in insert queries
  • boolean writePropertyValue() -- returning true to ensure id value is always present in insert queries
  • String[] getReferencedColumnValues(Dialect dialect) -- returning keyword/special value that will prompt database (or a trigger) to generate id when it is not manually set / null. Most dialects use default keyword to make use of column default value even when it is explicitly inserted, mysql and postgresql included. Dialects without similar keyword/default values may require something like null and a trigger to handle id generation on database side.
  • boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) -- returning false when manually set id is detected on entity, indicating that generate(..) method must be used, and true otherwise.
  • Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) -- returning manually set id extracted from owner (aka entity) when generatedOnExecution returned false.

If you're extending IdentityGenerator and implementing BeforeExecutionGenerator interface, you would also have to resolve inheritance diamonds (super classes/interfaces both providing different default implementations of the same method signature) in favor of IdentityGenerator.

An example extending IdentityGenerator, assuming you have a dialect with default keyword:

public class MyIdentityGenerator extends IdentityGenerator implements BeforeExecutionGenerator {
    @Override
    public boolean referenceColumnsInSql(Dialect dialect) {
        return true;
    }

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

    @Override
    public String[] getReferencedColumnValues(Dialect dialect) {
        return new String[] { "default" };
    }

    @Override
    public boolean generatedOnExecution(
        Object entity,
        SharedSessionContractImplementor session
    ) {
        Object id = session.getEntityPersister(null, entity)
            .getIdentifierMapping()
            .getIdentifier(entity);

        if (id != null) {
            return false;
        }

        return super.generatedOnExecution(entity, session);
    }

    @Override
    public Object generate(
        SharedSessionContractImplementor session,
        Object owner,
        Object currentValue,
        EventType eventType
    ) {
        return session.getEntityPersister(null, owner)
            .getIdentifierMapping()
            .getIdentifier(owner);
    }

    // resolve diamond inheritence in favor of IdentityGenerator
    @Override
    public EnumSet<EventType> getEventTypes() {
        return super.getEventTypes();
    }

    // resolve diamond inheritence in favor of IdentityGenerator
    @Override
    public String[] getUniqueKeyPropertyNames(EntityPersister persister) {
        return super.getUniqueKeyPropertyNames(persister);
    }

    // resolve diamond inheritence in favor of IdentityGenerator
    @Override
    public boolean generatedOnExecution() {
        return super.generatedOnExecution();
    }

    // resolve diamond inheritence in favor of IdentityGenerator
    @Override
    public void configure(
        Type type,
        Properties parameters,
        ServiceRegistry serviceRegistry
    ) {
        super.configure(type, parameters, serviceRegistry);
    }
}

Generator annotation

Previous hibernate versions used @GeneratedValue/@GenericGenerator (now deprecated) for the same purpose. Now you must provide your own annotation and annotate it in turn with meta-annotation @IdGeneratorType

@IdGeneratorType(MyIdentityGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, METHOD })
public @interface MyIdentityGenerated {
}

or

@IdGeneratorType(MyUuidGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, METHOD })
public @interface MyUuidGenerated {
}

Then use it on a @Id field:

@Id
@MyIdentityGenerated
@Column(name = "id", unique = true, nullable = false)
private Long id;

or

@Id
@MyUuidGenerated
@Column(name = "id", unique = true, nullable = false)
private UUID id;

Interceptor implementation

Only Boolean isTransient(Object entity) method is required, which should return either true (new entity), false (already saved entity) or null to allow hibernate to try few other ways of detecting whether entity is new and requires insert on save instead of update.

One way is to provide some base marker type for entities that should support manually set ids - either implement a common interface, or extend a common [abstract] @MappedSuperclass with known method indicating new entities, so interceptor could attempt casting Object entity to that type.

To detect whether entity is new, it is possible to add a @Transient boolean field, which would be updated in some @PostLoad @PostPersist method.

@MappedSuperclass
public abstract class MyBaseEntity<T> {
    @Transient
    private boolean pristine = true;

    @PostLoad
    @PostPersist
    public void tarnish() {
        pristine = false;
    }

    public boolean isNew() {
        return pristine;
    }
}

Now make use of that common base type in the interceptor implementation:

public class MyInterceptor implements Interceptor {
    @Override
    public Boolean isTransient(Object entity) {
// if spring-data is used, it is better to check on interface instead
// see next section for Persistable implementation
//        if (entity instanceof Persistable p) {
        if (entity instanceof MyBaseEntity<?> p) {
            return p.isNew();
        }

        return null;
    }
}

And then reference the interceptor in jpa properties:

Properties p = new Properties();
...
p.setProperty("hibernate.session_factory.interceptor", "com.example.MyInterceptor")
...

Or use spring application.properties:

spring.jpa.properties.hibernate.session_factory.interceptor=com.example.MyInterceptor

Spring-data Persistable implementation

Extending example above, simply move @Id field to that MyBaseEntity and make it implements Persistable<T> -- add getter/setters/equals/hashcode for the id field and an @Override to isNew()

@MappedSuperclass
public abstract class MyBaseEntity<T> implements Persistable<T> {
    @Transient
    private boolean pristine = true;

    @Id
    @MyIdentityGenerated
    @Column(name = "id", unique = true, nullable = false)
    private Long id;

    @PostLoad
    @PostPersist
    public void tarnish() {
        pristine = false;
    }

    @Override
    public boolean isNew() {
        return pristine;
    }

    @Override
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        MyBaseEntity that = (MyBaseEntity) o;

        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

or implement Persistable directly on each of your entities that should support manually-set ids.

Upvotes: 0

Luiz Rossi
Luiz Rossi

Reputation: 892

Is important to say, if you use this way:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

And you don't have the id you set in the database it will ignore your id set and the database will create a new one. I mean, if you have an empty database and set user.setId(100L) and save, when you find it in database the service.findById(100L) will not find any object.

Upvotes: 2

Jon Tirsen
Jon Tirsen

Reputation: 4990

The problem is that I was using the default (AUTO) strategy for the @GeneratedValue. If I change it to explicitly specify IDENTITY then it works!

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

I don't really understand why though... :-)

Upvotes: 0

Raman Sahasi
Raman Sahasi

Reputation: 31841

@GeneratedValue will only apply it's strategy if the id on which you're applying this annotation is null/0.

This means that if you've manually assigned an id field, then @GeneratedValue annotation is USELESS.

Upvotes: 0

Alexey Soshin
Alexey Soshin

Reputation: 17691

By default, Hibernate won't override value if it was already set, so everything should work as you expect. Just make sure you're not using AUTO.

If you need a more complex logic, take a look at @GenericGenerator. https://docs.jboss.org/hibernate/stable/annotations/reference/en/html_single/#entity-hibspec-identifier

Upvotes: 0

Related Questions