Статьи

Hibernate — Настройка запросов с использованием подкачки страниц, размера пакета и выборочных объединений

В этой статье рассматриваются запросы — в частности, настраиваемый тестовый пример и отношения между простыми запросами, запросами на выборку соединений, результатами запросов подкачки и размером пакета.

Пейджинг результатов запроса

Я начну с краткого введения в пейджинг EJB3:

Для поддержки подкачки интерфейс EJB3 Query определяет следующие два метода:

  • setMaxResults — устанавливает максимальное количество строк для извлечения из базы данных
  • setFirstResult — устанавливает первую строку для получения

Например, если в нашем графическом интерфейсе отображается список клиентов, а в нашей базе данных имеется 500 000 клиентов (строк базы данных), мы не хотели бы отображать все 500 000 записей в одном представлении (даже если мы оставим в стороне соображения производительности — никто не может ничего сделать с список 500 000 строк). Дизайн GUI обычно включает в себя разбиение на страницы — мы разбиваем список записей для отображения на логические страницы (например, 100 записей на страницу), и пользователь может перемещаться между страницами (так же, как навигатор результатов Google вниз по странице поиска).

При использовании поддержки подкачки важно помнить, что запрос должен быть отсортированв противном случае мы не можем быть уверены, что при извлечении «следующей страницы» это будет действительно следующая страница (так как при отсутствии предложения «order by» формируется SQL-запрос, порядок, в котором извлекаются строки, непредсказуем).

Вот пример использования для извлечения первых страниц буксира по 100 строк каждая:

        Query q = entityManager.createQuery("select c from Customer c order by c.id");
        q.setFirstResult(0).setMaxResults(100);

        .... next page ...

        Query q = entityManager.createQuery("select c from Customer c order by c.id");
        q.setFirstResult(100).setMaxResults(100);


Это простой API, и важно (для производительности) помнить об его использовании, когда нам нужно получить только часть результатов.

Описание теста

Этот тестовый случай основан на реальной настройке приложения, я просто изменил имена классов на Customer и Order. Давайте предположим, что у меня есть сущность Customer с набором заказов (лениво извлекается, но это также происходит в нетерпеливом извлечении), и нам нужно:


  1. Получить клиентов и их заказы
  2. Сделайте это в «режиме пейджинга» — 100 клиентов на страницу

Требование настройки № 1 — выборка клиентов и их заказов


Есть две возможности выполнить этот вид извлечения:

  • Простой выбор: выберите c из клиента c заказ по c.id
  • Регистрация выборки: выберите отчетливые с из клиента с левой внешним присоединиться принести заказ c.orders по c.id

Простой выбор настолько прост, насколько это возможно, мы загружаем список клиентов с прокси-коллекцией в поле их заказов. Коллекция заказов будет заполнена данными, как только я получу к ней доступ (например, c.getOrders (). GetSize ()). «Выборка соединения» означает, что мы хотим получить ассоциацию как неотъемлемую часть выполнения запроса. Объединенные извлеченные сущности (в примере выше: c.orders) должны быть частью ассоциации, на которую ссылается сущность, возвращаемая из запроса (в примере выше: c). «Выборка соединения» является одним из инструментов, используемых для повышения производительности запросов (подробнее см. Здесь ). В базовой документации Hibernate объясняется, что «выборочное» соединение позволяет инициализировать ассоциации или коллекции значений вместе с их родительскими объектами,используя один выбор «(см.здесь )


У меня в базе данных 18 998 записей о клиентах, каждая из которых имеет несколько заказов. Давайте сравним время выполнения двух запросов. Мой код выглядит одинаково для обоих запросов (кроме самого запроса), я выполняю запрос, затем повторяю результаты, проверяя размер каждой коллекции заказов клиентов и распечатываю время выполнения и количество извлекаемых записей (в качестве здравого смысла для синтаксис запроса):

        Query q = entityManager.createQuery(queryStr);

        long a = System.currentTimeMillis();
        List<Customer> l = q.getResultList();
        for (Customer c : l) {
            c.getOrders().size();
        }
        long b = System.currentTimeMillis();

        System.out.println("Execution time: " + (b - a)+ "; Number of records fetch: " + l.size() );


И на номера (ср. 3 исполнения):

  • Простой выбор: 24 984 миллис
  • Присоединиться выборка: 1219 миллис

Время выполнения запроса на получение соединения было в 20 раз быстрее (!), Чем простой запрос. Причина очевидна, используя выборку соединения, у меня была только одна поездка в базу данных. При использовании простого выбора мне приходилось выбирать клиентов (1 поездка в базу данных), и каждый раз, когда я обращался к коллекции, у меня была очередная поездка (это 18 998 дополнительных поездок!).


Победитель — «Присоединяйся». Но так ли это? ждать следующего — пейджинга …


Требование по настройке № 2 — используйте пейджинг

Второе требование заключалось в том, чтобы сделать это при подкачке страниц — на каждой странице будет 100 клиентов (поэтому у нас будет 18 900/100 + 1 страница — на последней странице 98 клиентов). Итак, давайте немного изменим код выше:

        Query q = entityManager.createQuery(queryStr);

          q.setFirstResult(pageNum*100).setMaxResults(100);

        long a = System.currentTimeMillis();
        List<Customer> l = q.getResultList();
        for (Customer c : l) {
            c.getOrders().size();
        }
        long b = System.currentTimeMillis();

        System.out.println("Execution time: " + (b - a)+ "; Number of records fetch: " + l.size() );

Я добавил вторую строку, которая ограничивает результат запроса конкретной страницей, содержащей до 100 записей на страницу. И цифры такие (в среднем 3 исполнения):

  • Простой выбор: 328 миллис
  • Присоединяйтесь: 1660 миллис

Колесо перевернулось. Почему? Сначала цитата из спецификации персистентности EJB3:



«Эффект применения setMaxResults или setFirstResult к запросу, включающему выборочные соединения по коллекциям, не определен» (раздел 3.6.1 — Интерфейс запроса)


Мы могли бы остановиться здесь, но интересно понять проблему и посмотреть, что делает Hibernate.


Для реализации функций подкачки Hibernate делегирует работу в базу данных, используя ее синтаксис для ограничения количества записей, выбираемых запросом. Каждая база данных имеет свой собственный синтаксис для ограничения количества извлекаемых записей, например:

  • Postgres использует LIMIT и OFFSET
  • Oracle имеет rownum
  • MySQL использует свою версию LIMIT и OFFSET
  • MSSQL имеет ключевое слово TOP в списке выбора
  • и так далее


Здесь важно помнить значение такого ограничения: база данных возвращает подмножество результата запроса . Таким образом, если мы запросим первые 100 клиентов, чьи имена содержат «Eyal», то результат будет логически таким же, как и построение таблицы в памяти из всех клиентов, которые соответствуют критериям и будут брать первые 100 строк. И здесь есть одна загвоздка: если запрос с ограничением включает в себя предложение объединения для коллекциичем первые 100 строк в «логической таблице» не обязательно будут первыми 100 клиентами. результат объединения может дублировать клиентов в «логических таблицах», но база данных не знает об этом или не заботится об этом — она ​​выполняет операции с таблицами, а не с объектами !. Например, подумайте о крайнем случае, у клиента Eyal есть 100 заказов. Запрос вернет 100 строк, hibernate определит, что все принадлежат одному и тому же клиенту, и вернет только одного клиента в качестве результата запроса — это не то, что мы просили.

Это также работает, конечно, наоборот. Если у клиента было более 100 заказов, а размер результирующего набора был ограничен 100 гнилями, сбор заказов не будет содержать все заказы клиента.

Чтобы справиться с этим ограничением, Hibernate на самом деле не выпускает оператор SQL с предложением LIMIT. Вместо этого он выбирает все записи и выполняет пейджинг в памяти. Это объясняет, почему использование оператора join fetch с подкачкой заняло больше, чем без подкачки — дельта — это подкачка в памяти, выполняемая Hibernate. Если вы посмотрите журналы Hibernate, то увидите следующее предупреждение, выданное Hibernate:


ВНИМАНИЕ: firstResult / maxResults указывается в выборке коллекции;
применяя в памяти!

Окончательная настройка — BatchSize

Означает ли это, что в случае пейджинга мы не должны использовать выборку соединения? обычно это происходит (если размер вашей страницы не очень близок к фактическому количеству записей). Но даже если вы используете простой выбор, это классический случай использования аннотации @BatchSize.

Если к моему менеджеру сеанса / сущности присоединено 100 клиентов, то по умолчанию для каждого первого доступа к одному из наборов заказов клиентов Hibernate будет выдавать оператор SQL для заполнения этой коллекции. В конце я выполню 100 операторов, чтобы получить 100 коллекций. Вы можете увидеть это в журнале:

Hibernate: / * выберите c из клиентского заказа c с помощью c.id * / выберите customer0_.id в качестве id0_, customer0_.ccNumber в качестве ccNumber0_, customer0_.name в качестве name0_, customer0_.fixedDiscount в качестве fixedDis5_0_, customer0_.DTYPE в качестве DTYPE0_ из заказа CUSTOMERS customer0_ лимитом customer0_.id? смещение?

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id =?
Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id =?
Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id =?
…………

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id =?

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id =?

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id =?










Аннотация @BatchSize может использоваться для определения количества идентичных ассоциаций для заполнения в одном запросе к базе данных. Если к сеансу подключено 100 клиентов, а отображение коллекции «заказов» помечено @BatchSize размера n . Это означает, что всякий раз, когда Hibernate нужно заполнить коллекцию отложенных заказов, он проверяет сеанс и, если у него больше клиентов, которым необходимо заполнить их коллекции заказов, он получает до nколлекции. Пример: если у нас было 100 клиентов, а размер пакета был установлен равным 16, при итерации по клиентам, чтобы получить их количество заказов, hibernate отправится в базу данных только 7 раз (6 раз, чтобы получить 16 коллекций, и еще один раз, чтобы получить 4 остальные коллекции — см. образец ниже). Если бы размер нашей партии был установлен на 50, она пошла бы только дважды.

@OneToMany(mappedBy="customer",cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    @BatchSize(size=16)
    private Set<Order> orders = new HashSet<Order>();


И в журнале:


Hibernate: / * выберите c из клиентского заказа c с помощью c.id * / выберите customer0_.id в качестве id0_, customer0_.ccNumber в качестве ccNumber0_, customer0_.name в качестве name0_, customer0_.fixedDiscount в качестве fixedDis5_0_, customer0_.DTYPE в качестве DTYPE0_ из заказа CUSTOMERS customer0_ лимитом customer0_.id? смещение?

Hibernate: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ от ORDERS orders0_, где orders0_.customer_id в
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ из ORDERS orders0_, где orders0_.customer_id в
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Hibernate: / * загрузить один -to-many par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве дескриптора2_1_0_, orders0_Iordes_0_0_0_0_0_0_Row_id где orders0_.customer_id в
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ из ORDERS orders0_, где orders0_.customer_id в
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Hibernate: / * загрузить один -to-many par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве дескриптора2_1_0_, orders0_Iordes_0_0_0_0_0_0_Row_id где orders0_.customer_id в
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Спящий режим: / * загрузить один-ко-многим par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве descript2_1_0_ orders. orderId как orderId1_0_ из ORDERS orders0_, где orders0_.customer_id в
(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)

Hibernate: / * загрузить один -to-many par2.Customer.orders * / выберите orders0_.customer_id в качестве customer4_1_, orders0_.id в качестве id1_, orders0_.id в качестве id1_0_, orders0_.customer_id в качестве customer4_1_0_, orders0_.description в качестве дескриптора2_1_0_, orders0_Iordes_0_0_0_0_0_0_Row_id где orders0_.customer_id в
(?,?,?,?)


Вернемся к нашему тесту. В моем примере установка размера партии в 100 выглядит как хорошая возможность настройки. И действительно, при установке значения 100 общее время выполнения уменьшилось до 188 миллис (это в 132 (!!!) раза быстрее, чем худший результат, который мы имели). Размер пакета также можно задать глобально, установив свойство hibernate.default_batch_fetch_size для фабрики сеансов.

С http://www.jroller.com/eyallupu/