Hello everybody,
we have recently upgraded Spring Boot in our project from 3.2.6 to 3.4.2 and therefore also Hibernate from 6.4.8.Final to 6.6.5.Final.
After the upgrade, we are experiencing an issue with a query that throws a FetchNotFoundException: org.hibernate.FetchNotFoundException: Entity 'MyBEntity' with identifier value 'B1' does not exist
. We tried some older versions and found that the queries still worked as expected in Hibernate 6.5.3.Final and stopped working from 6.6.0.Final onwards.
We were able to reproduce it using the test case template. You can find the test case, queries and entities in the code sample below.
package org.hibernate.bugs;
import custom.MyAEntity;
import custom.MyBEntity;
import custom.MyCEntity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.NonUniqueResultException;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/**
* This template demonstrates how to develop a test case for Hibernate ORM, using the Java Persistence API.
*/
class JPAUnitTestCase {
private EntityManagerFactory entityManagerFactory;
@BeforeEach
void init() {
entityManagerFactory = Persistence.createEntityManagerFactory( "templatePU" );
}
@AfterEach
void destroy() {
entityManagerFactory.close();
}
// Entities are auto-discovered, so just add them anywhere on class-path
// Add your tests, using standard JUnit.
@Test
void testFetchNotFoundException() throws Exception {
EntityManager entityManager = entityManagerFactory.createEntityManager();
// Given
entityManager.getTransaction().begin();
var newB = new MyBEntity("B1", 1);
var newC = new MyCEntity("C1");
entityManager.persist(newB);
entityManager.persist(newC);
entityManager.getTransaction().commit();
// When
entityManager.getTransaction().begin();
getB(entityManager, "B1");
getAListForB(entityManager, "B1");
var newAEntity = new MyAEntity();
newAEntity.setId("A1");
newAEntity.setBId("B1");
newAEntity.setCId("C1");
entityManager.merge(newAEntity);
// Then
assertDoesNotThrow(() -> getAListForC(entityManager, "C1", 1));
entityManager.getTransaction().commit();
entityManager.close();
}
private MyBEntity getB(EntityManager entityManager, String bId) {
var query = entityManager.createQuery("""
SELECT b
FROM MyBEntity b
WHERE b.id = :id
""", MyBEntity.class);
query.setParameter("id", bId);
var list = query.getResultList();
if (list == null || list.isEmpty()) return null;
else if (list.size() > 1) throw new NonUniqueResultException();
else return list.get(0);
}
private List<MyAEntity> getAListForB(EntityManager entityManager, String bId) {
var query = entityManager.createQuery("""
SELECT DISTINCT a
FROM MyAEntity a
JOIN a.c c
JOIN a.b b
where a.bId = :bId
AND c.deletedOn IS NULL
""", MyAEntity.class);
query.setParameter("bId", bId);
return query.getResultList();
}
private List<MyAEntity> getAListForC(EntityManager entityManager, String cId, Integer someNumber) {
var query = entityManager.createQuery("""
SELECT DISTINCT a
FROM MyAEntity a
JOIN FETCH a.c c
JOIN FETCH a.b b
where b.someNumber = :someNumber
and a.cId = :cId
""", MyAEntity.class);
query.setParameter("cId", cId);
query.setParameter("someNumber", someNumber);
return query.getResultList();
}
}
@Getter
@Setter
@EqualsAndHashCode(of = {"id"})
@Entity
public class MyAEntity {
@Id
@Column(name = "ID", nullable = false)
private String id;
@Column(name = "B_ID", nullable = false)
private String bId;
@Column(name = "C_ID", nullable = false)
private String cId;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "B_ID", nullable = false, insertable = false, updatable = false)
private MyBEntity b;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "C_ID", nullable = false, insertable = false, updatable = false)
private MyCEntity c;
}
@Setter
@Getter
@Entity
public class MyBEntity {
@Id
@Column(name = "ID", nullable = false)
private String id;
@Column(name = "SOME_NUMBER", nullable = false)
private Integer someNumber;
@Setter(AccessLevel.NONE)
@OneToMany(mappedBy = "b", fetch = FetchType.LAZY)
private List<MyAEntity> aList;
public MyBEntity() {}
public MyBEntity(String id, Integer someNumber) {
this.id = id;
this.someNumber = someNumber;
}
}
@Setter
@Getter
@Entity
public class MyCEntity {
@Id
@Column(name = "ID", nullable = false)
private String id;
@Column(name = "DELETED_ON")
private Date deletedOn;
@OneToMany(mappedBy = "c", fetch = FetchType.EAGER)
private List<MyAEntity> aList;
public MyCEntity() {}
public MyCEntity(String id) {
this.id = id;
}
}
Side note: if we do a simple join without the FETCH
in the query in getAListForC
no exception is thrown.
Now for the question:
Did we stumble across a bug, is this a known behaviour change or are we doing something that is not supposed to be done this way? If so, could somebody provide an explanation or link to some piece of documentation.
The latter might be possible as this is part of some greater project with lots of legacy code (lots of weird stuff happening there).
Thank you for your help