JPA效率优化—@EntityGraph
业务场景
在Student中的model中,有一个关系映射,存储了一个String集合,但是在实际业务逻辑中出了问题。
Model:
@Data
@Entity
@Table(name = "student_tab")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", columnDefinition = "INTEGER(10) UNSIGNED", nullable = false)
private Integer id;
@Column(name = "name")
private String name;
private Set<String> elementCollectionString1;
private Set<String> elementCollectionString2;
}
业务:
@Transactional
public void test() {
Student student = repository.findById(id);
asyncFunction(student);
}
......
@Async
public void asyncFunction(Student student) {
......
student.getElementCollectionString1();
......
}
错误与解决
在asyncFunction
中调用student.getElementCollectionString1()
会报错:
failed to lazily initialize a collection of role…… 主要原因是数据库session被close了,导致无法读取数据。
这时有两种解决方式
-
在
asyncFunction(student);
之前,手动调用一次student.getElementCollectionString1();
,这样JPA会查询一次lazyString的内容,在传进asyncFunction中前,model就有值了。但如果关联对象过多,在async之前需要手动调用多个对象,让代码变得非常magic,也不利于维护。 -
为ElementCollection设置为EAGER。
@ElementCollection(fetch = FetchType.EAGER)
这样在查询数据的时候,JPA也会直接读取student.getElementCollectionString1的数据,但有很多场景下,并不需要读取关联关系的表,会造成性能浪费。
所以这依然不是最好的解决方式。
@EntityGraph
当使用@ManyToMany、@ManyToOne、@OneToMany、@OneToOne,@Element关联关系的时候,FetchType不管配置LAZY或者EAGER。SQL真正执行的时候是由一条主表查询和N条子表查询组成的,这种查询效率一般比较低下,比如子对象有N个就会执行N+1条SQL。
这也是JPA的N+1问题。
有时候我们需要用到Left Join或者Inner Join来提高效率,只能通过@Query的JQPL语法实现。
Spring Data JPA为了简单地提高查询率,引入了EntityGraph的概念,可以解决N+1条SQL的问题。
实现步骤
- 先在Entity里面定义@NamedEntityGraph,@ NamedEntityGraph和@NamedAttributeNode可以有多个,也可以有一个。
@NamedEntityGraphs(
@NamedEntityGraph(name = "student.all",
attributeNodes = {
@NamedAttributeNode("elementCollectionString1"),
@NamedAttributeNode("elementCollectionString2")
})
)
public class Student {
2.只需要在查询方法上加@EntityGraph注解即可,其中value就是@NamedEntityGraph中的Name。
@EntityGraph(value="student.all",type= EntityGraph.EntityGraphType.FETCH)
List<Student> findAll();
JPA的语句执行过程
代码:
@Transactional
@Test
public void getData() {
System.out.println("findAll");
List<Student> studentList = studentRepository.findAll();
if (studentList.isEmpty()) {
System.out.println("return");
return;
}
System.out.println("get");
Student student1 = studentList.get(0);
System.out.println("getElementCollectionString1");
student1.getElementCollectionString1().size();
System.out.println("getElementCollectionString2");
student1.getElementCollectionString2().size();
}
Lazy
findAll
Hibernate: select student0_.id as id1_2_, student0_.name as name2_2_ from student_tab student0_
get
getElementCollectionString1
Hibernate: select elementcol0_.student_id as student_1_0_0_, elementcol0_.element_collection_string1 as element_2_0_0_ from student_element_collection_string1 elementcol0_ where elementcol0_.student_id=?
getElementCollectionString2
Hibernate: select elementcol0_.student_id as student_1_1_0_, elementcol0_.element_collection_string2 as element_2_1_0_ from student_element_collection_string2 elementcol0_ where elementcol0_.student_id=?
Eager
生成的语句
findAll
Hibernate: select student0_.id as id1_2_, student0_.name as name2_2_ from student_tab student0_
Hibernate: select elementcol0_.student_id as student_1_1_0_, elementcol0_.element_collection_string2 as element_2_1_0_ from student_element_collection_string2 elementcol0_ where elementcol0_.student_id=?
Hibernate: select elementcol0_.student_id as student_1_0_0_, elementcol0_.element_collection_string1 as element_2_0_0_ from student_element_collection_string1 elementcol0_ where elementcol0_.student_id=?
get
getElementCollectionString1
getElementCollectionString2
EntityGraph
findAll
Hibernate: select student0_.id as id1_2_, student0_.name as name2_2_, elementcol1_.student_id as student_1_1_0__, elementcol1_.element_collection_string2 as element_2_1_0__, elementcol2_.student_id as student_1_0_1__, elementcol2_.element_collection_string1 as element_2_0_1__ from student_tab student0_ left outer join student_element_collection_string2 elementcol1_ on student0_.id=elementcol1_.student_id left outer join student_element_collection_string1 elementcol2_ on student0_.id=elementcol2_.student_id
get
getElementCollectionString1
getElementCollectionString2
总结
不管是lazy还是eager,在读取数据的时候,都会有N+1问题。
而EntityGraph则直接在查询语句的时候,直接用到用到Left Join,优化了数据库的性能。
那么针对性能优化了多少,我做了测试。
每个数据有2个ElementCollection外部关联关系。
每次从数据库中拿一组数据,十个为一组。
进行2000次数据请求。
使用Eager修饰的数据,需要10.2-10.8s。
使用EntityGraph的数据,只需要4.4-4.6s。
节约了一半多的时间。
PS:如果只单纯查询列表,不访问关联数据,只需要3.0-3.2s。
更多
spring-data-jpa-entity-graph
spring-data-jpa-entity-graph 这个库在相对于原本的使用上更加灵活。
原本是使用注解的方式添加,现在可以通过传参的方式,将entitygraph传入。
比如:
productRepository.findByName("MyProduct", EntityGraphs.named("Product.brand"));
EntityGraphType
@EntityGraph(value = "student.all", type = EntityGraph.EntityGraphType.FETCH)
这里的type分为fetch与load两种方式。
fetch: 被EntityGraph指定的值为FetchType.EAGER方式加载,其他的以FetchType.LAZY方式加载。
load: 被EntityGraph指定的值为FetchType.EAGER方式加载,其他的以默认方式或是其已经设定的FetchType方式加载。
一般使用fetch即可。
其他问题
多是因为才接触java后端才知道的问题,熟悉的可以略过。
命名
如果使用了@EntityGraph,确实要快很多,但是有时候查询是不希望查询多余的数据的,怎么解决?
List<Student> findAllByIdIn(List<Integer> ids);
@EntityGraph(value = "Student.all", type = EntityGraph.EntityGraphType.FETCH)
List<Student> findAllGraphByIdIn(List<Integer> ids);
可以根据EntityGraph自定义(没有规则约束)新的查询方式,只在需要关联数据的时候调用findAllGraphByIdIn
。
编译器优化带来的问题
for (Student student: studentList) {
System.out.print(1);
student.getLazyStrings();
}
在System.out.print(1);
这一行打了断点,debug模式下走到这里的时候发现lazyStrings已经有值了,明明没有访问,为什么lazy属性不lazy了呢?
这是因为在debug模式下,编译器的表现不同,断点时访问了lazy属性导致了,正常情况是不会发生的。
隐式调用
public List<Student> findAll() {
return studentRepository.findAll();
}
没有添加@EntityGraph,没有显式的调用的情况下,返回数据时依然会进行N+1查询,这是由于在序列化时,访问了相关的属性。
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
对于分页查询添加了@ EntityGraph之后,会报错.可使明明JPA返回的是一个Page对象,里面的数据数量也远远低于会发生内存警告的数量,为什么会报错?
原因也是因为@EntityGraph使用了left join.
在没有使用left join 的情况下,sql语句能清楚的知道需要多少条数据,以及偏移位置。
但是使用之后就不能清楚知道limit与offset,只能将数据返回给后端,让JPA处理,组装完成后再返回一个Page对象。
现在的处理方式是先通过普通查询,将需要的数据ID找出,再通过数据的ID和@Entity查询详细的细节。
转载自:https://juejin.cn/post/6869650227268157454