After upgrading hibernate from v5 to v6, it is not possible to have columns with generated values that are not PK

Hi,

I have entity mapping

@Entity
@Table(name = "temp_file")
public class TempFile extends MyAppEntity {

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

    @GenericGenerator(name = "myapp_id", strategy = "MyAppIdGenerator")
    @GeneratedValue(generator = "myapp_id")
    @Generated(GenerationTime.INSERT)
    @Column(name = "uuid", insertable = false, nullable = false, unique = true, updatable = false)
    private String uuid;
    ...
}

This worked fine with Hibernate 5.6.15.Final, but now in Hibernate 6.5.2.Final it gives an error

Property 'TempFile.uuid' is annotated @GeneratedValue but is not part of an identifier

Does this mean that Hibernate 6.5.2.Final does not allow generated values beside PK?

Hi, the @GeneratedValue annotation must only be used for identifiers. Also, @GenericGenerator is deprecated, so I suggest using the new @ValueGenerationType meta-annotation to define a custom generation strategy that uses your generator. For example:

@ValueGenerationType( generatedBy = MyAppIdGenerator.class )
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE } )
@Inherited
public @interface GeneratedUuidValue {
	GenerationTiming timing();
}

[...]

@GeneratedUuidValue
@Column(name = "uuid", insertable = false, nullable = false, unique = true, updatable = false)
private String uuid;

Find out more in our user guide.

Thanks for the quick answer.
Isnā€™t GenerationTiming deprecated as well? I should probably use EventType?

Also, since our MyAppIdGenerator implements org.hibernate.id.IdentifierGenerator, it now throws error
Generator class 'MyAppIdGenerator' implements 'IdentifierGenerator' and may not be used with '@ValueGenerationType'

Isnā€™t GenerationTiming deprecated as well?

You can customize the annotation members as needed, that one happened to already be in the example I took from but you could put your own values (or none if you donā€™t need any configuration).

You only need your generator to implement BeforeExecutionGenerator with timing EventType.INSERT and it should work with both identifiers and normal values. IdentifierGenerator is a classic extension point from the early Hibernate days but itā€™s now superfluous (see also its javadoc).

Thanks for the answer, I managed to implement it easily. Itā€™s just EventType thatā€™s substitution for GenerationTiming.
Also, I figured out from the documentation that generator needs to implement BeforeExecutionGenerator.

The trickiest part is about adapting queries for new SQM. E.g.

(COALESCE(:fromTime, '') = '' OR al.createdAt >= :fromTime)

where both al.createdAt and parameter :fromTime are java.time.LocalDateTime, but itā€™s not working anymore, saying:

org.hibernate.query.SemanticException: Literal type 'class java.lang.String' did not match domain type 'java.time.LocalDateTime' nor converted type 'java.util.Date'

and thatā€™s due to SQM I guess, and itā€™s hard to find a (proper) solution for such cases (without explicit type casting).

The trickiest part is about adapting queries for new SQM. E.g.

(COALESCE(:fromTime, '') = '' OR al.createdAt >= :fromTime)

Why are you using coalesce(:fromTime, '') = '' here? That is not valid Hibernate syntax. If you know your parameter is going to be null, just donā€™t include the restriction in the query with i.e. dynamic Criteria APIs.

Anyway, if you want more specific help with this please create a new topic as itā€™s unrelated with the generator discussion.

1 Like

Iā€™m using

coalesce(:fromTime, '') = ''

because

(:fromTime) is null

produces an error (in both v5 and v6): could not determine data type of parameter.

Anyway, Iā€™m going to open another ticket for this one.
Thanks for helping!

Seems that I need to decorate the annotation with @IdGeneratorType in order to use it for PK field, cause having as you proposed

@ValueGenerationType( generatedBy = MyAppIdGenerator.class )
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE } )
@Inherited
public @interface GeneratedUuidValue {
	GenerationTiming timing();
}

throws an error on startup: ā€œProperty ā€˜idā€™ is annotated ā€˜interface GeneratedUuidValueā€™ which is not an ā€˜@IdGeneratorTypeā€™ā€.

So, should I:

  1. decorate the annotation with both @IdGeneratorType and @ValueGeneratorType
@IdGenerationType( MyAppIdGenerator.class )
@ValueGenerationType( generatedBy = MyAppIdGenerator.class )
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE } )
@Inherited
public @interface GeneratedUuidValue {
	GenerationTiming timing();
}
  1. decorate the annotation with only @IdGenerationType
@IdGenerationType( MyAppIdGenerator.class )
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE } )
@Inherited
public @interface GeneratedUuidValue {
	GenerationTiming timing();
}
  1. have different annotations for id and other generated fields
  2. something else
    ?

You can surely annotate with both @IdGeneratorType and @ValueGenerationType if you want to use the same annotation for both identifiers and normal values, see e.g. Hibernateā€™s @Generated annotation.

Thanks for the quick answer.
Iā€™ve checked @Generated annotation, and it has both @IdGenerationType and @ValueGenerationType, indeed (both using same generator).

Although, it seems that having only @IdGenerationType is enough for both, IDs and regular (generated) values.

Iā€™m sorry for reopening this ticket, but Iā€™ve just found an issue with new generators.
As already mentioned, Iā€™m using the same generator for both PK and regular field, so itā€™s decorated with both @IdGeneratorType and @ValueGenerationType

@IdGeneratorType(CascadesUuidValueGenerator.class)
@ValueGenerationType(generatedBy = CascadesUuidValueGenerator.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Inherited
public @interface GeneratedUuidValue {
    EventType eventType() default EventType.INSERT;
}

However, I have a problem with regular fields: as soon as repo.save() executes, entity gets populated its PK and regular guid value. But when transaction commits, PK value stays the same while regular field value gets changed, and I want them both, once generated, to stay the same.

PK mapping:

@Id
@GeneratedUuidValue
private String id;

Regular guid field mapping:

@GeneratedUuidValue
@Column(name = "uuid", insertable = false, nullable = false, unique = true, updatable = false)
private String uuid;

P.S.

  1. Update of the entity works fine, it does not automatically change this field on every save/update.
  2. When I change column parameter insertable to true, it starts working as expected - once generated, on repo.save(), it does not get changed when transaction commits. However this column definition is the same as it used to be while we were using hibernate v5, where it did not change on transaction commit. Where does the difference in behavior come from?
    This was column mapping from hibernate v5:
@GenericGenerator(name = "myapp_id", strategy = "MyAppIdGenerator")
@GeneratedValue(generator = "myapp_id")
@Generated(GenerationTime.INSERT)
@Column(name = "uuid", insertable = false, nullable = false, unique = true, updatable = false)
private String uuid;

But when transaction commits, PK value stays the same while regular field value gets changed, and I want them both, once generated, to stay the same

Hibernate has to execute the insert statement and retrieve generated primary keys as soon as the entity is persisted to maintain its identity in the persistence context. Generated values, however, only need to be retrieved once the state of the PC is flushed to the database.

Now, Iā€™m not sure why in your case the generated value is changing, since only calling org.hibernate.Session#persist() once on your entity instance should not cause generated values to be populated more than once. Since it appears youā€™re using Spring, that might be causing some unnecessary / additional processing when saving.

Yes, Iā€™m using Spring, and @Column annotation is jakarta.persistence annotation.
Ok, thanks for the answer, Iā€™ll continue from there.
I was just wondering if something changed on Hibernate side (between v5 and v6) that caused our existing column mapping to change behavior.