JPQL, получите список объектов, которые имеют дочерний список, избегайте n + 1 запросов и выбирайте только указанные c поля - PullRequest
1 голос
/ 16 января 2020

У меня есть следующая сущность Project:

@Data
@NoArgsConstructor
@Entity
@ToString(exclude = "roles")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Project {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  @Column(unique = true)
  private String name;

  private String description;

  private Boolean isArchived;

  private LocalDate archivedDate;

  private LocalDate creationDate;

  @Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE")
  private Boolean invoicingActivated;

  @ManyToOne
  @NotNull
  private Order order;

  @OneToOne(cascade = CascadeType.ALL)
  private DefaultDailyEntrySettings defaultDailyEntrySettings;

  @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<ProjectEmployee> projectEmployees;
}

Я хочу получить все проекты. Каждый проект также должен иметь свой список projectEmployees.

То есть сущность ProjectEmployee:

@Data
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"employee_id", "project_id"})})
@NoArgsConstructor
@ToString(exclude = "project")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProjectEmployee {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  @ManyToOne
  @JsonIgnore
  @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
  @NotNull
  private Project project;

  @ManyToOne
  @NotNull
  private Employee employee;

  @ManyToOne
  private ProjectEmployeeRole projectEmployeeRole;
}

Чтобы избежать n + 1 запросов, я написал следующий запрос:

@Query("SELECT project FROM Project project JOIN FETCH project.order ord JOIN FETCH ord.customer " +
          "LEFT JOIN FETCH project.projectEmployees projectEmployee LEFT JOIN FETCH project.defaultDailyEntrySettings " +
          "LEFT JOIN FETCH projectEmployee.employee LEFT JOIN FETCH projectEmployee.projectEmployeeRole")
List<Project> findAllProjectsInOneQuery();

Это работает, но возвращает все свойства каждого отдельного объекта. Например, меня интересуют только идентификатор и имя ord.customer, в этом случае мне не нужны все остальные поля ord.customer. Проблема с получением всех полей таким способом состоит в том, что передается много данных, которые мне не нужны в этом случае. Чтобы выбрать только те, которые мне нужны, и уменьшить объем данных, которые я посылаю через inte rnet, я мог бы сделать что-то вроде этого:

@Query("SELECT new de.project.Project(project.id, project.name, " +
          "project.description, project.isArchived, project.archivedDate, " +
          "project.creationDate, project.invoicingActivated, project.order.id, " +
          "project.order.name, project.order.customer.id, project.order.customer.name) " +
          "FROM  Project project")
List<Project> findAllMinimal();

Но, как вы видите, я не могу получить project.projectEmployees таким образом, потому что это список, и я не думаю, что смогу передать список через конструктор таким образом.

Я пытался:

@Query("SELECT new de.project.Project(project.id, project.name, " +
              "project.description, project.isArchived, project.archivedDate, " +
              "project.creationDate, project.invoicingActivated, project.order.id, " +
              "project.order.name, project.order.customer.id, project.order.customer.name, " +
              "projectEmployee.id) " +
              "FROM  Project project JOIN project.projectEmployees projectEmployee")
    List<Project> findAllMinimal();

Но projectEmployee.id это просто идентификатор первого projectEmployee, я не думаю, что смогу передать все projectEmployees таким образом.

Есть ли способ получить все проекты с их ProjectEmployees (и другими свойствами, которые я перечислил в запросе выше) и укажите, какие поля я хотел бы получить? Это не должен быть один запрос, постоянное количество запросов - это нормально. Следует избегать n + 1 запросов.

Редактировать:

Я нашел обходной путь. Я использую следующие два запроса:

@Query("SELECT new de.project.Project(project.id, project.name, " +
          "project.description, project.isArchived, project.archivedDate, " +
          "project.creationDate, project.invoicingActivated, project.order.id, " +
          "project.order.name, project.order.customer.id, project.order.customer.name) " +
          "FROM  Project project")
List<Project> findAllMinimal();

@Query("SELECT DISTINCT new de.projectemployee.ProjectEmployee(projectEmployee.id, " +
          "projectEmployee.employee.id, projectEmployee.employee.email, " +
          "projectEmployee.employee.firstName, projectEmployee.employee.lastName, " +
          "projectEmployee.employee.address, projectEmployee.employee.weeklyHoursEnabled, " +
          "projectEmployee.employee.weeklyHours, projectEmployee.employee.isArchived, " +
          "projectEmployee.employee.archivedDate, projectEmployee.project.id, projectEmployeeRole.id, " +
          "projectEmployeeRole.name, projectEmployeeRole.hourlyWage) FROM ProjectEmployee projectEmployee " +
          "LEFT JOIN projectEmployee.projectEmployeeRole projectEmployeeRole " +
          "WHERE projectEmployee.project IN :projects")
  List<ProjectEmployee> findByProjects(@Param("projects") List<Project> projects);

Чтобы дать каждому проекту свой проект. Мне нужен какой-то дополнительный java код:

    List<Project> projects = projectRepository.findAllMinimal();
    List<ProjectEmployee> projectEmployees = projectEmployeeRepository.findByProjects(projects);
    Map<Long, List<ProjectEmployee>> projectIdToProjectEmployeesMap = new HashMap<>();
    for (ProjectEmployee projectEmployee : projectEmployees) {
      List<ProjectEmployee> projectEmployeesToBeSaved = projectIdToProjectEmployeesMap.getOrDefault(projectEmployee.getProject().getId(), new ArrayList<>());
      projectEmployeesToBeSaved.add(projectEmployee);
      projectIdToProjectEmployeesMap.put(projectEmployee.getProject().getId(), projectEmployeesToBeSaved);
    }
    projects.forEach(project -> project.setProjectEmployees(projectIdToProjectEmployeesMap.get(project.getId())));
    return projects;

Итак, вы, как вы видите, можете достичь Моя цель - собрать все проекты с их ProjectEmployees в постоянном количестве запросов (2) и выбрать только те поля, которые мне нужны. Недостатком является то, что у меня есть javacode, работающий со сложностью O (n). Но я уменьшил размер передаваемых им данных более чем на 90%, поэтому думаю, что это стоит.

Трудно поверить, что алгоритм кода java, подобный тому, который я использовал, необходим для поиска решение для моей проблемы, поэтому, если кто-то найдет лучшее решение (только с sql запросами), способное выполнить вышеизложенное, поделитесь им.

1 Ответ

0 голосов
/ 16 января 2020

Не проверял это для вашего случая, но на первый взгляд вы можете воспользоваться кешем первого уровня. После выполнения

List<Project> projects = projectRepository.findAllMinimal();
List<ProjectEmployee> projectEmployees = projectEmployeeRepository.findByProjects(projects);

Любой доступ к projectEmployee.getProject() должен вернуть свой проект из кэша, избегая проблемы n + 1. Поэтому, если вы можете оставить project.projectEmployees Список незаполненным, вы можете отказаться от своего "пользовательского javacode".

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...