quarkus: Quarkus 3/Hibernate ORM 6: entity inheritance does not work with Kotlin

Describe the bug

Using Hibernate entity inheritance with Kotlin works fine with Quarkus 2 but not with Quarkus 3.

I’ve created a repo that reproduce the bug: https://github.com/cthiebault/quarkus-hibernate-kotlin

Use the quarkus2 and quarkus3 branches to reproduce the issue.

Here are the entities

@Entity
@Table(name = "shape")
@Inheritance(strategy = InheritanceType.JOINED)
abstract class Shape(

  @Id
  @JdbcTypeCode(SqlTypes.VARCHAR)
  @Column(name = "id", updatable = false, nullable = false, unique = true)
  open val id: UUID,

  @Length(max = 100)
  @Column(name = "name", nullable = false, length = 100)
  open var name: String,

  @Enumerated(EnumType.STRING)
  @Column(name = "color", nullable = false, length = 50)
  open var color: Color,

  @Embedded
  var properties: Properties?,
)
@Entity
@Table(name = "rectangle")
@PrimaryKeyJoinColumn(name = "shape_id")
data class Rectangle(
  override val id: UUID,
  override var name: String,
  override var color: Color,
  override var properties: Properties?
) : Shape(id, name, color, properties) 
@ApplicationScoped
class ShapeRepository : PanacheRepositoryBase<Shape, UUID>

And the test I run

  @Test
  @Transactional
  fun test() {

    val rectangle = Rectangle(
      id = UUID.randomUUID(),
      name = "Rectangle",
      color = Color.Red,
      properties = Properties("foo", "bar")
    )

    Log.info("rectangle: $rectangle")
    repository.persist(rectangle)

    val found = repository.findById(rectangle.id)
    Log.info("found: $found")

    assertEquals(rectangle, found)
    assertEquals(1, repository.count())
  }

The error with Qurakus 3:

INFO: rectangle: Rectangle(id=7c0863fb-a786-4dd7-b9f1-c3b3f48fc0d8, name=Rectangle, color=Red, properties=Properties(foo=foo, bar=bar))

INFO: found: Rectangle(id=7c0863fb-a786-4dd7-b9f1-c3b3f48fc0d8, name=Rectangle, color=Red, properties=Properties(foo=foo, bar=bar))
ERROR: ERROR: null value in column "name" of relation "shape" violates not-null constraint
  Detail: Failing row contains (7c0863fb-a786-4dd7-b9f1-c3b3f48fc0d8, null, null, null, null).

could not execute statement [ERROR: null value in column "name" of relation "shape" violates not-null constraint
  Detail: Failing row contains (7c0863fb-a786-4dd7-b9f1-c3b3f48fc0d8, null, null, null, null).] [insert into shape (color,name,bar,foo,id) values (?,?,?,?,?)]
org.hibernate.exception.ConstraintViolationException: could not execute statement [ERROR: null value in column "name" of relation "shape" violates not-null constraint
  Detail: Failing row contains (7c0863fb-a786-4dd7-b9f1-c3b3f48fc0d8, null, null, null, null).] [insert into shape (color,name,bar,foo,id) values (?,?,?,?,?)]
	at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:95)
	at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:56)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:108)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:278)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:108)
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorStandard.lambda$performNonBatchedOperations$1(MutationExecutorStandard.java:217)
	at java.base@17.0.7/java.util.TreeMap.forEach(TreeMap.java:1282)
	at org.hibernate.engine.jdbc.mutation.internal.PreparedStatementGroupStandard.forEachStatement(PreparedStatementGroupStandard.java:90)
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorStandard.performNonBatchedOperations(MutationExecutorStandard.java:217)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:53)
	at org.hibernate.persister.entity.mutation.InsertCoordinator.doStaticInserts(InsertCoordinator.java:170)
	at org.hibernate.persister.entity.mutation.InsertCoordinator.coordinateInsert(InsertCoordinator.java:112)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2656)
	at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:102)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:616)
	at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:487)
	at java.base@17.0.7/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:484)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:358)
	at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:55)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1375)
	at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$0(ConcreteSqmSelectQueryPlan.java:107)
	at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:302)
	at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:243)
	at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:521)
	at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:367)
	at org.hibernate.query.sqm.internal.QuerySqmImpl.list(QuerySqmImpl.java:1084)
	at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:461)
	at org.hibernate.query.sqm.internal.QuerySqmImpl.getSingleResult(QuerySqmImpl.java:1110)
	at io.quarkus.hibernate.orm.panache.common.runtime.AbstractJpaOperations.count(AbstractJpaOperations.java:323)
	at org.acme.ShapeRepository.count(ShapeRepository.kt)
	at org.acme.ShapeRepository_ClientProxy.count(Unknown Source)
	at org.acme.ShapeTest.test(ShapeTest.kt:43)

Expected behavior

No response

Actual behavior

No response

How to Reproduce?

https://github.com/cthiebault/quarkus-hibernate-kotlin

Output of uname -a or ver

No response

Output of java -version

No response

GraalVM version (if different from Java)

No response

Quarkus version or git rev

No response

Build tool (ie. output of mvnw --version or gradlew --version)

No response

Additional information

Gradle 8.1.1 Build time: 2023-04-21 12:31:26 UTC Revision: 1cf537a851c635c364a4214885f8b9798051175b

Kotlin: 1.8.10 Groovy: 3.0.15 Ant: Apache Ant™ version 1.10.11 compiled on July 10 2021 JVM: 17.0.7 (Eclipse Adoptium 17.0.7+7) OS: Linux 6.1.30-1-MANJARO amd64

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Comments: 21 (11 by maintainers)

Most upvoted comments

Not in 3.2.0, since core artifacts have already been released. It’ll be fixed next time we upgrade Hibernate ORM in Quarkus. Probably in 3.2.1/3.2.2.

You would need a backport of HHH-16593 to Hibernate ORM 6.2, which from what I understand is unlikely unless you have a support contract: https://hibernate.org/orm/releases/6.2/ , https://hibernate.org/community/maintenance-policy/#levels . You can always ask the Hibernate ORM team though 🤷

As for Quarkus, this will get fixed when we upgrade to Hibernate ORM 6.3/6.4, which I’m currently working on. So this should hopefully be in Quarkus 3.6, or Quarkus 3.7 if we don’t get a Hibernate Reactive release in time.

Can we get this functionality merged in Quarkus 3.2.8 Final?

@cthiebault, thanks again for reporting a problem. As it turned out, it is a different bug from the one about Kotlin. I’ve created this ticket for it: https://hibernate.atlassian.net/browse/HHH-16799 I couldn’t see any workarounds for you, so it seems you’d need to stay on 3.1.0 for now.

Yes, sure. Will do 👍🏻😃

@cthiebault As stated above, this is a bug in Hibernate ORM.

If things still don’t work, or are getting worse, I’d recommend reaching out to the Hibernate ORM team, in particular to assist in analyzing the causes on the Kotlin side of things. Christian asked for just that here.

Also tested your suggestion about @Access(AccessType.PROPERTY) – that helps:

Glad to hear that.

FWIW, you should be able to put the annotation at the type level:

@Entity
@Table(name = "shape")
@Inheritance(strategy = InheritanceType.JOINED)
@Access(AccessType.PROPERTY)
abstract class Shape(

  @Id
  @JdbcTypeCode(SqlTypes.VARCHAR)
  @Column(name = "id", updatable = false, nullable = false, unique = true)
  open val id: UUID,

  @Length(max = 100)
  @Column(name = "name", nullable = false, length = 100)
  open var name: String,

  @Enumerated(EnumType.STRING)
  @Column(name = "color", nullable = false, length = 50)
  open var color: Color,

  @Embedded
  var properties: Properties?,

  )

Created https://hibernate.atlassian.net/browse/HHH-16735 ( reported earlier https://hibernate.atlassian.net/browse/HHH-15874)

Also tested your suggestion about @Access(AccessType.PROPERTY) – that helps:

@Entity
@Table(name = "shape")
@Inheritance(strategy = InheritanceType.JOINED)
abstract class Shape(

  @Id
  @JdbcTypeCode(SqlTypes.VARCHAR)
  @Column(name = "id", updatable = false, nullable = false, unique = true)
  open val id: UUID,

  @Length(max = 100)
  @Column(name = "name", nullable = false, length = 100)
  @Access(AccessType.PROPERTY)
  open var name: String,

  @Enumerated(EnumType.STRING)
  @Column(name = "color", nullable = false, length = 50)
  @Access(AccessType.PROPERTY)
  open var color: Color,

  @Embedded
  var properties: Properties?,

  )

makes the test pass.

would be a part that would need some adjustments if we’d want to support this case in ORM.

Can you please create an issue in Hibernate ORM? Though I’m not sure this will get a high priority given Hibernate ORM mostly targets Java and not Kotlin…

I would guess this has to do with Kotlin’s property override? Most likely field access won’t work well in such case, since, well… Kotlin probably just never uses the parent class’ field.

I’d suggest trying to avoid field override and seeing where this leads.

Failing that, putting @Access(AccessType.PROPERTY) on Shape or Rectangle could be a valid workaround, assuming Kotlin goes generate getters and setters (getId(), etc.). Someone would have to try, though.