Оптимизация эффективности JPA — @EntityGraph

Spring

Деловая сцена

В модели Student есть реляционное сопоставление, в котором хранится коллекция 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()Будет сообщено об ошибке:

не удалось лениво инициализировать набор ролей...

Основная причина в том, что сеанс базы данных закрыт, что приводит к невозможности чтения данных.

Есть два решения

  1. существуетasyncFunction(student);Раньше вызывайте его вручную один раз

student.getElementCollectionString1();, так что JPA запросит содержимое lazyString один раз, и модель будет иметь значение перед передачей его в asyncFunction. Но если связанных объектов слишком много, вам нужно вручную вызвать несколько объектов перед асинхронностью, что делает код очень волшебным и не способствует сопровождению.

  1. Установите EAGER для ElementCollection.

@ElementCollection(fetch = FetchType.EAGER)Таким образом, при запросе данных JPA также будет напрямую считывать данные student.getElementCollectionString1, но во многих сценариях нет необходимости читать связанную таблицу, что приведет к потере производительности.

Так что это все же не лучшее решение.

@EntityGraph

При использовании ассоциаций @ManyToMany, @ManyToOne, @OneToMany, @OneToOne, @Element FetchType не настраивает LAZY или EAGER. Когда SQL фактически выполняется, он состоит из запроса к основной таблице и N запросов к подтаблицам.Этот запрос, как правило, неэффективен.Например, если есть N подобъектов, будет выполнен N+1 SQL.

Это также проблема N+1 с JPA.

Иногда нам нужно использовать левое соединение или внутреннее соединение для повышения эффективности, чего можно достичь только с помощью синтаксиса JQPL @Query.

Чтобы просто повысить скорость запросов, Spring Data JPA вводит концепцию EntityGraph, которая может решить проблему N+1 SQL.

Этапы реализации

  1. Сначала определите @NamedEntityGraph в Entity. Может быть несколько или один @NamedEntityGraph и @NamedAttributeNode.
@NamedEntityGraphs(
        @NamedEntityGraph(name = "student.all",
                attributeNodes = {
                @NamedAttributeNode("elementCollectionString1"),
                @NamedAttributeNode("elementCollectionString2")
        })
)
public class Student {

2. Просто добавьте аннотацию @EntityGraph к методу запроса, где значением является имя в @NamedEntityGraph.

@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

Суммировать

Ленивый он или нетерпеливый, при чтении данных возникнет N+1 проблем.

EntityGraph напрямую использует левое соединение при запросе инструкций, что оптимизирует производительность базы данных.

Так насколько это оптимизировано для производительности, я сделал тест.

Каждые данные имеют 2 внешних ассоциации ElementCollection.

Каждый раз, когда группа данных берется из базы данных, десять группируются вместе.

Сделайте 2000 запросов данных.

Используя данные, модифицированные Eager, это занимает 10,2–10,8 с.

При использовании данных EntityGraph требуется всего 4,4–4,6 с.

Сэкономил более чем в половине случаев.

PS: если вы только запрашиваете список без доступа к связанным данным, это займет всего 3,0–3,2 с.

Более

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)

Тип здесь делится на два способа: выборка и загрузка.

fetch: значение, указанное EntityGraph, загружается в режиме FetchType.EAGER, а остальные загружаются в режиме FetchType.LAZY.

load: значение, указанное EntityGraph, загружается в режиме FetchType.EAGER, а другие загружаются в режиме по умолчанию или в установленном режиме FetchType.

Как правило, можно использовать выборку.

другие проблемы

О большинстве проблем известно только из-за 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);Эта строка имеет точку останова.Когда я пришел сюда в режиме отладки, я обнаружил, что lazyStrings уже имеет значение, но к нему явно не обращались.Почему атрибут lazy не 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 использует левое соединение.

Без использования левого соединения оператор sql может четко знать, сколько фрагментов данных необходимо, и позицию смещения.

Но после его использования вы не можете четко знать предел и смещение, вы можете только вернуть данные на серверную часть, позволить JPA обработать их и вернуть объект Page после завершения сборки.

Текущий метод обработки заключается в том, чтобы сначала узнать требуемый идентификатор данных с помощью общего запроса, а затем запросить подробные сведения с помощью идентификатора данных и @Entity.