Hibernate создает запросы N+1 для аннотированного свойства @ManyToOne JPA

У меня есть эти классы:

@Entity
public class Invoice implements Serializable {
    @Id
    @Basic(optional = false)
    private Integer number;

    private BigDecimal value;

    //Getters and setters
}

@Entity
public class InvoiceItem implements Serializable {
    @EmbeddedId
    protected InvoiceItemPK invoiceItemPk;

    @ManyToOne
    @JoinColumn(name = "invoice_number", insertable = false, updatable = false)
    private Invoice invoice;

    //Getters and setters
}

Когда я запускаю этот запрос:

session.createQuery("select i from InvoiceItem i").list();

Он выполняет один запрос для выбора записей из InvoiceItem, и если у меня есть 10 000 элементов счета, он создает 10 000 дополнительных запросов для выбора счета из каждого элемента счета.

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

Итак, как я могу это сделать?


person Mateus Viccari    schedule 06.01.2015    source источник


Ответы (4)


Попробуйте с

session.createQuery("select i from InvoiceItem i join fetch i.invoice inv").list();

Он должен получить все данные в одном SQL-запросе с использованием соединений.

person Predrag Maric    schedule 06.01.2015
comment
На самом деле это работает нормально, но есть ли способ сделать это с помощью аннотаций? Поскольку, скажем, у нас много аннотированных полей @ManyToOne, я не хочу писать много JOIN FETCH XXX только для того, чтобы избежать проблемы... - person Mateus Viccari; 06.01.2015
comment
Hibernate имеет аннотацию @Fetch, которую вы можете использовать, поместите @Fetch(FetchMode.JOIN) в сопоставление отношений. - person Predrag Maric; 06.01.2015
comment
Даже при использовании @Fetch(FetchMode.JOIN) он по-прежнему создает дополнительные запросы. Думаю, мне придется поработать с исправлением запроса. - person Mateus Viccari; 06.01.2015
comment
Возможно, @Fetch игнорируется в запросах, но используется, если вы получаете сущность с помощью session.get(), вы можете проверить, верно ли это. - person Predrag Maric; 06.01.2015
comment
@PredragMaric Да, это игнорируется - см. Также stackoverflow.com/a/36797931/7095884 - person olivmir; 28.05.2018
comment
У меня не работает. QueryException: в запросе указана выборка объединения, но владелец извлеченной ассоциации отсутствует в списке выбора - person Alkis Mavridis; 21.01.2020

Проблема здесь связана не с Hibernate, а с JPA.

До JPA 1.0 Hibernate 3 использовал ленивую загрузку для всех ассоциаций.

Однако спецификация JPA 1.0 использует FetchType.LAZY только для ассоциаций коллекций:

@ManyToOne и @OneToOne ассоциации используют FetchType.EAGER по умолчанию, и это очень плохо с точки зрения производительности.

Описанное здесь поведение называется [проблемой запроса N+1][5], и это происходит потому, что Hibernate необходимо убедиться, что ассоциация @ManyToOne инициализирована, прежде чем возвращать результат пользователю.

Теперь, если вы используете прямую выборку через entityManager.find, Hibernate может использовать LEFT JOIN для инициализации ассоциаций FetchTYpe.EAGER.

Однако при выполнении запроса, в котором явно не используется предложение JOIN FETCH, Hibernate не будет использовать JOIN для получения ассоциаций FetchTYpe.EAGER, так как он не может изменить запрос, который вы уже указали для построения. Таким образом, он может использовать только вторичные запросы.

Исправление простое. Просто используйте FetchType.LAZY для всех ассоциаций:

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "invoice_number", insertable = false, updatable = false)
   private Invoice invoice;

Более того, вам следует использовать проект db-util для подтверждения количества операторов, выполненных JPA и Спящий режим.

person Vlad Mihalcea    schedule 05.02.2019
comment
Однако при выполнении запроса, в котором явно не используется предложение JOIN FETCH, Hibernate не будет использовать JOIN для извлечения ассоциаций FetchTYpe.EAGER, поскольку он не может изменить запрос, который вы уже указали как создавать. Таким образом, он может использовать только вторичные запросы. Из моего недавнего тестирования я увидел, что это неприменимо к запросам Criteria. Это правда? - person Adam; 18.03.2019
comment
У вас есть повторяющийся тестовый пример, который показывает это? - person Vlad Mihalcea; 18.03.2019
comment
Немного сложно создать тестовый проект для этого из-за сложности того, как я сейчас загружаюсь, но я постараюсь не забыть сделать проект, который покажет это, когда у меня будет шанс. Основная идея заключается в том, что когда я выбираю объект, который имеет отношение @ManyToOne, в случае запроса Critieria я вижу, что выполняется один SQL-запрос (и к связанному объекту присоединяется). В случае с JPQL я вижу два выполняемых SQL-запроса, второй из которых выбирает связанный объект. Я не использовал ваш инструмент для подсчета запросов, просто просматривал вывод SQL (hibernate.show_sql = true). - person Adam; 20.03.2019
comment
Не могли бы вы посмотреть на эту проблему? stackoverflow.com/questions/59849508/ - person Sahbaz; 22.01.2020

Да, вам нужна настройка: @BatchSize(size=25). Проверьте это здесь:

20.1.5. Использование пакетной выборки

маленькая цитата:

Используя пакетную выборку, Hibernate может загружать несколько неинициализированных прокси-серверов при доступе к одному прокси-серверу. Пакетная выборка — это оптимизация стратегии отложенной выборки. Пакетную выборку можно настроить двумя способами: на уровне класса и на уровне коллекции.

Пакетную выборку для классов/сущностей легче понять. Рассмотрим следующий пример: во время выполнения у вас есть 25 экземпляров Cat, загруженных в Session, и каждый Cat имеет ссылку на своего владельца, Person. Класс Person сопоставляется с прокси, lazy="true". Если теперь вы пройдете по всем кошкам и вызовете getOwner() для каждой, Hibernate по умолчанию выполнит 25 операторов SELECT для получения прокси-владельцев. Вы можете настроить это поведение, указав размер партии в отображении Person:

<class name="Person" batch-size="10">...</class>

С указанным размером пакета Hibernate теперь будет выполнять запросы по запросу, когда необходимо получить доступ к неинициализированному прокси, как указано выше, но разница в том, что вместо запроса точного прокси-объекта, к которому осуществляется доступ, он будет запрашивать больше владельцев Person одновременно, поэтому при доступе к владельцу другого человека он может быть уже инициализирован этой пакетной выборкой, и будет выполнено только несколько (намного меньше 25) запросов.

Итак, мы можем использовать эту аннотацию для обоих:

  • коллекции/наборы
  • классы/сущности

Проверьте это также здесь:

person Radim Köhler    schedule 06.01.2015

В этом методе запускается несколько SQL-запросов. Этот первый запускается для получения всех записей в родительской таблице. Остальные запускаются для получения записей для каждой родительской записи. Первый запрос извлекает M записей из базы данных, в данном случае M родительских записей. Для каждого родителя новый запрос извлекает дочерний элемент.

person Rohith K    schedule 06.01.2015