从零开始 Spring Boot 67:JPA 中的惰性元素
图源:简书 (jianshu.com)
惰性加载带来的问题
在实体类之间建立关系时,可以选择“惰性加载”,比如:
@Entity
public class Student {
// ...
@OneToMany(mappedBy = "student",
cascade = CascadeType.ALL,
fetch = FetchType.LAZY)
@Builder.Default
private List<Email> emails = new ArrayList<>();
// ...
}
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"account", "domain"}))
public class Email {
// ...
@Setter
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
}
@OneToMany
的fetch
属性可以选择FetchType.LAZY
或FetchType.EAGER
,前者是惰性加载,后者是急切加载。如果是惰性加载,JPA 从数据库中加载Student
实例时,不会立即加载关联的emails
属性。而是直到真正使用emails
属性时才会再查询并加载数据。
这样虽然可以避免不必要的关联查询,但同时也会产生另一个问题——获取到的关联数据是否有效?
为了避免这个问题,JPA 要求使用惰性加载时,相关数据的延迟加载与之前的实体数据加载在同一个事务下,否则就无法保证数据一致性。如果不这样做,就会产生异常。
比如下面这个示例,为Student
创建服务层,并提供一个根据学生姓名查询学生实体实例的方法:
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
public Student findStudentByName(String name) {
return studentRepository.findOne(Example.of(Student.builder()
.name(name)
.build())).get();
}
// ...
}
调用示例:
var student = studentService.findStudentByName("icexmoon");
Assertions.assertEquals("icexmoon", student.getName());
Assertions.assertThrows(LazyInitializationException.class, () -> {
student.getEmails().size();
});
因为调用示例中并没有使用事务,所以调用student.getEmails().size()
时会产生一个LazyInitializationException
异常。
要解决这个问题,最简单的是将关联集合的加载方式修改为FetchType.EAGER
,但有时候我们可能不希望这么做,此时有一些其他方式可供我们选择。
使用 JPQL
可以直接使用 JPQL 进行关联查询,同时加载关联的数据:
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Service
public class StudentService {
@Autowired
private SessionFactory sessionFactory;
public Student findStudentByName2(String name) {
var session = sessionFactory.openSession();
Student student = session.createQuery("select s from Student s join fetch s.emails where s.name=:name", Student.class)
.setParameter("name", name)
.getSingleResult();
session.close();
return student;
}
}
SQL 日志:
select s1_0.id,e1_0.student_id,e1_0.id,e1_0.account,e1_0.domain,s1_0.name from student s1_0 join email e1_0 on s1_0.id=e1_0.student_id where s1_0.name=?
binding parameter [1] as [VARCHAR] - [icexmoon]
使用实体图
可以用实体图(Entity Graph)指定在 JPA 查询时需要加载的属性:
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Service
public class StudentService {
@Autowired
private SessionFactory sessionFactory;
// ...
public Student findStudentByName3(String name) {
long id = this.findStudentByName("icexmoon").getId();
var session = sessionFactory.openSession();
RootGraph<Student> entityGraph = session.createEntityGraph(Student.class);
entityGraph.addAttributeNodes("name", "emails");
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
return session.find(Student.class, id, properties);
}
}
可以阅读这篇文章了解实体图的更多信息。
使用事务
正如前面所说的,如果在延迟加载数据时能够在同一个事务中,也就不会出现类似的问题:
@Test
@Transactional
void test4(){
var student = studentService.findStudentByName("icexmoon");
Assertions.assertEquals("icexmoon", student.getName());
Assertions.assertEquals(2, student.getEmails().size());
}
The End,谢谢阅读。
参考资料
- Working with Lazy Element Collections in JPA | Baeldung