spring-boothibernatejpa

Hibernate AssertionError fails Entity Manager creation when checking ManyToOne mapping in Embeddable class used as EmbeddedId


Context

After migrating to Spring Boot 3 and Hibernate 6.2.4.Final, I started encountering an issue when running Spring Boot integration tests, related to a failing entity manager creation, preventing the Spring Boot context from starting.

This problem did not occur in the previous version of Spring Boot, and does not occur when running production code as this is an AssertionError and -ea flag is not used when running production, but it is when running tests.

When running any of my Spring Boot integration tests, an exception is thrown indicating a failure to initialize mappings. The root exception is:

Caused by: java.lang.AssertionError
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyOrder(MappingModelCreationHelper.java:1197)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildEmbeddableForeignKeyDescriptor(MappingModelCreationHelper.java:1133)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.buildEmbeddableForeignKeyDescriptor(MappingModelCreationHelper.java:1072)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.interpretToOneKeyDescriptor(MappingModelCreationHelper.java:986)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.lambda$buildSingularAssociationAttributeMapping$11(MappingModelCreationHelper.java:1740)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess$PostInitCallbackEntry.process(MappingModelCreationProcess.java:247)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess.executePostInitCallbacks(MappingModelCreationProcess.java:107)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess.execute(MappingModelCreationProcess.java:89)
    at org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess.process(MappingModelCreationProcess.java:43)
    at org.hibernate.metamodel.model.domain.internal.MappingMetamodelImpl.finishInitialization(MappingMetamodelImpl.java:201)
    at org.hibernate.internal.SessionFactoryImpl.initializeMappingModel(SessionFactoryImpl.java:320)
    at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:270)
    at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:431)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1455)
    at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:75)
    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:1816)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1766)
    ... 69 more

The line in question in MappingModelCreationHelper.java is:

assert ( (ToOne) bootValueMapping ).isSorted();

This assertion fails for one of my mappings. What is bothering me is that it seems to be a check on whether the mapping is sorted or not but we are talking about a ManyToOne mapping so a mapping to a single entity, not a collection.

This seems to stem from changes in how Hibernate validates mappings in version 6.

The mapping which does not return true for isSorted() and crashes the whole server startup when checked by Hibernate is:

    @NonNull
    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinColumns({
            @JoinColumn(name = "locale_language_code", referencedColumnName = "language_code"),
            @JoinColumn(name = "locale_territory_code", referencedColumnName = "territory_code")
    })
    private MyLocale locale;

in LocalizedId.java

Below are the relevant class definitions:

LocalizedPoiAlias Class

@Entity
@Table(name = "poialias_localized")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LocalizedPoiAlias {
    @EmbeddedId
    private LocalizedId localizedId;

    @ManyToOne
    @MapsId("id")
    @JoinColumn(name = "poi_alias_id")
    private PoiAlias poiAlias;

    private String name;

    // Constructors, equals, hashCode, toString...
}

LocalizedId Class

@Embeddable
@NoArgsConstructor
@Getter
@ToString
public class LocalizedId implements Serializable {
    private long id;

    @NonNull
    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinColumns({
            @JoinColumn(name = "locale_language_code", referencedColumnName = "language_code"),
            @JoinColumn(name = "locale_territory_code", referencedColumnName = "territory_code")
    })
    private MyLocale locale;

    // Constructors, equals, hashCode...
}

PoiAlias Class

@Entity
@Table(name = "poialias")
@Getter
@Setter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class PoiAlias {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pois_seq")
    @SequenceGenerator(name = "pois_seq", sequenceName = "pois_id_seq", allocationSize = 20)
    @Column(name = "id", nullable = false, updatable = false)
    private long id;

    @OneToMany(
            mappedBy = "poiAlias",
            cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH},
            orphanRemoval = true)
    @MapKey(name = "localizedId")
    private Map<LocalizedId, LocalizedPoiAlias> localizations = new HashMap<>();

    // Additional fields, methods, equals, hashCode, toString...
}

Did something change between Hibernate 5 and 6, related to how mappings are loaded and checked?

The code itself works without issues in production so I guess this might be a "false-positive" or an "error which should be a warning" kind of issue but I have no idea how to work around it.

Of course I could just disable assertions when running my tests but I would like to keep them, as I think they contain useful info.

Feel free to ask for more details if needed. Thanks.


EDIT: I noticed something. Turns out Hibernate passes two times on the same mapping in MappingModelCreationHelper, the first occurrence of this mapping is as it "should" be (sorted is true, assertion passes) and the second one fails.

Differences I've observed between the two successively tested mappings using debug mode:

Comparison between the two mappings

Why would it be declared and checked twice, with different attributes, and how can I prevent it?
I also have a very similar entity with the same mapping to LocalizedId which is checked before the failing one and does not seem to fail like this. I can't be sure that it would not also be checked a second time later and fail since the server stops on my first failing mapping though.

By going further down the rabbit hole and debugging the call to creationProcess.registerForeignKeyPostInitCallbacks callback method, I found out that the following mappings are set to be interpreted:

ToOneAttributeMapping(NavigableRole[com.xxxx.yyyy.product.entity.zzzz.LocalizedSite#{id}.locale])@1064280797 -> The working mapping of the similar entity
ToOneAttributeMapping(NavigableRole[com.xxxx.yyyy.product.entity.zzzz.LocalizedPoiAlias#{id}.locale])@663646750 -> The working mapping of LocalizedPoiAlias

and then

ToOneAttributeMapping(NavigableRole[com.xxxx.yyyy.product.entity.zzzz.PoiAlias.localizations#{index}.locale])@596372611

Which is the one that seems to be failing. I don't understand where this mapping is coming from. The localizations field of PoiAlias is defined like this:

    @OneToMany(
            mappedBy = "poiAlias",
            cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH},
            orphanRemoval = true)
    @MapKey(name = "localizedId")
    private Map<LocalizedId, LocalizedPoiAlias> localizations = new HashMap<>();

    public Map<LocalizedId, LocalizedPoiAlias> getLocalizations() {
        return localizations;
    }

Solution

  • I found a workaround for my own case after trying lots of different ways of mapping the Map of localizations in PoiAlias. The bad news is I still have no idea why my original mapping with @MapKey was not tolerated by Hibernate assertions.

    With the following adaptations, my production code still behaves as it should and Spring Boot context of integration tests is now successfully started without the previous assertion errors.

    What I changed:


    In PoiAlias.java
    Before:

        @OneToMany(
                mappedBy = "poiAlias",
                cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH},
                orphanRemoval = true)
        @MapKey(name = "localizedId")
        private Map<LocalizedId, LocalizedPoiAlias> localizations = new HashMap<>();
    

    After:

        @OneToMany(
                mappedBy = "poiAlias",
                cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH},
                orphanRemoval = true)
        @MapKeyJoinColumn(name = "id", insertable = false, updatable = false)
        private Map<LocalizedId, LocalizedPoiAlias> localizations = new HashMap<>();
    

    Since @MapKey was causing the mysterious assertion error I was having, I ended up using the @MapKeyJoinColumn annotation to map the localizations relationship in the PoiAlias entity. I figured it could work, since the column linking the two entities is id. I also added insertable = false and updatable = false.


    In LocalizedPoiAlias.java
    Before:

        @ManyToOne
        @MapsId("id")
        @JoinColumn(name = "id")
        private PoiAlias poiAlias;
    

    After:

        @ManyToOne
        @MapsId("id")
        @JoinColumn(name = "id", insertable = false, updatable = false)
        private PoiAlias poiAlias;
    

    I added insertable = false and updatable = false to the existing @JoinColumn definition.


    In LocalizedId.java
    Before:

        private long id;
    
    

    After:

        @Column(name = "id", insertable = false, updatable = false)
        private long id;
    

    Here, I added an explicit column definition in order to specify insertable = false and updatable = false.


    Why
    When I started using @MapKeyJoinColumn in PoiAlias without any added insertable = false and updatable = false in my definitions, it caused a Hibernate error about repeated mappings on the id column.
    Indeed, none of these mappings should be in charge of updating or inserting my entity's id column. The repeated mapping errors went away when I explicitly added insertable = false and updatable = false to the different relationship mappings referencing the id column of my poialias table.


    Conclusion
    I'm glad I could find a way out of this but and hope it will help someone else but as I said, I'm still in the dark about the fact that my previous mapping refused to work because of the assertion error. If anyone knows more about this, I'm all ears!