likes
comments
collection
share

JPA效率优化—@EntityGraph

作者站长头像
站长
· 阅读数 16

业务场景

在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了,导致无法读取数据。

这时有两种解决方式

  1. asyncFunction(student);之前,手动调用一次 student.getElementCollectionString1();,这样JPA会查询一次lazyString的内容,在传进asyncFunction中前,model就有值了。但如果关联对象过多,在async之前需要手动调用多个对象,让代码变得非常magic,也不利于维护。

  2. 为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的问题。

实现步骤

  1. 先在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查询详细的细节。