REST APIで大量のデータを扱う場合、すべてのレコードを一度に返却するのは現実的ではありません。Spring Data JPAが提供するPageableインターフェースを活用することで、効率的なページネーションとソート機能を簡潔に実装できます。本記事では、Pageableパラメータの受け取り方からPageSliceの違い、クライアント向けのカスタムレスポンス設計まで、実践的なページネーション実装を解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Data JPA 3.4.x(Spring Boot Starterに含まれる)
データベース H2 Database(開発・テスト用インメモリDB)
ビルドツール Maven または Gradle

事前に以下の準備を完了してください。

  • JDK 17以上のインストール
  • Spring Boot REST APIプロジェクトの基本構成(REST API入門記事を参照)
  • Spring Data JPAとエンティティの基本設定(JPA入門記事を参照)

ページネーションの基本概念

ページネーションとは、大量のデータを複数のページに分割して取得する仕組みです。Spring Data JPAでは、Pageableインターフェースを使用してページ番号、ページサイズ、ソート条件を指定できます。

ページネーション関連の主要インターフェース

インターフェース 説明
Pageable ページ番号、ページサイズ、ソート条件を保持するリクエスト情報
Page<T> ページングされた結果と総件数・総ページ数を含むレスポンス
Slice<T> ページングされた結果と次ページの有無のみを含む軽量レスポンス
Sort ソート条件(プロパティ名と昇順/降順)を定義

Pageableパラメータの受け取り方

Spring MVCでは、コントローラーのメソッド引数にPageableを指定するだけで、リクエストパラメータから自動的にページング情報を取得できます。

基本的なControllerの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.demoapi.controller;

import com.example.demoapi.entity.Task;
import com.example.demoapi.service.TaskService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @GetMapping
    public Page<Task> getTasks(Pageable pageable) {
        return taskService.findAll(pageable);
    }
}

デフォルトのリクエストパラメータ

Pageableは以下のクエリパラメータを自動的に解釈します。

パラメータ 説明 デフォルト値
page ページ番号(0から開始) 0
size 1ページあたりの件数 20
sort ソート条件(プロパティ名,方向) なし

以下のようなリクエストでページネーションを制御できます。

GET /api/tasks?page=0&size=10
GET /api/tasks?page=1&size=20&sort=createdAt,desc
GET /api/tasks?page=0&size=10&sort=title,asc&sort=createdAt,desc

デフォルト値のカスタマイズ

@PageableDefaultアノテーションを使用して、デフォルトのページサイズやソート条件を指定できます。

1
2
3
4
5
6
7
8
9
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;

@GetMapping
public Page<Task> getTasks(
        @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC)
        Pageable pageable) {
    return taskService.findAll(pageable);
}

ページサイズの上限設定

悪意のあるリクエストで極端に大きなページサイズが指定されることを防ぐため、application.propertiesで上限を設定できます。

1
2
3
4
# ページネーションのデフォルト設定
spring.data.web.pageable.default-page-size=20
spring.data.web.pageable.max-page-size=100
spring.data.web.pageable.one-indexed-parameters=false
設定項目 説明
default-page-size sizeパラメータ未指定時のデフォルト値
max-page-size sizeパラメータの最大許容値
one-indexed-parameters trueにするとページ番号が1から開始

ソート条件の指定方法

Pageableに含まれるSortオブジェクトを使用して、動的なソートを実現できます。

クエリパラメータによるソート指定

複数のソート条件を指定する場合は、sortパラメータを複数回指定します。

GET /api/tasks?sort=completed,asc&sort=createdAt,desc

この例では、まずcompletedで昇順ソートし、同じ値の場合はcreatedAtで降順ソートします。

Repositoryでのソート対応

JpaRepositoryを継承したリポジトリでは、Pageableを引数に取るメソッドを定義するだけでソートに対応できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.demoapi.repository;

import com.example.demoapi.entity.Task;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {

    // Pageableを受け取ることでページングとソートに対応
    Page<Task> findByCompleted(boolean completed, Pageable pageable);

    // タイトルで部分一致検索(ページング対応)
    Page<Task> findByTitleContaining(String title, Pageable pageable);
}

Sortオブジェクトの直接使用

コントローラーやサービス層でSortオブジェクトを明示的に作成することも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import org.springframework.data.domain.Sort;

// 単一条件でのソート
Sort sort = Sort.by("createdAt").descending();

// 複数条件でのソート
Sort multiSort = Sort.by("completed").ascending()
        .and(Sort.by("createdAt").descending());

// タイプセーフなソート定義
Sort typedSort = Sort.sort(Task.class)
        .by(Task::getCreatedAt).descending();

PageとSliceの違い

Spring Data JPAは、ページング結果を返すためにPageSliceという2つのインターフェースを提供しています。用途に応じて適切な方を選択することが重要です。

Pageインターフェース

Pageは総件数と総ページ数を含む完全なページング情報を提供します。

1
2
3
public interface TaskRepository extends JpaRepository<Task, Long> {
    Page<Task> findAll(Pageable pageable);
}

Pageオブジェクトが提供する主要なメソッドは以下のとおりです。

メソッド 説明
getContent() 現在のページのデータリスト
getTotalElements() 全件数
getTotalPages() 総ページ数
getNumber() 現在のページ番号(0始まり)
getSize() ページサイズ
hasNext() 次のページが存在するか
hasPrevious() 前のページが存在するか

Sliceインターフェース

Sliceは次のページが存在するかどうかのみを判定し、総件数を計算しません。これにより、大量データの場合にCOUNTクエリを省略できます。

1
2
3
public interface TaskRepository extends JpaRepository<Task, Long> {
    Slice<Task> findByCompleted(boolean completed, Pageable pageable);
}

Sliceオブジェクトが提供する主要なメソッドは以下のとおりです。

メソッド 説明
getContent() 現在のページのデータリスト
getNumber() 現在のページ番号(0始まり)
getSize() ページサイズ
hasNext() 次のページが存在するか
hasPrevious() 前のページが存在するか

PageとSliceの使い分け

flowchart TD
    A[ページネーション実装] --> B{総件数・総ページ数が必要?}
    B -->|はい| C[Page を使用]
    B -->|いいえ| D{無限スクロールUI?}
    D -->|はい| E[Slice を使用]
    D -->|いいえ| F{データ量が多い?}
    F -->|はい| E
    F -->|いいえ| C
    C --> G[COUNTクエリが発行される]
    E --> H[COUNTクエリが省略される]
ユースケース 推奨 理由
管理画面のデータ一覧 Page 総件数を表示することが多い
無限スクロールUI Slice 総件数不要、次があるかだけ判定
大量データの一覧取得 Slice COUNTクエリのコスト削減
ページ番号ナビゲーション Page 総ページ数が必要

実行されるSQLの違い

Pageを使用した場合、データ取得クエリに加えてCOUNTクエリが発行されます。

1
2
3
4
5
-- データ取得クエリ
SELECT * FROM tasks ORDER BY created_at DESC LIMIT 10 OFFSET 0;

-- COUNTクエリ(Pageの場合のみ)
SELECT COUNT(*) FROM tasks;

Sliceを使用した場合、リクエストされたサイズより1件多く取得し、次のページの有無を判定します。

1
2
-- サイズ+1件を取得して次ページの有無を判定
SELECT * FROM tasks ORDER BY created_at DESC LIMIT 11 OFFSET 0;

クライアント向けレスポンス形式の設計

Spring Data JPAが返すPageオブジェクトをそのままJSONレスポンスとして返却することも可能ですが、APIの一貫性やクライアントの利便性を考慮してカスタムレスポンスを設計することを推奨します。

デフォルトのPageレスポンス構造

Pageオブジェクトをそのまま返却した場合のJSONレスポンスは以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
  "content": [
    {
      "id": 1,
      "title": "タスク1",
      "description": "説明1",
      "completed": false,
      "createdAt": "2026-01-04T10:00:00",
      "updatedAt": null
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 10,
    "sort": {
      "sorted": true,
      "unsorted": false,
      "empty": false
    },
    "offset": 0,
    "paged": true,
    "unpaged": false
  },
  "totalElements": 50,
  "totalPages": 5,
  "last": false,
  "first": true,
  "size": 10,
  "number": 0,
  "sort": {
    "sorted": true,
    "unsorted": false,
    "empty": false
  },
  "numberOfElements": 10,
  "empty": false
}

このレスポンスは情報量が多く冗長なため、クライアントにとって扱いやすい形式にカスタマイズすることを検討します。

カスタムページネーションレスポンスの設計

クライアントが必要とする情報のみを含むカスタムレスポンスクラスを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.demoapi.dto;

import org.springframework.data.domain.Page;
import java.util.List;

public record PageResponse<T>(
        List<T> content,
        PageInfo page
) {
    public record PageInfo(
            int number,
            int size,
            long totalElements,
            int totalPages,
            boolean first,
            boolean last,
            boolean hasNext,
            boolean hasPrevious
    ) {}

    public static <T> PageResponse<T> of(Page<T> page) {
        return new PageResponse<>(
                page.getContent(),
                new PageInfo(
                        page.getNumber(),
                        page.getSize(),
                        page.getTotalElements(),
                        page.getTotalPages(),
                        page.isFirst(),
                        page.isLast(),
                        page.hasNext(),
                        page.hasPrevious()
                )
        );
    }
}

Controllerでのカスタムレスポンス使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @GetMapping
    public PageResponse<TaskResponse> getTasks(
            @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC)
            Pageable pageable) {
        Page<Task> taskPage = taskService.findAll(pageable);
        Page<TaskResponse> responsePage = taskPage.map(TaskResponse::from);
        return PageResponse.of(responsePage);
    }
}

DTOクラスの実装

エンティティを直接返却するのではなく、DTOに変換してレスポンスします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demoapi.dto;

import com.example.demoapi.entity.Task;
import java.time.LocalDateTime;

public record TaskResponse(
        Long id,
        String title,
        String description,
        boolean completed,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
) {
    public static TaskResponse from(Task task) {
        return new TaskResponse(
                task.getId(),
                task.getTitle(),
                task.getDescription(),
                task.isCompleted(),
                task.getCreatedAt(),
                task.getUpdatedAt()
        );
    }
}

カスタムレスポンスのJSON出力例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "content": [
    {
      "id": 1,
      "title": "タスク1",
      "description": "説明1",
      "completed": false,
      "createdAt": "2026-01-04T10:00:00",
      "updatedAt": null
    }
  ],
  "page": {
    "number": 0,
    "size": 10,
    "totalElements": 50,
    "totalPages": 5,
    "first": true,
    "last": false,
    "hasNext": true,
    "hasPrevious": false
  }
}

Serviceクラスの実装

ビジネスロジック層でページネーションを扱うServiceクラスの実装例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demoapi.service;

import com.example.demoapi.entity.Task;
import com.example.demoapi.repository.TaskRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class TaskService {

    private final TaskRepository taskRepository;

    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }

    public Page<Task> findAll(Pageable pageable) {
        return taskRepository.findAll(pageable);
    }

    public Page<Task> findByCompleted(boolean completed, Pageable pageable) {
        return taskRepository.findByCompleted(completed, pageable);
    }

    public Page<Task> searchByTitle(String keyword, Pageable pageable) {
        return taskRepository.findByTitleContaining(keyword, pageable);
    }
}

動作確認

curlによるAPIテスト

アプリケーションを起動し、以下のリクエストでページネーションの動作を確認します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# デフォルトのページング(1ページ目、10件)
curl -X GET "http://localhost:8080/api/tasks" | jq

# 2ページ目を取得
curl -X GET "http://localhost:8080/api/tasks?page=1&size=10" | jq

# ソート条件を指定
curl -X GET "http://localhost:8080/api/tasks?sort=title,asc&sort=createdAt,desc" | jq

# 完了済みタスクのみを取得(カスタムエンドポイント)
curl -X GET "http://localhost:8080/api/tasks?completed=true&page=0&size=5" | jq

期待されるレスポンス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "content": [
    {
      "id": 10,
      "title": "最新のタスク",
      "description": "説明",
      "completed": false,
      "createdAt": "2026-01-04T15:30:00",
      "updatedAt": null
    },
    {
      "id": 9,
      "title": "2番目のタスク",
      "description": "説明",
      "completed": true,
      "createdAt": "2026-01-04T14:00:00",
      "updatedAt": "2026-01-04T14:30:00"
    }
  ],
  "page": {
    "number": 0,
    "size": 10,
    "totalElements": 25,
    "totalPages": 3,
    "first": true,
    "last": false,
    "hasNext": true,
    "hasPrevious": false
  }
}

ページネーション実装のベストプラクティス

パフォーマンス最適化

  1. 適切なインデックスの作成: ソートやフィルタリングに使用するカラムにはインデックスを作成する
  2. N+1問題の回避: @EntityGraphJOIN FETCHを使用して関連エンティティを効率的に取得する
  3. Sliceの活用: 総件数が不要な場合はSliceを使用してCOUNTクエリを省略する

セキュリティ考慮事項

  1. ページサイズの上限設定: max-page-sizeを設定して過大なリクエストを防止する
  2. ソート対象プロパティの制限: 任意のプロパティでソートを許可せず、許可リストを設ける
  3. オフセットの上限検討: 極端に大きなオフセットはパフォーマンス低下を招くため制限を検討する

カスタムソートプロパティの制限例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@GetMapping
public PageResponse<TaskResponse> getTasks(
        @RequestParam(defaultValue = "createdAt") String sortBy,
        @RequestParam(defaultValue = "desc") String direction,
        @PageableDefault(size = 10) Pageable pageable) {

    // 許可されたソートプロパティのみを受け入れる
    Set<String> allowedSortProperties = Set.of("id", "title", "createdAt", "completed");

    if (!allowedSortProperties.contains(sortBy)) {
        throw new IllegalArgumentException("Invalid sort property: " + sortBy);
    }

    Sort.Direction sortDirection = "asc".equalsIgnoreCase(direction)
            ? Sort.Direction.ASC
            : Sort.Direction.DESC;

    Pageable customPageable = PageRequest.of(
            pageable.getPageNumber(),
            pageable.getPageSize(),
            Sort.by(sortDirection, sortBy)
    );

    Page<Task> taskPage = taskService.findAll(customPageable);
    return PageResponse.of(taskPage.map(TaskResponse::from));
}

アーキテクチャ全体像

ページネーション実装における各レイヤーの責務を以下の図に示します。

sequenceDiagram
    participant Client as クライアント
    participant Controller as Controller
    participant Service as Service
    participant Repository as Repository
    participant DB as Database

    Client->>Controller: GET /api/tasks?page=0&size=10&sort=createdAt,desc
    Controller->>Controller: Pageableオブジェクト生成
    Controller->>Service: findAll(Pageable)
    Service->>Repository: findAll(Pageable)
    Repository->>DB: SELECT ... LIMIT 10 OFFSET 0
    Repository->>DB: SELECT COUNT(*)
    DB-->>Repository: データ + 総件数
    Repository-->>Service: Page<Task>
    Service-->>Controller: Page<Task>
    Controller->>Controller: PageResponse変換
    Controller-->>Client: JSON Response

まとめ

本記事では、Spring Boot REST APIにおけるページネーション実装について解説しました。

  • Pageableインターフェースにより、ページ番号・サイズ・ソート条件をシンプルに受け取れる
  • @PageableDefaultでデフォルト値をカスタマイズできる
  • Pageは総件数を含む完全な情報、Sliceは軽量な次ページ判定のみを提供する
  • カスタムレスポンスクラスを作成することで、APIの一貫性とクライアントの利便性を向上できる
  • ページサイズの上限設定やソートプロパティの制限によりセキュリティを確保する

ページネーションは、実用的なREST APIを構築する上で必須の機能です。Spring Data JPAの豊富な機能を活用して、効率的で使いやすいAPIを実装してください。

参考リンク