quarkusio / quarkus

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

Double Array Hibernate @JdbcTypeCode(SqlTypes.ARRAY) causes NullPointerException with 6.6.0.Final #43056

Open dhoffer opened 1 week ago

dhoffer commented 1 week ago

Describe the bug

We had previously upgraded our application to Quarkus 3.12.3 and the following entity mapping appeared to work fine, it did not cause errors at boot time.

@JdbcTypeCode(SqlTypes.ARRAY)
    @Column(name = "annLims")
    @Size(max = 10)
    @JsonView(Views.Abridged.class)
    private Integer[][] annLims;

However today I updated to Quarkus 3.14.2 which uses Hibernate 6.6.0.Final and the above entity mapping causes this fatal error at boot time.

Exception in thread "main" java.lang.ExceptionInInitializerError
        at java.base/jdk.internal.misc.Unsafe.ensureClassInitialized0(Native Method)
        at java.base/jdk.internal.misc.Unsafe.ensureClassInitialized(Unsafe.java:1160)
        at java.base/jdk.internal.reflect.MethodHandleAccessorFactory.ensureClassInitialized(MethodHandleAccessorFactory.java:300)
        at java.base/jdk.internal.reflect.MethodHandleAccessorFactory.newConstructorAccessor(MethodHandleAccessorFactory.java:103)
        at java.base/jdk.internal.reflect.ReflectionFactory.newConstructorAccessor(ReflectionFactory.java:200)
        at java.base/java.lang.reflect.Constructor.acquireConstructorAccessor(Constructor.java:549)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
        at io.quarkus.runtime.Quarkus.run(Quarkus.java:70)
        at io.quarkus.runtime.Quarkus.run(Quarkus.java:44)
        at com.bs.sdl.Main.main(Main.java:45)
Caused by: java.lang.RuntimeException: Failed to start quarkus
        at io.quarkus.runner.ApplicationImpl.<clinit>(Unknown Source)
        ... 11 more
Caused by: java.lang.NullPointerException
        at java.base/java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
        at java.base/java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
        at org.hibernate.type.BasicTypeRegistry.resolve(BasicTypeRegistry.java:217)
        at org.hibernate.type.BasicTypeRegistry.resolve(BasicTypeRegistry.java:158)
        at org.hibernate.boot.model.process.internal.InferredBasicValueResolver.from(InferredBasicValueResolver.java:199)
        at org.hibernate.mapping.BasicValue.resolution(BasicValue.java:647)
        at org.hibernate.mapping.BasicValue.buildResolution(BasicValue.java:498)
        at org.hibernate.mapping.BasicValue.resolve(BasicValue.java:350)
        at org.hibernate.mapping.BasicValue.resolve(BasicValue.java:340)
        at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.lambda$processValueResolvers$6(InFlightMetadataCollectorImpl.java:1827)
        at java.base/java.util.ArrayList.removeIf(ArrayList.java:1765)
        at java.base/java.util.ArrayList.removeIf(ArrayList.java:1743)
        at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processValueResolvers(InFlightMetadataCollectorImpl.java:1826)
        at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1812)
        at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:334)
        at io.quarkus.hibernate.orm.runtime.boot.FastBootMetadataBuilder.build(FastBootMetadataBuilder.java:410)
        at io.quarkus.hibernate.orm.runtime.PersistenceUnitsHolder.createMetadata(PersistenceUnitsHolder.java:101)
        at io.quarkus.hibernate.orm.runtime.PersistenceUnitsHolder.constructMetadataAdvance(PersistenceUnitsHolder.java:73)
        at io.quarkus.hibernate.orm.runtime.PersistenceUnitsHolder.initializeJpa(PersistenceUnitsHolder.java:40)
        at io.quarkus.hibernate.orm.runtime.HibernateOrmRecorder$1.created(HibernateOrmRecorder.java:78)
        at io.quarkus.arc.runtime.ArcRecorder.initBeanContainer(ArcRecorder.java:80)
        at io.quarkus.deployment.steps.ArcProcessor$notifyBeanContainerListeners1304312071.deploy_0(Unknown Source)
        at io.quarkus.deployment.steps.ArcProcessor$notifyBeanContainerListeners1304312071.deploy(Unknown Source)
        ... 12 more

Is this a regression in Hibernate? Or perhaps is there a new SqlTypes enum for double arrays? I did not see one.

Expected behavior

Expected no errors at boot time.

Actual behavior

java.lang.NullPointerException at boot time, stack trace is above.

How to Reproduce?

No response

Output of uname -a or ver

No response

Output of java -version

java 21.0.4 2024-07-16 LTS

Quarkus version or git rev

3.14.2

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

Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)

Additional information

No response

quarkus-bot[bot] commented 1 week ago

/cc @barreiro (jdbc), @gsmet (hibernate-orm), @yrodiere (hibernate-orm,jdbc)

dhoffer commented 1 week ago

I have confirmed this is a regression in Quarkus 3.14.2.

Not only does Quarkus boot with 3.12.3 but the JAX-RS & JPA layer works perfect with the entity annotations above. Specifically @JdbcTypeCode(SqlTypes.ARRAY) worked perfectly with double integer array data.

Also our annLims column is typed as _int4.

yrodiere commented 1 week ago

Hey,

Thanks for reporting.

Is this a regression in Hibernate?

Seems likely. Either that or a this is just a usecase that was not intended to work and happened to work by chance: I had a quick look and could not find tests about persisting arrays of arrays in Hibernate ORM.

What's this @JsonViews though? I suppose the problem appears even without that?

In any case, can you please set up a reproducer based on this and submit a bug report on the Hibernate Jira, linking it here?

dhoffer commented 1 week ago

The @JsonView is a Jackson thing that lets us customize what fields are returned to the user during the JAX-RS serialization process so you can ignore that, not relevant for this bug. The key is just to add a few columns to one of your existing Hibernate tests and just make sure that Hibernate can read/write the single & multi-dimensional arrays correctly. This worked perfectly in Hibernate 5.x and 6.x through Quarkus 3.12.3 (not sure which version broke it, but does not work in 3.14.2.

@JdbcTypeCode(SqlTypes.ARRAY)
@Column(name = "annLims")
@Size(max = 10)
private Integer[][] annLims;

(with Postgres DB column of _int4)

@JdbcTypeCode(SqlTypes.ARRAY)
@Column(name = "annText")
@Size(max = 256)
private String[] annText;

(with Postgres DB column of _varchar)

We use a lot of single arrays, String, Integer, Double, etc But for double arrays the above is the only one I found but should work with any data type.

dhoffer commented 1 week ago

Here is a reproducer, As soon as I added the double array field the test framework failed.

`@Entity @Table(name = "test", schema = "public") public class Foo { @Id @Column(name = "id", nullable = false, length = 36) private String id;

@Column(name = "bar")
private String bar;

@JdbcTypeCode(SqlTypes.ARRAY)
@Column(name = "bars")
private String[] bars;

@Column(name = "integer")
@JdbcTypeCode(SqlTypes.ARRAY)
private Integer[][] integers;

public String getId() {
    return id;
}

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

public String getBar() {
    return bar;
}

public void setBar(String bar) {
    this.bar = bar;
}

public String[] getBars() {
    return bars;
}

public void setBars(String[] bars) {
    this.bars = bars;
}

public Integer[][] getIntegers() {
    return integers;
}

public void setIntegers(Integer[][] integers) {
    this.integers = integers;
}

}`

JPAUnitTestCase test case

` @Test void hhh123Test() throws Exception {

    EntityManager entityManager = entityManagerFactory.createEntityManager();

    {
        String id = UUID.randomUUID().toString();
        {
            entityManager.getTransaction().begin();
            Foo foo = new Foo();
            foo.setId(id);
            foo.setBar("test");
            entityManager.persist(foo);
            entityManager.getTransaction().commit();
        }

        {
            entityManager.getTransaction().begin();
            Foo foo = entityManager.find(Foo.class, id);
            String actual = foo.getBar();
            Assert.assertEquals("test", actual);
            entityManager.getTransaction().commit();
        }
    }

    {
        String id = UUID.randomUUID().toString();
        String[] bars = {"1", "2", "3"};
        {
            entityManager.getTransaction().begin();
            Foo foo = new Foo();
            foo.setId(id);
            foo.setBars(bars);
            entityManager.persist(foo);
            entityManager.getTransaction().commit();
        }

        {
            entityManager.getTransaction().begin();
            Foo foo = entityManager.find(Foo.class, id);
            String[] actual = foo.getBars();
            Assert.assertArrayEquals(bars, actual);
            entityManager.getTransaction().commit();
        }
    }

    {
        String id = UUID.randomUUID().toString();
        Integer[][] expected = new Integer[2][2];
        {
            entityManager.getTransaction().begin();
            // Do stuff...
            Foo foo = new Foo();
            foo.setId(id);
            expected[0] = new Integer[2];
            expected[0][0] = 1;
            expected[0][1] = 2;
            expected[1] = new Integer[2];
            expected[1][0] = 3;
            expected[1][1] = 4;
            foo.setIntegers(expected);
            entityManager.persist(foo);
            entityManager.getTransaction().commit();
        }

        {
            entityManager.getTransaction().begin();
            Foo foo = entityManager.find(Foo.class, id);
            Integer[][] actual = foo.getIntegers();
            Assert.assertArrayEquals(expected[0], actual[0]);
            Assert.assertArrayEquals(expected[1], actual[1]);

            entityManager.getTransaction().commit();
        }

    }
    entityManager.close();
}`
yrodiere commented 1 week ago

Thank you. Reported upstream as https://hibernate.atlassian.net/browse/HHH-18582

yrodiere commented 6 days ago

@dhoffer Please head over to https://hibernate.atlassian.net/browse/HHH-18582 if you want to make your case, because mapping arrays of arrays is being described as a non-feature (not intended to work) and apparently can't be made to work on ORM 6.5 with your reproducer.

dhoffer commented 5 days ago

@yrodiere Okay I went to the hibernate link and made my case. It's always worked pre hibernate 6.6.0 and we have massive production databases using it so its a blocker for us.