Постраничный вывод
Блог с тысячами статей не может показать их все на одной странице. Пагинация (постраничный вывод) загружает по порции: статьи 1–10, потом 11–20 и так далее. В Hibernate это два метода запроса; нужные строки возвращает сама база, поэтому память не тратится впустую.
Страница статей
Пример создаёт 25 статей и показывает третью страницу по десять статей:
// src/main/java/com/example/blog/Main.java
package com.example.blog;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import java.util.List;
public class Main {
static final int PAGE_SIZE = 10;
public static void main(String[] args) {
try (EntityManagerFactory emf = Persistence.createEntityManagerFactory("blog")) {
emf.runInTransaction(em -> {
Author anna = new Author("Анна");
em.persist(anna);
for (int i = 1; i <= 25; i++) {
em.persist(new Post("Статья " + i, anna));
}
});
emf.runInTransaction(em -> {
long total = em.createQuery("select count(p) from Post p", Long.class).getSingleResult();
long pages = (total + PAGE_SIZE - 1) / PAGE_SIZE;
IO.println("Статей: " + total + ", страниц: " + pages);
int page = 3;
List<Post> posts = em.createQuery("select p from Post p order by p.id", Post.class)
.setFirstResult((page - 1) * PAGE_SIZE)
.setMaxResults(PAGE_SIZE)
.getResultList();
IO.println("Страница " + page + ": " + posts.stream().map(Post::getTitle).toList());
});
}
}
}Hibernate: select count(p1_0.id) from Post p1_0
Статей: 25, страниц: 3
Hibernate: select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 order by p1_0.id offset ? rows fetch first ? rows only
Страница 3: [Статья 21, Статья 22, Статья 23, Статья 24, Статья 25]| Метод | Значение |
|---|---|
setFirstResult(n) | Пропустить первые n строк; счёт с нуля |
setMaxResults(n) | Вернуть не больше n строк |
Номера страниц — ваше соглашение: для третьей страницы по десять строк нужно пропустить (3 - 1) * 10 = 20 строк. Hibernate переводит два метода в синтаксис вашей базы: offset ... fetch first ... rows only в H2 и PostgreSQL, limit ... offset ... в MySQL. Для общего числа страниц нужен отдельный запрос count.
Пагинация и join fetch
Совмещать страницу с join fetch коллекции непросто: после соединения каждая статья повторяется столько раз, сколько у неё комментариев, и «первые 2 строки» — это не «первые 2 статьи». Hibernate 7 решает это сам, через подзапрос:
// src/main/java/com/example/blog/FetchPage.java
List<Post> posts = em.createQuery("select p from Post p join fetch p.comments order by p.id", Post.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();Hibernate: select ... from (select p1_0.id,p1_0.author_id,p1_0.title,p1_0.views from Post p1_0 where exists(select 1 from Comment c1_0 where p1_0.id=c1_0.post_id) order by p1_0.id offset ? rows fetch first ? rows only) p1_0(id,author_id,title,views) join Comment c1_0 on p1_0.id=c1_0.post_id order by p1_0.id
[Привет, Hibernate, Типы record в Java 25]Внутренний запрос берёт две статьи, внешний добавляет их комментарии. Старые версии Hibernate вместо этого загружали все строки и вырезали страницу в памяти.
Далёкие страницы
offset заставляет базу прочитать и выбросить все пропущенные строки, поэтому страница номер 5000 работает медленно. Для бесконечных лент используют условие по последнему увиденному ключу: where p.id > :lastId order by p.id вместе с setMaxResults. Это называется пагинацией по ключу (keyset pagination).
Частая ошибка
Запрос страницы без order by возвращает строки в том порядке, который удобен базе. Порядок может меняться от запроса к запросу: одна статья попадает на две страницы, а другая не показывается вовсе. Сообщения об ошибке нет — баг видят только пользователи. Всегда сортируйте, причём по уникальному столбцу: если сортируете по дате, добавьте id вторым ключом.