Slice в Spring Data JPA: легка пагінація для infinite scroll у Spring Boot 3

Оновлено:
Slice в Spring Data JPA: легка пагінація для infinite scroll у Spring Boot 3

Більшість розробників за замовчуванням обирають Page<T> для пагінації — і платять за це зайвим COUNT(*) запитом при кожному scroll-івенті. Slice вирішує саме цю проблему: повертає наступну порцію даних без підрахунку загальної кількості рядків — і саме тому є правильним вибором для infinite scroll у продакшені.

⚡ Коротко

  • Slice не робить COUNT(*): менше навантаження на БД при кожному запиті
  • hasNext(): замість totalPages — просто «є ще дані чи ні»
  • Ідеально для: infinite scroll, мобільних API, соцмереж, стрічок товарів
  • Не підходить для: адмін-панелей де потрібна навігація по сторінках
  • 👇 Нижче — механіка, код і SQL під капотом

📚 Зміст

Інші статті серії:
📌 List в Spring Data JPA — коли безпечно і коли це ризик
📌 Page в Spring Data JPA — пагінація з COUNT і countQuery
📌 Specification — динамічні фільтри без combinatorial explosion
📌 List vs Page vs Slice vs Specification — повний гід у Spring Data JPA

🎯 Розділ 1. Чому Page не підходить для infinite scroll

Коротка відповідь: прихований COUNT(*) при кожному запиті

Кожен виклик методу що повертає Page<T> генерує два SQL-запити: основний SELECT і окремий COUNT(*) для підрахунку загальної кількості рядків. Для infinite scroll цей COUNT не потрібен — користувачу не важливо скільки всього товарів, йому важливо чи є наступна порція. При таблиці в 1M+ рядків і фільтрах цей overhead стає відчутним bottleneck при кожному scroll-івенті.

Page відповідає на питання «скільки всього сторінок». Infinite scroll ставить інше питання: «є щось далі?». Це різні задачі — і різні інструменти.

Що генерує Spring Data для Page<T>

Розглянемо стандартний Repository-метод:

Page<Product> findAllByOrderByCreatedAtDesc(Pageable pageable);

При кожному виклику Hibernate генерує два окремих SQL-запити незалежно від того чи потрібен вам COUNT:

-- 1. Основний запит — повертає дані
SELECT p.id, p.name, p.price, p.created_at
FROM products p
ORDER BY p.created_at DESC
LIMIT 20 OFFSET 40;

-- 2. COUNT-запит — виконується завжди, автоматично
SELECT COUNT(p.id)
FROM products p;

Другий запит не можна відключити через конфігурацію — він є частиною контракту Page<T>. Якщо ви повертаєте Page, COUNT виконується завжди.

Як COUNT деградує при реальних даних

Проблема не в самому COUNT — вона в тому, як він поводиться при зростанні даних і складності запиту.

Сценарій 1 — проста таблиця без фільтрів. На таблиці products без WHERE-умов PostgreSQL може виконати COUNT(*) через index-only scan на первинному ключі. При 100K рядків — мілісекунди. Проблема майже непомітна.

EXPLAIN ANALYZE SELECT COUNT(p.id) FROM products p;
-- Index Only Scan using products_pkey on products
-- (cost=0.42..1893.42 rows=100000) — прийнятно

Сценарій 2 — фільтрація по категорії і статусу. Як тільки з'являється WHERE — COUNT змушений пройти по всіх рядках що відповідають умові:

-- Реальний запит для каталогу з фільтрами
SELECT COUNT(p.id)
FROM products p
WHERE p.category_id = 5
  AND p.status = 'ACTIVE'
  AND p.price BETWEEN 100 AND 500;

-- EXPLAIN при 5M рядків без composite index:
-- Seq Scan on products
-- Filter: (category_id = 5 AND status = 'ACTIVE' AND price BETWEEN 100 AND 500)
-- Rows Removed by Filter: 4850000  <-- читає майже всю таблицю
-- Execution Time: 1200ms

1.2 секунди на COUNT при кожному scroll-івенті. При 100 активних користувачів одночасно — це 100 важких запитів у секунду тільки для підрахунку, який ніхто не бачить в UI.

Сценарій 3 — JOIN з іншими таблицями. Якщо запит включає JOIN (наприклад, товари з їх категоріями і тегами), COUNT-запит Spring Data часто не може оптимізуватись і виконує повний JOIN:

-- Spring Data генерує COUNT з тим самим JOIN що і основний запит
SELECT COUNT(DISTINCT p.id)
FROM products p
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
WHERE t.name IN ('sale', 'new');
-- При великих таблицях тегів — дорогий запит при кожному scroll

У таких випадках часто доводиться писати окремий countQuery в @Query щоб оптимізувати COUNT — але це додаткова складність яку можна повністю уникнути використовуючи Slice.

Чому це особливо критично для infinite scroll

У класичній сторінковій пагінації (адмін-панель, пошукова видача) COUNT виправданий: він потрібен для відображення «Сторінка 3 з 47» або «Знайдено 234 результати». Користувач бачить цю інформацію і вона несе цінність.

У infinite scroll ця інформація не відображається взагалі. Instagram не показує «пост 847 з 23 419». Amazon при прокрутці каталогу не оновлює лічильник при кожному підвантаженні. Все що потрібно UI — булеве значення: підвантажувати ще чи зупинитись.

При цьому scroll-івенти відбуваються часто — кожні 1–3 секунди при активній прокрутці. Тобто COUNT виконується з максимальною частотою саме тоді, коли навантаження на API найвище.

Коли Page все ж виправданий

Щоб бути точним: Page — правильний інструмент коли UI дійсно потребує загальної кількості. Конкретні сценарії:

  • Адмін-панель з numbered pagination («Сторінка 3 з 47»)
  • Пошукова видача з лічильником результатів («Знайдено 1 234 товари»)
  • Звіти і таблиці де користувач може перейти на конкретну сторінку
  • Будь-який UI з totalPages або totalElements в відповіді

🎯 Як працює Slice — механіка без COUNT

Коротка відповідь: Slice запитує N+1 рядків замість N

Щоб визначити чи є наступна сторінка, Slice використовує просту механіку: запитує на один рядок більше ніж потрібно (pageSize + 1). Якщо прийшло більше ніж pageSize рядків — hasNext() повертає true і зайвий рядок відкидається. Жодного окремого COUNT-запиту не відбувається.

Замість «порахуй все» — «принеси на один більше і перевір». Це O(1) overhead замість O(n) для COUNT на великій таблиці.

Механіка pageSize + 1 зсередини

  1. Hibernate виконує SELECT з LIMIT = pageSize + 1
  2. Якщо результат містить більше ніж pageSize елементів — встановлюється hasNext = true
  3. Зайвий (pageSize + 1)-й елемент відкидається — в getContent() він не потрапляє
  4. Якщо результат менший або рівний pageSize — це остання сторінка, hasNext = false

Наочно для pageSize = 3 — запитуємо 4 рядки:

SELECT * FROM products ORDER BY created_at DESC LIMIT 4;

-- Випадок 1: БД повернула 4 рядки
-- → hasNext() = true
-- → getContent() = [row1, row2, row3]  // 4-й відкидається

-- Випадок 2: БД повернула 2 рядки
-- → hasNext() = false
-- → getContent() = [row1, row2]        // всі рядки в результаті

-- Випадок 3: БД повернула рівно 3 рядки
-- → hasNext() = false
-- → getContent() = [row1, row2, row3]  // це остання сторінка

Overhead від цього підходу мінімальний: один зайвий рядок читається і передається по мережі між БД і додатком — і одразу відкидається в пам'яті JVM. Порівняно з COUNT(*) по мільйонам рядків це несуттєво.

SQL порівняння: Page vs Slice для однієї сторінки

Розглянемо що відбувається в БД при запиті сторінки 3 (0-based) з розміром 20:

-- Page<Product> — два запити до БД:

-- Запит 1: основні дані
SELECT p.id, p.name, p.price, p.created_at
FROM products p
ORDER BY p.created_at DESC
LIMIT 20 OFFSET 40;

-- Запит 2: COUNT — завжди, незалежно від потреби
SELECT COUNT(p.id)
FROM products p;


-- Slice<Product> — один запит до БД:

SELECT p.id, p.name, p.price, p.created_at
FROM products p
ORDER BY p.created_at DESC
LIMIT 21 OFFSET 40;  -- pageSize + 1, без COUNT

При 100 scroll-івентах за хвилину Page генерує 200 запитів до БД, Slice — 100. Половина запитів зникає без жодних змін в бізнес-логіці.

Інтерфейс Slice<T> — що є і чого немає

Slice<T> надає мінімально необхідний контракт для cursor-based навігації:

// Доступні методи Slice<T>
slice.getContent()       // List<T> — поточна порція даних
slice.hasNext()          // true якщо є наступна сторінка
slice.hasPrevious()      // true якщо є попередня сторінка
slice.isFirst()          // true якщо це перша сторінка
slice.isLast()           // true якщо це остання сторінка
slice.getNumber()        // номер поточної сторінки (0-based)
slice.getSize()          // розмір сторінки (те що запросили)
slice.getNumberOfElements() // скільки реально повернулось
slice.getSort()          // поточне сортування
slice.nextPageable()     // Pageable для наступного запиту
slice.previousPageable() // Pageable для попереднього запиту

Чого немає — і саме тому немає COUNT:

// Ці методи є тільки в Page<T>, не в Slice<T>
page.getTotalElements()  // потребує COUNT(*)
page.getTotalPages()     // обчислюється з COUNT(*)

Це не обмеження — це свідоме архітектурне рішення. Slice не може повернути totalElements бо він принципово не виконує запит для їх підрахунку.

Page розширює Slice — ієрархія інтерфейсів

Важливий нюанс для розуміння: в Spring Data Page<T> розширює Slice<T>, а не навпаки:

public interface Slice<T> extends Streamable<T> {
    boolean hasNext();
    List<T> getContent();
    // ... базова навігація
}

public interface Page<T> extends Slice<T> {
    long getTotalElements(); // додає COUNT
    int getTotalPages();     // додає COUNT
}

Це означає: Page є повним Slice плюс два методи що вимагають COUNT. Якщо ваш сервісний метод повертає Slice<T> — ви можете безболісно передати туди PageImpl або SliceImpl. І навпаки: метод що очікує Page<T> не прийме Slice<T> без явного каста.

hasNext() у JSON-відповіді — мінімальний контракт для фронтенду

Для infinite scroll фронтенду потрібно рівно одне булеве значення. Типовий цикл підвантаження:

// Псевдокод на фронтенді
let currentPage = 0;
let isLoading = false;

async function loadMore() {
    if (isLoading) return;
    isLoading = true;

    const response = await fetch(`/api/products?page=${currentPage}&size=20`);
    const data = await response.json();

    renderProducts(data.content);

    if (data.hasNext) {
        currentPage++;
        // показати кнопку "Завантажити ще" або тригерити автоматично
    } else {
        //ховати індикатор завантаження — більше даних немає
    }

    isLoading = false;
}

totalPages і totalElements в цій логіці не потрібні взагалі — і тому генерувати їх через COUNT немає сенсу.

🎯 Реалізація у Spring Boot 3 — від Repository до REST

Коротка відповідь: достатньо змінити тип повернення на Slice<T>

Spring Data автоматично розпізнає Slice<T> як тип повернення і генерує відповідний запит без COUNT. Жодних додаткових налаштувань не потрібно — тільки правильний тип у сигнатурі методу Repository.

Entity:

@Entity
@Table(name = "products")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private BigDecimal price;

    @Column(name = "created_at")
    private LocalDateTime createdAt;
}

Repository — достатньо одного рядка:

public interface ProductRepository extends JpaRepository<Product, Long> {

    // Spring Data генерує SELECT без COUNT автоматично
    Slice<Product> findAllByOrderByCreatedAtDesc(Pageable pageable);
}

Service:

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    public Slice<Product> getProducts(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        return productRepository.findAllByOrderByCreatedAtDesc(pageable);
    }
}

Controller + DTO для відповіді:

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {

    private final ProductService productService;

    @GetMapping
    public SliceResponse<Product> getProducts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {

        Slice<Product> slice = productService.getProducts(page, size);

        return new SliceResponse<>(
            slice.getContent(),
            slice.hasNext(),
            slice.getNumber()
        );
    }
}

// DTO — тільки те що потрібно клієнту
public record SliceResponse<T>(
    List<T> content,
    boolean hasNext,
    int currentPage
) {}

JSON-відповідь яку отримує фронтенд:

{
  "content": [...],   // масив товарів
  "hasNext": true,    // чи підвантажувати ще
  "currentPage": 0
}

Фронтенд при scroll до кінця сторінки перевіряє hasNext і якщо true — робить запит з page + 1. Просто і без зайвої логіки.

🎯 SQL під капотом і production-нюанси

Коротка відповідь: OFFSET деградує, індекс на сортувальному полі — обов'язковий

Slice вирішує проблему COUNT, але не вирішує проблему OFFSET на великих таблицях.

При глибокій прокрутці (OFFSET 10000+) PostgreSQL все одно читає

і відкидає тисячі рядків перед поверненням потрібних. Для більшості

інтернет-магазинів і соцмереж це не критично — реальні користувачі рідко

гортають далі 200–300 елементів. Але знати про це потрібно.

Slice — правильний крок від Page: прибирає COUNT.

Cursor-based pagination — наступний крок від Slice: прибирає OFFSET.

Для більшості продуктів Slice достатньо.

EXPLAIN ANALYZE: без індексу vs з індексом

Розглянемо що відбувається в PostgreSQL для першої сторінки Slice (OFFSET 0):

EXPLAIN ANALYZE

SELECT p.id, p.name, p.price, p.created_at

FROM products p

ORDER BY p.created_at DESC

LIMIT 21 OFFSET 0;

Без індексу на created_at:

Sort (cost=14523.45..14773.45 rows=100000)

Sort Key: created_at DESC

Sort Method: external merge Disk: 8432kB <-- сортує всю таблицю на диску

-> Seq Scan on products (cost=0.00..1834.00 rows=100000)

Planning Time: 0.8 ms

Execution Time: 312 ms <-- 312ms для першої сторінки

З індексом CREATE INDEX idx_products_created_at ON products(created_at DESC):

Limit (cost=0.42..2.14 rows=21)

-> Index Scan Backward using idx_products_created_at on products

(cost=0.42..8195.42 rows=100000)

Planning Time: 0.3 ms

Execution Time: 0.08 ms <-- в 3900 разів швидше

Різниця між 312ms і 0.08ms — це різниця між full table sort і direct index walk.

Індекс на полі сортування є обов'язковою умовою, не опціональною оптимізацією.

Без нього Slice на таблиці 100K+ рядків буде повільнішим ніж здається.

-- Flyway / Liquibase міграція

CREATE INDEX idx_products_created_at ON products(created_at DESC);

-- Для composite фільтрів — partial або composite index

-- Якщо завжди фільтруєте по status = 'ACTIVE':

CREATE INDEX idx_products_active_created

ON products(created_at DESC)

WHERE status = 'ACTIVE';

Деградація OFFSET: коли це стає проблемою

OFFSET змушує PostgreSQL прочитати і відкинути всі рядки до вказаної позиції —

навіть якщо є індекс. При OFFSET 0 це безкоштовно.

При глибокій прокрутці вартість зростає лінійно:

-- page=0,   size=20 → OFFSET 0    → читає 21 рядок

-- page=10, size=20 → OFFSET 200 → читає 221 рядок

-- page=100, size=20 → OFFSET 2000 → читає 2021 рядок

-- page=500, size=20 → OFFSET 10000→ читає 10021 рядок

EXPLAIN ANALYZE

SELECT p.id FROM products p

ORDER BY p.created_at DESC

LIMIT 21 OFFSET 10000;

-- Index Scan Backward using idx_products_created_at on products

-- Rows Removed by OFFSET: 10000 <-- зчитує і відкидає 10000 рядків

-- Execution Time: 28 ms <-- проти 0.08ms для OFFSET 0

28ms проти 0.08ms — деградація в 350 разів при OFFSET 10000.

При OFFSET 100000 це буде вже ~280ms навіть з індексом.

Чому для більшості продуктів це не критично:

реальні користувачі в інтернет-магазині або соцмережі рідко гортають

далі 5–10 сторінок. При size=20 це максимум

OFFSET 200 — де деградація ще несуттєва (менше 1ms).

Моніторте реальний розподіл значень page у вашому API

— якщо 99-й перцентиль не перевищує 50, offset-based Slice цілком достатній.

Cursor-based pagination: коли і як переходити

Якщо аналітика показує що користувачі регулярно гортають глибоко,

або таблиця перевищує 10M рядків — варто розглянути cursor-based підхід.

Ідея: замість page=500 передавати значення останнього

побаченого елемента як курсор.

// Cursor-based Repository метод

public interface ProductRepository extends JpaRepository<Product, Long> {

// Замість OFFSET — WHERE created_at < :cursor

Slice<Product> findByCreatedAtBeforeOrderByCreatedAtDesc(

LocalDateTime cursor,

Pageable pageable

);

}

SQL що генерується — без OFFSET, завжди O(log n) з індексом:

SELECT p.id, p.name, p.price, p.created_at

FROM products p

WHERE p.created_at < '2024-03-01 12:00:00' -- курсор з попереднього запиту

ORDER BY p.created_at DESC

LIMIT 21;

-- Index Scan Backward using idx_products_created_at

-- Index Cond: (created_at < '2024-03-01 12:00:00')

-- Execution Time: 0.08 ms -- незалежно від глибини прокрутки

Controller з cursor-based підходом:

@GetMapping

public SliceResponse<Product> getProducts(

@RequestParam(required = false) String cursor, // ISO timestamp або null

@RequestParam(defaultValue = "20") int size) {

Pageable pageable = PageRequest.of(0, size); // page завжди 0

Slice<Product> slice = (cursor != null)

? productRepository.findByCreatedAtBeforeOrderByCreatedAtDesc(

LocalDateTime.parse(cursor), pageable)

: productRepository.findAllByOrderByCreatedAtDesc(pageable);

// Наступний курсор = created_at останнього елемента

String nextCursor = slice.hasNext()

? slice.getContent().get(slice.getContent().size() - 1)

.getCreatedAt().toString()

: null;

return new SliceResponse<>(slice.getContent(), slice.hasNext(), nextCursor);

}

JSON-відповідь з cursor:

{

"content": [...],

"hasNext": true,

"nextCursor": "2024-02-28T10:30:00" // фронтенд передає у наступному запиті

}

Типові помилки в production

1. Сортування по полю без індексу.

Найчастіша причина повільного Slice — відсутність індексу на ORDER BY полі.

Перевіряйте EXPLAIN перед деплоєм. Будь-який Slice-запит без Index Scan

в EXPLAIN — привід для міграції.

2. Нестабільне сортування.

Якщо два рядки мають однакове значення сортувального поля,

порядок між ними не детермінований. При прокрутці користувач може

побачити дублікати або пропущені елементи. Завжди додавайте

вторинне сортування по унікальному полю:

// Не стабільно: два продукти можуть мати однаковий created_at

Slice<Product> findAllByOrderByCreatedAtDesc(Pageable pageable);

// Стабільно: додаємо id як вторинний ключ сортування

Slice<Product> findAllByOrderByCreatedAtDescIdDesc(Pageable pageable);

-- SQL:

-- ORDER BY created_at DESC, id DESC -- детермінований порядок завжди

3. Відсутність ліміту на розмір сторінки.

Клієнт може передати size=10000. Валідуйте

Pageable на рівні контролера або через

@PageableDefault і @Max:

@GetMapping

public SliceResponse<Product> getProducts(

@RequestParam(defaultValue = "0") int page,

@RequestParam(defaultValue = "20") @Max(100) int size) {

Pageable pageable = PageRequest.of(page, Math.min(size, 100));

// ...

}

🎯 Коли Slice, коли Page, коли List

Коротка відповідь: один критерій — чи потрібна загальна кількість

Якщо UI показує «Сторінка 3 з 47» або «Знайдено 234 результати» — потрібен Page.

Якщо UI показує кнопку «Завантажити ще» або автоматичний infinite scroll — достатньо Slice.

Якщо дані гарантовано малі і завжди завантажуються повністю — підійде List.

КритерійListPageSlice
COUNT(*) запит❌ немає✅ завжди❌ немає
totalPages / totalElements
hasNext()
Infinite scroll⚠️ тільки малі дані⚠️ overhead✅ оптимально
Адмін-панель з навігацією
Мобільний API⚠️⚠️
Ризик OutOfMemory⚠️ без LIMIT✅ захищений✅ захищений
Продуктивність на 1M+ рядків⚠️ COUNT overhead

❓ FAQ

Чи можна використовувати Slice з JPQL або @Query?

Так. Spring Data розпізнає Slice<T> як тип повернення і для

кастомних запитів через @Query. Важливо: не додавайте

countQuery — він не потрібен і не буде виконуватись.

Hibernate сам додасть LIMIT pageSize + 1 до вашого запиту.

@Query("SELECT p FROM Product p WHERE p.category = :category ORDER BY p.createdAt DESC")

Slice<Product> findByCategory(@Param("category") String category, Pageable pageable);

Чому Slice повертає на 1 елемент менше ніж я запитав?

Це нормальна поведінка. Slice запитує pageSize + 1 рядків щоб

перевірити hasNext(), але повертає в getContent()

рівно pageSize елементів — зайвий відкидається всередині

SliceImpl. Якщо отримали менше ніж pageSize

це остання сторінка, hasNext() = false.

Slice підтримує сортування?

Так, через PageRequest.of(page, size, Sort.by("createdAt").descending())

або через ім'я методу як у прикладі вище. Сортування є обов'язковим для

стабільної пагінації — без ORDER BY порядок рядків не гарантований і

користувач може побачити дублікати або пропущені елементи при прокрутці.

Slice і Spring Data Reactive (R2DBC) — працює?

Так. Flux є більш природним підходом для реактивного стеку,

але Slice<T> підтримується через ReactiveSortingRepository.

При цьому механіка pageSize + 1 зберігається — COUNT не виконується

і в реактивному варіанті.

Коли Slice гірший за Page?

Коли UI потребує точної навігації: «Перейти на сторінку 15», відображення

«Показано 41–60 з 234 результатів», або будь-яка форма numbered pagination.

Slice не знає загальної кількості — тому для адмін-панелей, звітів і

пошукових результатів з лічильником Page залишається правильним вибором.

✅ Висновки

Slice<T> — це правильний інструмент для infinite scroll

у Spring Data JPA. Він вирішує конкретну проблему: прибирає зайвий

COUNT(*) запит який Page виконує при кожному виклику.

Реалізація мінімальна — достатньо змінити тип повернення в Repository.

  • Slice генерує один SQL-запит з LIMIT pageSize + 1 замість двох
  • hasNext() — єдине що потрібно фронтенду для infinite scroll
  • Індекс на полі сортування обов'язковий для production-продуктивності
  • При таблицях 1M+ рядків і глибокій прокрутці — розгляньте cursor-based підхід
  • Для адмін-панелей з numbered pagination Slice не підходить — там потрібен Page

Наступні статті серії:

📌 Page в Spring Data JPA — повноцінна пагінація, COUNT overhead і коли він виправданий

📌 List в Spring Data JPA — ризики без LIMIT і коли це прийнятно

📌 Specification — динамічні фільтри без combinatorial explosion у derived methods

Джерела:

Spring Data JPA — Special Parameters

Spring Data Commons — Slice JavaDoc

PostgreSQL — EXPLAIN документація

🌟 З повагою

Вадим Харовюк

☕ Java розробник, засновник WebCraft

Останні статті

Читайте більше цікавих матеріалів

List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

List vs Page vs Slice vs Specification у Spring Data JPA: повний гід Spring Boot 3

Spring Data JPA пропонує чотири принципово різних підходи до отримання даних.Кожен з них генерує різний SQL, по-різному поводиться з пам'яттюі вирішує різний клас задач. Неправильний вибір — це або зайвийCOUNT(*) при кожному scroll-івенті, або OOM при зростанні таблиці,або 64 derived methods...

List в Spring Data JPA: коли безпечно і коли це ризик — Spring Boot 3

List в Spring Data JPA: коли безпечно і коли це ризик — Spring Boot 3

List&lt;T&gt; — найпростіший тип повернення в Spring Data JPA. Саме тому він найчастіше використовується там де не повинен. Відсутність LIMIT у згенерованому SQL означає: розмір результату нічим не обмежений. При таблиці на 100K+ рядків це пряма дорога до OutOfMemory або...

Specification в Spring Data JPA: динамічні запити без combinatorial explosion — Spring Boot 3

Specification в Spring Data JPA: динамічні запити без combinatorial explosion — Spring Boot 3

Коли фільтрів в адмін-панелі стає більше трьох — derived methods перетворюютьсяна комбінаторний вибух: 4 опціональних фільтри дають 16 можливих комбінаційі стільки ж методів у Repository. Specification вирішує цю проблему:один метод, будь-яка комбінація фільтрів, чистий SQL під капотом.⚡ Коротко✅...

Page в Spring Data JPA: повноцінна пагінація з COUNT — Spring Boot 3

Page в Spring Data JPA: повноцінна пагінація з COUNT — Spring Boot 3

Page&lt;T&gt; — найпопулярніший тип пагінації в Spring Data JPA, і водночас найчастіше використовуваний там де він не потрібен. Він виконує два SQL-запити при кожному виклику: основний SELECT і COUNT(*). Ключове питання не «як використовувати Page», а «коли він...

Slice в Spring Data JPA: легка пагінація для infinite scroll у Spring Boot 3

Slice в Spring Data JPA: легка пагінація для infinite scroll у Spring Boot 3

Більшість розробників за замовчуванням обирають Page&lt;T&gt; для пагінації — і платять за це зайвим COUNT(*) запитом при кожному scroll-івенті. Slice вирішує саме цю проблему: повертає наступну порцію даних без підрахунку загальної кількості рядків — і саме тому є...

OpenAI випустив GPT-5.4: що змінилось  в 2026

OpenAI випустив GPT-5.4: що змінилось в 2026

5 березня 2026 року OpenAI випустив GPT-5.4 — одночасно у ChatGPT, API і Codex.Це не черговий incremental update: модель вперше об'єднує coding pipeline GPT-5.3-Codexіз загальним reasoning, отримує native computer use і контекстне вікно до 1M токенів.Коротко: якщо ви будуєте агентні воркфлоу або...