JOIN FETCH 查询在 Spring DataJpaTest 中不获取惰性集合



>Background

你好!我正在尝试编写一个测试来检查 JOIN FETCH 查询是否正确获取惰性集合。我正在一个简单的 Spring Boot 2.1.7 项目中尝试这个,h2 有数据源,并且加载了 spring-boot-starter-data-jpa。测试是使用 Junit4 和断言J,并不是我认为这很重要。

当我使用@DataJpaTest时,集合在此处返回空,而不是例如@SpringBootTest,我不明白为什么。

实体和存储库

我有两个简单的实体,ClassroomPerson。一间教室可以容纳多人。这在课堂课程中由以下方式定义:

@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应用于所有测试方法。

在这种情况下,第一个classroomclassRoomRepository.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.setClassroomsetter,但它有点重:

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();

在断言之前。