Spring Data JPAは、Spring Bootアプリケーションにおけるデータベースアクセスを劇的に簡素化するライブラリです。本記事では、JPA Entityの定義からJpaRepositoryを使ったCRUD操作、クエリメソッドの命名規則、@Queryアノテーションによるカスタムクエリまで、REST APIとデータベースを連携させるための実践的な手法を解説します。

実行環境と前提条件

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

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

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

  • JDK 17以上のインストール
  • Spring Boot REST APIプロジェクトの基本構成(前回記事を参照)
  • Maven または Gradle によるプロジェクトビルド環境

Spring Data JPAとH2 Databaseの依存関係を追加する

Spring Data JPAを使用するには、spring-boot-starter-data-jpaと使用するデータベースドライバーを依存関係に追加する必要があります。開発時はH2 Databaseを使用することで、手軽にインメモリデータベースでテストできます。

Mavenの場合

pom.xmlに以下を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<dependencies>
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- H2 Database(開発・テスト用) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Gradleの場合

build.gradleに以下を追加します。

1
2
3
4
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
}

application.propertiesでデータベースを設定する

H2 Databaseの接続設定とJPAの動作設定をapplication.propertiesに記述します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# H2 Database設定
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA/Hibernate設定
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# H2 Console設定(開発時のみ有効化)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

各設定項目の意味は以下のとおりです。

設定項目 説明
spring.jpa.hibernate.ddl-auto テーブルの自動生成モード。create-dropはアプリ起動時に作成、終了時に削除
spring.jpa.show-sql 実行されるSQLをコンソールに出力
spring.h2.console.enabled H2 Consoleを有効化(ブラウザからDBを確認可能)

Entityクラスを定義する

JPA Entityは、データベースのテーブルに対応するJavaクラスです。@Entityアノテーションを付与することで、JPAがそのクラスをエンティティとして認識します。

基本的なEntityクラスの構成

以下は、タスク管理アプリケーションを想定したTaskエンティティの例です。

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.example.demoapi.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;

@Entity
@Table(name = "tasks")
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String title;

    @Column(length = 500)
    private String description;

    @Column(nullable = false)
    private boolean completed;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    // JPAが要求する引数なしコンストラクタ
    protected Task() {
    }

    public Task(String title, String description) {
        this.title = title;
        this.description = description;
        this.completed = false;
        this.createdAt = LocalDateTime.now();
    }

    // Getter/Setter
    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
        this.updatedAt = LocalDateTime.now();
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
        this.updatedAt = LocalDateTime.now();
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
        this.updatedAt = LocalDateTime.now();
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }
}

主要なJPAアノテーションの解説

Entityクラスで使用する主要なアノテーションを整理します。

アノテーション 説明
@Entity クラスがJPAエンティティであることを宣言
@Table マッピングするテーブル名を指定(省略時はクラス名がテーブル名)
@Id 主キーフィールドを指定
@GeneratedValue 主キーの生成戦略を指定
@Column カラムの詳細設定(名前、制約、長さなど)

@GeneratedValueの生成戦略

@GeneratedValuestrategy属性で主キーの生成方法を指定できます。

戦略 説明
GenerationType.IDENTITY データベースのAUTO_INCREMENT機能を使用(MySQL、PostgreSQL等)
GenerationType.SEQUENCE データベースのシーケンスを使用(Oracle、PostgreSQL等)
GenerationType.TABLE キー生成用のテーブルを使用
GenerationType.AUTO JPAプロバイダが適切な戦略を自動選択

Repositoryインターフェースを作成する

Spring Data JPAでは、JpaRepositoryインターフェースを継承することで、基本的なCRUD操作を自動的に利用できます。

JpaRepositoryの定義

以下のように、エンティティ型と主キーの型を指定してインターフェースを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.example.demoapi.repository;

import com.example.demoapi.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
    // 基本的なCRUD操作はJpaRepositoryが提供
}

JpaRepositoryが提供する主要メソッド

JpaRepositoryを継承するだけで、以下のメソッドが利用可能になります。

メソッド 説明
save(S entity) エンティティを保存(新規作成または更新)
findById(ID id) IDでエンティティを検索(Optional<T>を返却)
findAll() 全エンティティを取得
findAllById(Iterable<ID> ids) 複数IDでエンティティを検索
deleteById(ID id) IDでエンティティを削除
delete(T entity) エンティティを削除
count() エンティティの総数を取得
existsById(ID id) 指定IDのエンティティが存在するか確認

クエリメソッドの命名規則

Spring Data JPAの強力な機能のひとつが「クエリメソッド」です。メソッド名の命名規則に従うことで、JPQLやSQLを書かずにクエリを自動生成できます。

基本的なクエリメソッドの例

 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
package com.example.demoapi.repository;

import com.example.demoapi.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {

    // タイトルで検索
    List<Task> findByTitle(String title);

    // 完了状態で検索
    List<Task> findByCompleted(boolean completed);

    // タイトルに特定文字列を含むタスクを検索
    List<Task> findByTitleContaining(String keyword);

    // 指定日時以降に作成されたタスクを検索
    List<Task> findByCreatedAtAfter(LocalDateTime dateTime);

    // 完了状態かつタイトルに特定文字列を含むタスクを検索
    List<Task> findByCompletedAndTitleContaining(boolean completed, String keyword);

    // タイトルで検索し、作成日時の降順でソート
    List<Task> findByTitleContainingOrderByCreatedAtDesc(String keyword);

    // 最初の5件を取得
    List<Task> findTop5ByOrderByCreatedAtDesc();
}

クエリメソッドで使用できるキーワード一覧

メソッド名に使用できる主要なキーワードを以下に示します。

キーワード サンプルメソッド 生成されるJPQL相当
And findByTitleAndCompleted WHERE title = ?1 AND completed = ?2
Or findByTitleOrDescription WHERE title = ?1 OR description = ?2
Between findByCreatedAtBetween WHERE createdAt BETWEEN ?1 AND ?2
LessThan findByIdLessThan WHERE id < ?1
GreaterThan findByIdGreaterThan WHERE id > ?1
IsNull findByDescriptionIsNull WHERE description IS NULL
IsNotNull findByDescriptionIsNotNull WHERE description IS NOT NULL
Like findByTitleLike WHERE title LIKE ?1
Containing findByTitleContaining WHERE title LIKE %?1%
StartingWith findByTitleStartingWith WHERE title LIKE ?1%
EndingWith findByTitleEndingWith WHERE title LIKE %?1
OrderBy findByCompletedOrderByCreatedAtDesc ORDER BY createdAt DESC
Not findByCompletedNot WHERE completed <> ?1
In findByIdIn WHERE id IN (?1)
True findByCompletedTrue WHERE completed = true
False findByCompletedFalse WHERE completed = false
IgnoreCase findByTitleIgnoreCase WHERE UPPER(title) = UPPER(?1)

@Queryによるカスタムクエリを定義する

命名規則だけでは表現が難しい複雑なクエリは、@Queryアノテーションを使用してJPQLまたはネイティブSQLで記述できます。

JPQLによるカスタムクエリ

JPQL(Java Persistence Query Language)はエンティティを対象としたクエリ言語です。

 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.repository;

import com.example.demoapi.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {

    // JPQLによるカスタムクエリ(位置パラメータ)
    @Query("SELECT t FROM Task t WHERE t.completed = false AND t.title LIKE %?1%")
    List<Task> findIncompleteTasksByKeyword(String keyword);

    // JPQLによるカスタムクエリ(名前付きパラメータ)
    @Query("SELECT t FROM Task t WHERE t.completed = :completed AND t.title LIKE %:keyword%")
    List<Task> findTasksByStatusAndKeyword(
            @Param("completed") boolean completed,
            @Param("keyword") String keyword
    );

    // 完了タスクの件数を取得
    @Query("SELECT COUNT(t) FROM Task t WHERE t.completed = true")
    long countCompletedTasks();

    // タイトルと説明文の両方で検索(OR条件)
    @Query("SELECT t FROM Task t WHERE t.title LIKE %:keyword% OR t.description LIKE %:keyword%")
    List<Task> searchByKeyword(@Param("keyword") String keyword);
}

ネイティブSQLによるカスタムクエリ

データベース固有の機能を使用したい場合は、nativeQuery = trueを指定してネイティブSQLを記述できます。

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

import com.example.demoapi.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {

    // ネイティブSQLによるカスタムクエリ
    @NativeQuery(value = "SELECT * FROM tasks WHERE completed = false ORDER BY created_at DESC LIMIT :limit")
    List<Task> findRecentIncompleteTasks(@Param("limit") int limit);

    // 従来の書き方(nativeQuery属性を使用)
    @Query(value = "SELECT * FROM tasks WHERE LOWER(title) LIKE LOWER(CONCAT('%', :keyword, '%'))",
           nativeQuery = true)
    List<Task> findByTitleCaseInsensitive(@Param("keyword") String keyword);
}

更新・削除クエリの定義

データを更新または削除するクエリには、@Modifyingアノテーションを併用します。

 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.repository;

import com.example.demoapi.entity.Task;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {

    // 一括更新クエリ
    @Modifying
    @Transactional
    @Query("UPDATE Task t SET t.completed = true WHERE t.id IN :ids")
    int markAsCompleted(@Param("ids") List<Long> ids);

    // 一括削除クエリ
    @Modifying
    @Transactional
    @Query("DELETE FROM Task t WHERE t.completed = true")
    int deleteCompletedTasks();
}

@Modifyingを使用する際は以下の点に注意してください。

  • @Transactionalアノテーションが必要(ServiceクラスまたはRepositoryメソッドに付与)
  • 戻り値はint(影響を受けた行数)またはvoid
  • 更新後、永続化コンテキストと実データの不整合に注意(必要に応じてclearAutomatically = trueを指定)

ServiceクラスでRepositoryを使用する

Repositoryで定義したメソッドは、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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.example.demoapi.service;

import com.example.demoapi.entity.Task;
import com.example.demoapi.repository.TaskRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;

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

    private final TaskRepository taskRepository;

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

    public List<Task> getAllTasks() {
        return taskRepository.findAll();
    }

    public Optional<Task> getTaskById(Long id) {
        return taskRepository.findById(id);
    }

    public List<Task> getIncompleteTasks() {
        return taskRepository.findByCompleted(false);
    }

    public List<Task> searchTasks(String keyword) {
        return taskRepository.findByTitleContaining(keyword);
    }

    @Transactional
    public Task createTask(String title, String description) {
        Task task = new Task(title, description);
        return taskRepository.save(task);
    }

    @Transactional
    public Task updateTask(Long id, String title, String description, boolean completed) {
        Task task = taskRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Task not found: " + id));
        task.setTitle(title);
        task.setDescription(description);
        task.setCompleted(completed);
        return taskRepository.save(task);
    }

    @Transactional
    public void deleteTask(Long id) {
        if (!taskRepository.existsById(id)) {
            throw new RuntimeException("Task not found: " + id);
        }
        taskRepository.deleteById(id);
    }
}

ControllerクラスからServiceを呼び出す

REST APIのエンドポイントとしてControllerを実装し、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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.example.demoapi.controller;

import com.example.demoapi.entity.Task;
import com.example.demoapi.service.TaskService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;

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

    private final TaskService taskService;

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

    @GetMapping
    public ResponseEntity<List<Task>> getAllTasks() {
        return ResponseEntity.ok(taskService.getAllTasks());
    }

    @GetMapping("/{id}")
    public ResponseEntity<Task> getTaskById(@PathVariable Long id) {
        return taskService.getTaskById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/search")
    public ResponseEntity<List<Task>> searchTasks(@RequestParam String keyword) {
        return ResponseEntity.ok(taskService.searchTasks(keyword));
    }

    @PostMapping
    public ResponseEntity<Task> createTask(@RequestBody TaskRequest request) {
        Task created = taskService.createTask(request.title(), request.description());
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Task> updateTask(
            @PathVariable Long id,
            @RequestBody TaskRequest request
    ) {
        Task updated = taskService.updateTask(
                id,
                request.title(),
                request.description(),
                request.completed()
        );
        return ResponseEntity.ok(updated);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTask(@PathVariable Long id) {
        taskService.deleteTask(id);
        return ResponseEntity.noContent().build();
    }

    // リクエストボディ用のRecord
    public record TaskRequest(String title, String description, boolean completed) {}
}

動作確認の手順

アプリケーションを起動し、REST APIの動作を確認します。

アプリケーションの起動

1
./mvnw spring-boot:run

curlコマンドによる動作確認

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# タスクの作成
curl -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Spring Data JPAを学習する","description":"EntityとRepositoryを理解する","completed":false}'

# 全タスクの取得
curl http://localhost:8080/api/tasks

# IDでタスクを取得
curl http://localhost:8080/api/tasks/1

# キーワードで検索
curl "http://localhost:8080/api/tasks/search?keyword=JPA"

# タスクの更新
curl -X PUT http://localhost:8080/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Spring Data JPAを学習する","description":"EntityとRepositoryを理解した","completed":true}'

# タスクの削除
curl -X DELETE http://localhost:8080/api/tasks/1

H2 Consoleでデータを確認する

ブラウザでhttp://localhost:8080/h2-consoleにアクセスし、以下の設定で接続します。

項目
JDBC URL jdbc:h2:mem:testdb
User Name sa
Password (空欄)

接続後、SELECT * FROM TASKSなどのSQLでデータを確認できます。

期待される結果

本記事の手順を完了すると、以下の成果物が得られます。

成果物 説明
Entityクラス @Entityを付与したデータベーステーブル対応クラス
Repositoryインターフェース JpaRepositoryを継承したCRUD操作インターフェース
クエリメソッド 命名規則に基づく自動生成クエリ
カスタムクエリ @Queryで定義したJPQL/ネイティブSQLクエリ
Service/Controllerクラス レイヤードアーキテクチャに基づくREST API実装

以下のアーキテクチャ図は、本記事で構築したアプリケーションの全体構成を示しています。

flowchart TB
    subgraph Client["クライアント"]
        HTTP["HTTP リクエスト"]
    end
    
    subgraph Application["Spring Boot Application"]
        Controller["@RestController<br/>TaskController"]
        Service["@Service<br/>TaskService"]
        Repository["@Repository<br/>TaskRepository<br/>(JpaRepository)"]
        Entity["@Entity<br/>Task"]
    end
    
    subgraph Database["Database"]
        H2["H2 Database<br/>tasks テーブル"]
    end
    
    HTTP --> Controller
    Controller --> Service
    Service --> Repository
    Repository --> Entity
    Entity -.-> H2
    Repository -.-> H2

まとめ

本記事では、Spring Boot REST APIにおけるSpring Data JPAの基本的な使い方を解説しました。

  • @Entity@Id@GeneratedValueを使ったEntityクラスの定義
  • JpaRepositoryを継承したRepositoryインターフェースの作成
  • クエリメソッドの命名規則による自動クエリ生成
  • @Queryアノテーションを使ったカスタムJPQL/ネイティブSQLの記述
  • Service層を経由したビジネスロジックの実装

Spring Data JPAを活用することで、ボイラープレートコードを大幅に削減し、データアクセス層の開発を効率化できます。次のステップとして、エンティティ間のリレーションシップ設計やN+1問題の対策に取り組むことをおすすめします。

参考リンク