quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.44k stars 2.58k forks source link

NPE when fetching entity containing OneToOne property joined by multiple columns from the non-owning side #41700

Open DeKoxD opened 1 month ago

DeKoxD commented 1 month ago

Describe the bug

I started after I updated my project from Quarkus 3.8.5 to 3.12.0 (and .1), but this error also happens on 3.11.x. Every entity with a property mapped by OneToOne using @JoinColumns started failing to load from the non-owning side (the mappedBy side).

Example Entity A:

@Entity
@Table(name = "EntityA", uniqueConstraints = @UniqueConstraint(columnNames = {"columnAEntityA", "columnBEntityA"}))
public class EntityA extends BaseEntity {
    @Column(name = "columnAEntityA")
    public Long columnAEntityA;
    @Column(name = "columnBEntityA")
    public Long columnBEntityA;

    @OneToOne(mappedBy = "entityA")
    public EntityB entityB;
}

Example Entity B: Example:

@Entity
@Table(name = "EntityB", uniqueConstraints = @UniqueConstraint(columnNames = {"columnAEntityB", "columnBEntityB"}))
public class EntityB extends BaseEntity {
    @JoinColumn(name = "columnAEntityB")
    public Long columnAEntityB;
    @JoinColumn(name = "columnBEntityB")
    public Long columnBEntityB;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "columnAEntityB", referencedColumnName = "columnAEntityA", insertable = false, updatable = false),
            @JoinColumn(name = "columnBEntityB", referencedColumnName = "columnBEntityA", insertable = false, updatable = false)
    })
    public EntityA entityA;
}

Expected behavior

No response

Actual behavior

I get the following error:

2024-07-04 17:35:53,254 WARNING [cor.exc.han.RuntimeExceptionHandler] (executor-thread-13) Runtime exception handled: java.lang.NullPointerException: Cannot invoke "Object.hashCode()" because "value" is null
    at org.hibernate.type.descriptor.java.AbstractClassJavaType.extractHashCode(AbstractClassJavaType.java:93)
    at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:217)
    at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:226)
    at org.hibernate.type.EntityType.getHashCode(EntityType.java:362)
    at org.hibernate.engine.spi.EntityUniqueKey.generateHashCode(EntityUniqueKey.java:67)
    at org.hibernate.engine.spi.EntityUniqueKey.<init>(EntityUniqueKey.java:48)
    at org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchByUniqueKeyInitializer.initializeInstance(EntitySelectFetchByUniqueKeyInitializer.java:91)
    at org.hibernate.sql.results.internal.InitializersList.initializeInstance(InitializersList.java:73)
    at org.hibernate.sql.results.internal.StandardRowReader.coordinateInitializers(StandardRowReader.java:113)
    at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:87)
    at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:205)
    at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
    at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:211)
    at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:83)
    at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:76)
    at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:65)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$2(ConcreteSqmSelectQueryPlan.java:139)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:382)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:302)
    at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:526)
    at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:423)
    at org.hibernate.query.Query.getResultList(Query.java:120)
    at io.quarkus.hibernate.orm.panache.common.runtime.CommonPanacheQueryImpl.firstResult(CommonPanacheQueryImpl.java:316)
    at io.quarkus.hibernate.orm.panache.kotlin.runtime.PanacheQueryImpl.firstResult(PanacheQueryImpl.kt:123)
    ...

How to Reproduce?

No response

Output of uname -a or ver

Linux pop-os 6.8.0-76060800daily20240311-generic #202403110203~1710198088~22.04~1a3dbc7 SMP PREEMPT_DYNAMIC Mon M x86_64 x86_64 x86_64 GNU/Linux

Output of java -version

openjdk version "21.0.2" 2024-01-16 LTS OpenJDK Runtime Environment Temurin-21.0.2+13 (build 21.0.2+13-LTS) OpenJDK 64-Bit Server VM Temurin-21.0.2+13 (build 21.0.2+13-LTS, mixed mode, sharing)

Quarkus version or git rev

3.12.0

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

Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)

Additional information

As far as I dived in, I can see when it loads EntityA, it initializes its relations (EntitySelectFetchInitializer::initializeInstance). It tries to generate a key (EntityUniqueKey) for the inverse relation by passing a blank instance of the entity (EntityA<null>), where it tries to generate a hashCode (EntityUniqueKey::hashCode) from the null id. Also, it now creates a EntitySelectFetchByUniqueKeyInitializer instead of a EntityDelayedFetchInitializer like before for entityA (see InitializersList).

If I have both @OneToOne with @JoinColumns on both sides (EntityA and EntityB), the error does not happen.

quarkus-bot[bot] commented 1 month ago

/cc @geoand (kotlin)

geoand commented 1 month ago

cc @yrodiere

yrodiere commented 1 month ago

Hi,

Do you have any reason to believe this problem is caused by Quarkus itself?

If not, could you please create a reproducer based on https://github.com/hibernate/hibernate-test-case-templates/blob/main/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/QuarkusLikeORMUnitTestCase.java, and report this on the Hibernate JIRA (unless it's already fixed in Hibernate ORM 6.6.0.CR1)?

Thank you.

DeKoxD commented 1 month ago

I am not certain if it is or is not caused by Quarkus, so I generated two projects and added tests finding EntityA (non-owning) and EntityB (owning) separately:

onetoone-bug-test.zip

Actually, I was going back testing some older Quarkus versions down to 3.7.4 and this error still happens, so I am not sure why I am only seeing it now. Anyway, it is breaking using Quarkus but it is not when using Hibernate only.

Thanks

yrodiere commented 3 weeks ago

Thanks for the reproducers.

Anyway, it is breaking using Quarkus but it is not when using Hibernate only.

There's a difference between your Quarkus reproducer and the Hibernate-only reproducer: you're not using generated IDs on the Hibernate-only reproducer.

Adding @GeneratedValue to BaseEntity#id (and adjusting test setup/cleanup as well as ID expectations) leads to identical result the Hibernate-only reproducer.

So... this problem does indeed stem from a bug in Hibernate ORM.

yrodiere commented 2 weeks ago

Reported upstream as https://hibernate.atlassian.net/browse/HHH-18390