Search code examples
javaspring-boothibernate-envershibernate-6.x

Migration Hibernate 6


I try to migrate a SpringBoot application to SpringBoot 3. Sprinboot 3 use Hibernate 6. My application refuse to start because of the following error

Caused by: java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" because the return value of "java.util.Map.get(Object)" is null
at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.addJoins(AuditMetadataGenerator.java:206)
at org.hibernate.envers.configuration.internal.metadata.AuditMetadataGenerator.generateSecondPass(AuditMetadataGenerator.java:409)
at org.hibernate.envers.configuration.internal.EntitiesConfigurator.configure(EntitiesConfigurator.java:86)
at org.hibernate.envers.boot.internal.EnversServiceImpl.initialize(EnversServiceImpl.java:129)
at org.hibernate.envers.boot.internal.AdditionalJaxbMappingProducerImpl.produceAdditionalMappings(AdditionalJaxbMappingProducerImpl.java:92)
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:329)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1350)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1421)
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:66)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:376)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:352)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1797)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1747)
... 110 common frames omitted

After digging in the Envers code it appear that the problem is located in the org.hibernate.envers.configuration.internal.metadata.AuditMetadaGenerator class.

At line 337, there is a condition that let a audited class be referenced during the first pass of envers .

if ( entity.isJoinAware() ) {
        final JoinAwarePersistentEntity joinAwareEntity = (JoinAwarePersistentEntity) entity;
        createJoins( persistentClass, joinAwareEntity, auditingData );
        addJoins( persistentClass, propertyMapper, auditingData, persistentClass.getEntityName(), mappingData, true );
}

private void createJoins(PersistentClass persistentClass, JoinAwarePersistentEntity entity, ClassAuditingData auditingData) {
    final Iterator<org.hibernate.mapping.Join> joins = persistentClass.getJoinIterator();
    final Map<org.hibernate.mapping.Join, Join> joinElements = new HashMap<>();
    entityJoins.put( persistentClass.getEntityName(), joinElements );
    ....

This is this list that is called during the second pass line 206.

while ( joins.hasNext() ) {
        final org.hibernate.mapping.Join join = joins.next();
        final Join entityJoin = entityJoins.get( entityName ).get( join );

Here entityJoins.get(entityName) return null for one of my entity.

This entity is correctly annotated with @Audited and extend from another Entity like this:

@Entity
@Table(name = "a")
@Audited
@DiscriminatorValue("DISCRIMINATOR")
public class A extends B {
   //...
}

@Entity
@Table(name = "b")
@Inheritance(strategy = InheritanceType.JOINED)
@Audited
public abstract class B {
   //...
}

In my comprehension, specifying the Inheritance with a value of InheritanceType.JOINED make envers create a JoinedSubclassPersistentEntity that itself inherit from PersistentEntity.

This PersistentEntity has a method :

public boolean isJoinAware() {
    return false;
}

that is overriden by it's children like RootPersistentEntity by :

@Override
public boolean isJoinAware() {
    return true;
}

This override is not made by JoinedSubclassPersistentEntity which is the class generated when using Joined inheritence strategy.

This cause my entity to not be add to the first pass but still processed by the second pass.

So the question is ? Is it a bug in Envers ? Can i used Joined inheritence strategy with a @Audited class ?

It was working well in hibernate 5.6.

[Edit]: I managed to reproduce the error with a simple test class :

@Entity
@Audited
@DiscriminatorValue("OPTION")
public class Child extends Parent{

    private String propA;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(name = "recursive_children_child", joinColumns = {@JoinColumn(name = "child_id", nullable = false, updatable = false)}, inverseJoinColumns = {@JoinColumn(name = "recursive_id", nullable = false, updatable = false)})
    @NotAudited
    @OrderBy("propA DESC")
    private List<Child> children = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinTable(name = "recursive_children_child", joinColumns = {@JoinColumn(name = "recursive_id")}, inverseJoinColumns = {@JoinColumn(name = "child_id")})
    @NotAudited
    private Child recursiveChild;
    public Child(String propA) {
        this.propA = propA;
    }

    public Child() {

    }

    public String getPropA() {
        return propA;
    }

    public void setPropA(String propA) {
        this.propA = propA;
    }
}

The problem seems to be with the recursive relationship in the Child class. Ember try to audit the relationship despite the presence of @Audited annotation.

Link to a project that reproduce the bug => https://github.com/scandinave/envers6-migration-bug


Solution

  • I finally found the problem by reading the Jakarta Persistence Spec.

    The JoinTable annotation is used in the mapping of entity associations. A JoinTable annotation is specified on the owning side of the association

    In my project there is a @JoinTable annotation on both side of the association. This was working before Hibernate 6, but not now. I just had to remove the @JoinTable not needed to solve the error.