Это проблема N + 1 и как ее исправить?

У меня есть список проектов и список клиентов. Проект может быть для одного клиента, и у каждого клиента может быть много проектов. Таким образом, это простое отношение 1: n, когда проект является стороной-владельцем.

Упрощенный до существенного, это

@Entity
public class Project {
  @Id
  long id;

  @ManyToOne(optional = true)
  @JoinColumn(name = "customer", nullable = true, updatable = true)
  Customer customer;
}

@Entity
public class Customer {
  @Id
  long id;
}

Когда я загружаю список проектов, я хочу одновременно эффективно привлекать клиентов. Это не тот случай. Для проектов существует один-единственный запрос, а затем для каждого конкретного клиента, с которым встречается, выдается отдельный запрос.

Скажем, у меня есть 100 проектов, которые назначены 50 различным клиентам. Это приведет к одному запросу для проектов и 50 запросам для клиентов.

Это быстро накапливается, и для больших списков проектов / клиентов наше приложение работает довольно медленно. Также это всего лишь один пример. Это поведение влияет на все наши сущности, имеющие отношения.

Я уже пробовал @Fetch(FetchMode.JOIN) в поле customers, как было предложено здесь, но это ничего не дает и FetchMode.SUBQUERY не применяется в соответствии с Hibernate:

org.hibernate.AnnotationException: использование FetchMode.SUBSELECT не разрешено для ассоциаций ToOne

Как я могу исправить эту проблему?


person musiKk    schedule 12.04.2016    source источник
comment
Я не думаю, что это проблема N + 1, тип выборки по умолчанию для ваших @ManyToOne отношений уже FetchType.EAGER   -  person Raphael Roth    schedule 12.04.2016


Ответы (3)


Если вы используете Spring Data JPA для реализации своих репозиториев, вы можете указать ленивую выборку в JPA объектах:

@Entity
public class Project {
  @Id
  long id;

  @ManyToOne(fetch = FetchType.LAZY, optional = true)
  @JoinColumn(name = "customer", nullable = true, updatable = true)
  Customer customer;
}

@Entity
public class Customer {
  @Id
  long id;
...
}

И добавьте @EntityGraph в свой репозиторий на основе Spring Data JPA:

@Repository
public interface ProjectDao extends JpaRepository<Project, Long> {

    @EntityGraph(
            type = EntityGraphType.FETCH,
            attributePaths = { 
                    "customer" 
            }
    )
    Optional<Project> findById(Long id);
...
}

Мое сообщение в блоге на https://tech.asimio.net/2020/11/06/Preventing-N-plus-1-select-problem-using-Spring-Data-JPA-EntityGraph.html помогает вы предотвращаете проблему выбора N + 1, используя Spring Data JPA и @EntityGraph.

person ootero    schedule 10.11.2020
comment
Спасибо, сэр, что поделился! Это сработало для меня :) - person Fabio Delarias; 30.05.2021

Да, это практический пример задачи n + 1 select.

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

В качестве альтернативы вы можете использовать запрос JPQL с [left] join fetch для инициализации ассоциации непосредственно из набора результатов запроса:

select p from Project p left join fetch p.customer
person Dragan Bozanovic    schedule 12.04.2016
comment
Это единственный способ? Я спрашиваю, потому что использую Spring Data и не всегда имею письменный запрос. Разве @Fetch(FetchMode.JOIN) не следует делать то же самое? - person musiKk; 12.04.2016
comment
FetchMode не влияет на запросы (которые Spring Data, вероятно, в любом случае автоматически генерирует под капотом на основе соглашений об именах в методах репозитория). PS Spring Data не мешает писать запросы. - person Dragan Bozanovic; 12.04.2016
comment
Это правда, но я использую интерфейс JpaSpecificationExecutor, который afaik не позволяет писать запросы. Я могу установить режим выборки в спецификации, но это выглядит супер хаки для меня. Мне в основном приходится полагаться на побочный эффект спецификации, чтобы добиться того, чего я хочу. - person musiKk; 12.04.2016
comment
В моем ответе рассматривается чистое поведение JPA / Hibernate, вам придется проконсультироваться с документацией по любым используемым оболочкам. Но какими бы они ни были, у них нет другого выбора, кроме как делегировать основную ORM, поэтому основные концепции те же. ИМХО, он очень негибкий и ограниченный, если вы используете оболочку ORM, которая не позволяет вам писать собственные запросы JPA. - person Dragan Bozanovic; 12.04.2016
comment
Истинный. Но даже в этом случае у меня бывает много разных запросов для сущности, и мне приходится с избыточностью добавлять множество выборок соединений ко многим запросам. И я даже не могу присоединиться к получению транзитивных отношений. Если это действительно единственный способ, то JPA сама по себе немного ограничена. - person musiKk; 13.04.2016

Да, это наглядный пример задачи выбора n + 1, как сказал @dragan-bozanovic.

В Spring-Boot 2.1.3 @Fetch(FetchMode.JOIN) можно использовать для решения этой проблемы:

  @ManyToOne(optional = true)
  @Fetch(FetchMode.JOIN)
  @JoinColumn(name = "customer", nullable = true, updatable = true)
  Customer customer;

Предупреждение. Если отношение может быть недопустимым, например, если оно отмечено @NotFound(action = NotFoundAction.IGNORE), каждое недопустимое отношение вызовет другой SELECT запрос.

person Richard    schedule 30.04.2019