>Background
你好!我正在尝试编写一个测试来检查 JOIN FETCH 查询是否正确获取惰性集合。我正在一个简单的 Spring Boot 2.1.7 项目中尝试这个,h2 有数据源,并且加载了 spring-boot-starter-data-jpa。测试是使用 Junit4 和断言J,并不是我认为这很重要。
当我使用@DataJpaTest
时,集合在此处返回空,而不是例如@SpringBootTest
,我不明白为什么。
实体和存储库
我有两个简单的实体,Classroom
和Person
。一间教室可以容纳多人。这在课堂课程中由以下方式定义:
@OneToMany(mappedBy = "classroom", fetch = FetchType.LAZY)
private Set<Person> persons = new HashSet<>();
在人员类中:
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "classroom_id")
private Classroom classroom;
在ClassRoomRepository
中,我定义了一种方法,应该在教室里急切地吸引人:
@Query("SELECT DISTINCT c FROM Classroom c JOIN FETCH c.persons WHERE c.id = :classroomId")
Classroom getClassRoom(@Param("classroomId") Long classRoomId);
测试内容
@RunWith(SpringRunner.class)
@DataJpaTest
public class ClassroomTest{
@Autowired
private PersonRepository personRepository;
@Autowired
private ClassRoomRepository classRoomRepository;
@Test
public void lazyCollectionTest() {
Classroom classroom = new Classroom();
classRoomRepository.save(classroom);
Person person = new Person(classroom);
personRepository.save(person);
assertThat(classRoomRepository.getClassRoom(classroom.getId()).getPersons()).hasSize(1);
}
}
测试结果
我看到的是getPersons()
返回:
- 如果测试类注释为 0
@DataJpaTest
- 如果测试类注释为:
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
- 如果测试类注释为:
@SpringBootTest
结论/问题
我知道@DataJpaTest
在事务中运行每个测试,最后进行回滚。但是,为什么这会阻止此联接提取查询返回数据呢?
这是双向关联不同步时的预期行为。
整个测试 1 个事务:1 个持久性上下文
单独使用@DataJpaTest
时,测试方法使用 1 个事务、1 个持久性上下文、1 个一级缓存执行,因为@Transactional
应用于所有测试方法。
在这种情况下,第一个classroom
和classRoomRepository.getClassRoom(classroom.getId())
返回的实例是同一个实例,因为 Hibernate 使用其一级缓存返回教室实例,该实例是用空 Set 构造的,并且忽略查询中的ResultSet 记录。 可以验证:
Classroom classroom = new Classroom(); // constructs the Classroom with an empty Set
classRoomRepository.save(classroom);
Classroom classroom2 = classRoomRepository.getClassRoom(classroom.getId());
System.out.println("same? " + (classroom==classroom2));
// output: same? true
// and classroom2.persons is empty :)
修复:双向关联同步
由于 Hibernate 会忽略您的查询结果,因此查询后@OneToMany
仍然为空。 换句话说,您"忘记"在课堂中添加人员。
您必须在 setter 和 adder 中手动同步双向关联(或任何操作这些关联的方法,包括您的构造函数),或者使用 Hibernate 字节码增强和启用 AssociationManagement(魔术,但要谨慎使用)。
让我们编写一个(流利的)Classroom.addPerson
加法器,在此课堂中添加一个人,并更新这个人:
public Classroom addPerson(Person person) {
this.persons.add(person);
person.setClassroom(this);
return this;
}
请注意,您还应该添加一个Classroom.removePerson
方法,该方法将Person.classroom
设置为从 Set 中删除人员后null
。
然后重写测试以使其通过:
Classroom classroom = new Classroom();
classRoomRepository.save(classroom);
Person person = new Person();
classroom.addPerson(person);
personRepository.save(person);
在这种情况下,您手动将人员添加到集合中,并使关联的另一端保持同步,这是一种自然的做事方式。
但是如果你想坚持使用你的Person(Classroom classroom)
构造函数:
public Person(Classroom classroom) {
classroom.addPerson(this); // add this person to the classroom
}
如果您希望能够以两种方式操纵此关联,也可以使用Person.setClassroom
setter,但它有点重:
public Person setClassroom(Classroom classroom) {
this.classroom = classroom;
if(classroom != null)
this.classroom.getPersons().add(this);
else
this.classroom.getPersons().remove(this);
return this;
}
您手动使关联的两端保持同步,因此您不依赖于 Hibernate 获取集合。
您的测试将通过,我添加了一个检查以确保关联同步:
Classroom classroom = new Classroom();
classRoomRepository.save(classroom);
Person person = new Person(classroom);
// check that the classroom contains the person
Assertions.assertThat(classroom.getPersons().contains(person)).isTrue();
personRepository.save(person);
Assertions.assertThat(classRoomRepository.getClassRoom(classroom.getId())
.getPersons()).hasSize(1);
但请记住,对classRoomRepository.getClassRoom(classroom.getId())
的调用是无用的,因为如果结果已经存在于持久性上下文中,Hibernate 会忽略结果。 您应该只使用您的第一个classroom
实例。
多个事务:多个持久性上下文
添加@Transactional(propagation = Propagation.NOT_SUPPORTED)
时,您选择不使用事务,因此使用了 3 个事务、3 个持久性上下文和 3 个一级缓存(一个用于第一次保存,一个用于第二次保存,一个用于查询)。 对于根本不使用@Transactional
的@SpringBootTest
也是如此。
在这 2 种情况下,您正在操作不同的实例,因此 Hibernates 使用查询中的 ResultSet 记录为您的教室提供提取的人员,如预期的那样。
System.out.println("same? " + (classroom==classroom2));
// output: same? false
有关更多信息,请查看本文,以及弗拉德对爱迪生问题的回答: https://vladmihalcea.com/jpa-hibernate-first-level-cache/
如果已存在具有相同 ID 的托管实体,则 结果集记录被忽略。
您还可以查看有关双向关联同步的 https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/。
如果您对 Hibernate 字节码增强和魔术双向关联同步感兴趣,请阅读 https://docs.jboss.org/hibernate/orm/current/topical/html_single/bytecode/BytecodeEnhancement.html
它不会阻止联接获取获取正确的数据集,它只是阻止获取保存在同一事务中的任何记录,因为它们尚未存储。 要解决此问题,只需注入EntityManager
实例,然后调用:
em.flush():
em.close();
在断言之前。