Проблема N+1 и как её решить с помощью EntityGraph

от автора

Всем привет! В данной статье попробуем разобраться с проблемой N+1 (или может правильнее 1+N?) и как ее решить с помощью использования EntityGraph.

Проблема N+1 возникает, когда мы генерируем запрос на получение одной сущности из базы данных, но у данной сущности есть свои связанные сущности, которые мы тоже хотим получить и hibernate генерирует вначале один (1) запрос к базе данных, чтобы получить интересующую нас сущность, а потом N запросов, чтобы достать из базы данных связанные сущности. Данная проблема отражается отрицательно на производительности работы базы данных из-за большого числа обращений к ней.

Создадим проект и подключим следующие зависимости:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.github.javafaker</groupId> <artifactId>javafaker</artifactId> <version>1.0.2</version> </dependency> </dependencies>

Создадим две простые сущности Client и EmailAddress.

@Data @NoArgsConstructor @Entity @Table(name = "client") public class Client {     @Id     @Column(name = "id")     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(name = "full_name")     private String fullName;      @Column(name = "mobile_number")     private String mobileNumber;      @OneToMany(cascade = CascadeType.ALL, mappedBy = "client")     private List<EmailAddress> emailAddresses;      public Client(String fullName, String mobileNumber, List<EmailAddress> emailAddresses) {         this.fullName = fullName;         this.mobileNumber = mobileNumber;         this.emailAddresses = emailAddresses;     } }
@Entity @Table(name = "email_address") @Data @NoArgsConstructor public class EmailAddress {     @Id     @Column(name = "id")     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(name = "email")     private String email;      @JsonIgnore     @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)     @JoinColumn(name = "client_id", referencedColumnName = "id")     private Client client;      public EmailAddress(String email) {         this.email = email;     } }

Связь между Client и EmailAddress @OneToMany, то есть у одного клиента может быть несколько email адресов.

Создадим также ClientRepository.

public interface ClientRepository extends JpaRepository<Client, Long> { }

В application.properties пропишем подключение к базе данных, а также чтобы в консоль выводились sql команды.

spring.datasource.url=jdbc:postgresql://localhost:5432/название Вашей БД spring.datasource.username=Ваше имя для подключения к postgres spring.datasource.password=Ваш пароль  spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database=postgresql  spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true

Создадим класс ClientService, где у нас будет бизнес-логика. В данном классе создадим метод для генерации данных в нашу базу. Создадим 2000 клиентов и пусть у каждого клиента будет по два email адреса.

@Service public class ClientService {     private final ClientRepository clientRepository;      @Autowired     public ClientService(ClientRepository clientRepository) {         this.clientRepository = clientRepository;     }      public void generateDB(){         List<Client> clients = create2000Clients();         for (int i = 0; i < clients.size(); i++) {             clientRepository.save(clients.get(i));         }     }       public List<Client> create2000Clients() {         List<Client> clients = new ArrayList<>();         Faker faker = new Faker();         for (int i = 0; i < 2_000; i++) {             String firstName = faker.name().firstName();             String lastName = faker.name().lastName();             String sufixTel = String.valueOf(i);             String telephone = "+375290000000";              List<EmailAddress>emailAddresses= Arrays.asList(                     new EmailAddress((firstName + lastName).toLowerCase() + "1" + i + "@gmail.com"),                     new EmailAddress((firstName + lastName).toLowerCase() + "2" + i + "@gmail.com"));              telephone = telephone.substring(0, telephone.length()-sufixTel.length()) + sufixTel;             Client client = new Client(                     firstName + " " + lastName,                     telephone,                     emailAddresses             );              for (EmailAddress emailAddress:emailAddresses) {                 emailAddress.setClient(client);             }              clients.add(client);         }         return clients;     } }

Также создадим ClientController, где будем вызывать методы.

@RestController @RequestMapping("/api/v1/client") public class ClientController {      private final ClientService clientService;     private final ClientRepository clientRepository;     @Autowired     public ClientController(ClientService clientService, ClientRepository clientRepository) {         this.clientService = clientService;         this.clientRepository = clientRepository;     }      @ResponseStatus(HttpStatus.OK)     @GetMapping("/fillDB")     public String fillDataBase() {         clientService.generateDB();         return "Amount clients: " + clientRepository.count();     }  }

Через postman сделаем get запрос на http://localhost:8080/api/v1/client/fillDB наша тестовая база данных должна заполниться.

Далее дополним ClientRepository методом

 List<Client> findByFullNameContaining(String name);

Мы будем искать клиентов по части имени.

Дополним класс ClientService методом

 public List<Client> findByNameContaining(String userName){         return clientRepository.findByFullNameContaining(userName);     }

а также дополним класс ClientController методом

  @ResponseStatus(HttpStatus.OK)     @GetMapping()     public List<Client> findByNameContaining(@RequestParam String clientName) {         List<Client> clients = clientService.findByNameContaining(clientName);         return clients;     }

Создадим проблему N+1: зайдем в postman и сделаем get запрос с параметром clientName на http://localhost:8080/api/v1/client?clientName=Ren

И в консоли мы увидим, что hibernate сделал вначале один запрос в базу данных в таблицу client и нашел всех клиентов, а потом еще N-запросов к таблице email_address, чтобы получить у каждого клиента email адреса.

Как решить проблему N+1? Суть решения этой проблемы в том чтобы сократить количество запросов к базе данных до необходимого минимума, то есть до одного.

Есть несколько возможных решений, я покажу как это решить с помощью JPA Entity Graph.

Entity Graph — позволяет улучшить производительность во время выполнения запросов к базе данных при загрузке связанных ассоциаций и основных полей объекта. JPA Entity Graph загружает данные в один запрос выбора, избегая повторного обращения к базе данных. Это считается хорошим подходом для повышения производительности приложений.

Вариант 1. Пишем аннотацию@EntityGraph над методом findByFullNameContaining в ClientRepository.

public interface ClientRepository extends JpaRepository<Client, Long> {     @EntityGraph(type = EntityGraph.EntityGraphType.FETCH, attributePaths = "emailAddresses")     List<Client> findByFullNameContaining(String name); }

По умолчания @EntityGraph имеет тип EntityGraphType.FETCH , но для того чтобы понимать, что происходит я его указываю, и он применяет стратегию FetchType.EAGER к указанным атрибутам, то есть к emailAddresses.

Зайдем в postman и сделаем снова get запрос с параметром clientName на http://localhost:8080/api/v1/client?clientName=Ren

Мы получим только один запрос к базе данных.

Вариант 2. Пишем аннотацию @NamedEntityGraphнад классом Client.

@Data @NoArgsConstructor @Entity @Table(name = "client") @NamedEntityGraph(name = "client_entity-graph", attributeNodes = @NamedAttributeNode("emailAddresses")) public class Client {     @Id     @Column(name = "id")     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(name = "full_name")     private String fullName;      @Column(name = "mobile_number")     private String mobileNumber;      @OneToMany(cascade = CascadeType.ALL, mappedBy = "client")     private List<EmailAddress> emailAddresses;      public Client(String fullName, String mobileNumber, List<EmailAddress> emailAddresses) {         this.fullName = fullName;         this.mobileNumber = mobileNumber;         this.emailAddresses = emailAddresses;     } } 

В данном случае также будет использоваться «жадная» загрузка указанной связной сущности emailAddresses.

Также необходимо исправить аннотацию над ClientRepository.

public interface ClientRepository extends JpaRepository<Client, Long> {     @EntityGraph(type = EntityGraph.EntityGraphType.FETCH, value = "client_entity-graph")     List<Client> findByFullNameContaining(String name); }

Cделаем снова get запрос с параметром clientName на http://localhost:8080/api/v1/client?clientName=Ren

Получим один запрос к базе данных.

Спасибо Всем кто дочитал до конца данную статью. Всем пока.


ссылка на оригинал статьи https://habr.com/ru/post/714704/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *