Reputation: 4990
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
Reputation: 1591
With hibernate 6.5+ several things are required:
org.hibernate.generator.Generator
implementation.org.hibernate.annotations.IdGeneratorType
for @Id
fields to indicate which generator you want to use.org.hibernate.Interceptor
implementation to provide a hint to hibernate to use insert
instead of update
even when identifier is non-null.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+.
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 generatorOnExecutionGenerator
-- 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
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
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 queriesboolean writePropertyValue()
-- returning true
to ensure id value is always present in insert queriesString[] 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);
}
}
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;
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
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
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
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
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
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