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:
@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...
}
@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...
}
@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:
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;
}
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!