前回は SQL テーブルに対応する Entity を作成しました。
今回はデータベースを操作する Repository を作成します。

Repository とは?
データベースを操作するファイルです。
実際に SQL 文を実行して、データの参照・登録・更新・削除といった、いわゆる CRUD 処理を実装していきます。
Service から呼び出され、処理結果を Service に返却します。
なお、「Repository」と「リポジトリ」で2つの記載方法がありますが、どちらも同じ意味です。
英語表記か日本語表記かの違いだけなので。
Repository 作成
作成するリポジトリ(Repository)ファイルは全部で、
- AuthorityRepository
- CategoryRepository
- TodoCategoryRepository
- TodoRepository
- UserRepository
の5つです。

repository フォルダ内に上記5つのファイルを作成してください。
各Repositoryの内容
これまでに作成した下記3つを見ながら Repository ファイルの中身を書いていきます。
説明の都合上、作成順序は簡単なファイルからにしたいので、
- CategoryRepository
- TodoCategoryRepository
- AuthorityRepository
- TodoRepository
- UserRepository
の順番で作成します。
CategoryRepository
水色が第3回で作成した SQL テーブルです。
緑色が第5回で作成したエンティティです。
紫色が今回作成するリポジトリです。
-- カテゴリー情報
CREATE TABLE `category`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'カテゴリーID',
`name` varchar(16) NOT NULL COMMENT 'カテゴリー名',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
PRIMARY KEY (`id`),
UNIQUE (`name`)
) ENGINE=InnoDB
package com.ozack.todoapp.repository.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 lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/* カテゴリー情報 */
@Entity
@Table(name = "category")
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Category {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
}
package com.ozack.todoapp.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.ozack.todoapp.repository.entity.Category;
/* カテゴリー情報を格納している category テーブルと対応 */
@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
何も書いてないじゃんって絶対思われますが、これで問題ないです。
CategoryRepository は JpaRepository を継承しているからです。
JpaRepository について
下記のCRUD機能がデフォルトで用意されている Repository です。
- データの登録
- データの取得
- データの更新
- データの削除
これらCRUD機能を継承という形で呼び出すことで使用します。
public interface CategoryRepository extends JpaRepository<Category, Long>
引数には Category エンティティと、Category エンティティの主キーである id の型を設定しています。
第一引数には、操作するテーブルに対応するエンティティ名を指定。
第二引数には、エンティティの主キーの型を指定。
と覚えておけばいいです。
必要な処理
下記4つの処理が必要です。
- 全カテゴリーを取得
- 新規カテゴリーの登録
- 既存カテゴリーの変更
- 既存カテゴリーの削除
カテゴリーデータの取得は、フロントエンド側でカテゴリーを一覧表示するため必要です。
なお、これら CRUD 機能は JpaRepository がデフォルトで用意しているので、追加でクエリ処理を記載する必要はないです。
故に、JpaRepository の継承以外は何も書かれていない状態が完成形となります。
作成した Repository は実際に Service やテストコードから実行するのですが、その際には⬇️のように呼び出して実行します。
@Autowired
CategoryRepository categoryRepository;
/* 登録・更新データの定義 */
Category saveCategory = new Category(
null, // 登録時はDB側で勝手に付与するので不要
"category-name-1"
);
Category updateCategory = new Category(
1L, // 更新時は更新対象のid(主キー値)を指定
"update-category-name-1"
);
/* 1. フロント側で表示するカテゴリー一覧を取得 */
List<Category> Categories = categoryRepository.findAll();
/* 2. 新カテゴリーの登録 */
categoryRepository.save(saveCategory); // 引数に設定されたエンティティのidがDBに存在しなければ登録処理,存在すれば更新処理になる。
/* 3. 既存カテゴリーの更新 */
categoryRepository.save(updateCategory); // 引数に設定されたエンティティのidがDBに存在しなければ登録処理,存在すれば更新処理になる。
/* 4. 既存カテゴリーの削除 */
categoryRepository.deleteById(1L);
TodoCategoryRepository
CategoryRepository と同様、JpaRepository を継承しているだけです。
-- todo と category のマッピング情報
CREATE TABLE `todo_category`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'マッピングID',
`todo_id` bigint unsigned NOT NULL COMMENT 'TodoID',
`category_id` bigint unsigned NOT NULL COMMENT 'カテゴリーID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
PRIMARY KEY (`id`),
FOREIGN KEY (`todo_id`) REFERENCES `todo`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`category_id`) REFERENCES `category`(`id`) ON DELETE CASCADE,
UNIQUE (`todo_id`, `category_id`)
) ENGINE=InnoDB
package com.ozack.todoapp.repository.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Tolerate;
/* Todo と Category のマッピング情報 */
@Entity
@Table(name = "todo_category")
@RequiredArgsConstructor
@Getter
public class TodoCategory {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private final Long id;
@Column(name = "todo_id")
private final Long todoId;
@Column(name = "category_id")
private final Long categoryId;
@ManyToOne(
targetEntity = Todo.class,
fetch = FetchType.LAZY)
@JoinColumn(
name = "todo_id",
insertable = false,
updatable = false)
@JsonBackReference
private Todo todo;
@ManyToOne(
targetEntity = Category.class,
fetch = FetchType.LAZY)
@JoinColumn(
name = "category_id",
insertable = false,
updatable = false)
private Category category;
@Tolerate
public TodoCategory(){
this.id = null;
this.todoId = null;
this.categoryId = null;
}
}
package com.ozack.todoapp.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.ozack.todoapp.repository.entity.TodoCategory;
/* Todo と Category のマッピング情報を格納している todo_category テーブルと対応 */
@Repository
public interface TodoCategoryRepository extends JpaRepository<TodoCategory, Long> {
}
必要な処理
ここでは、(todo,category)のマッピング情報を管理する todo_category テーブルを操作します。
下記3つの処理が必要です。
- 新規マッピング情報の登録
- 既存マッピング情報の変更
- 既存マッピング情報の削除
データの取得に関しては、TodoRepository の方で、 todo_category テーブルのデータも一緒に取得する方法をとるため、この Repository では取得しません。
AuthorityRepository
ここも JpaRepository を継承しているだけです。
-- 権限情報
CREATE TABLE `authority`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '権限ID',
`name` varchar(16) NOT NULL COMMENT '権限名',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
PRIMARY KEY (`id`),
UNIQUE (`name`)
) ENGINE=InnoDB
package com.ozack.todoapp.repository.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 lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/* 権限情報 */
@Entity
@Table(name = "authority")
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Authority {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
}
package com.ozack.todoapp.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.ozack.todoapp.repository.entity.Authority;
/* 権限情報を格納している authority テーブルと対応 */
@Repository
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
}
必要な処理
ここでは、権限情報を管理する authority テーブルを操作します。
下記3つの処理が必要です。
- 全権限の取得
- 新規権限の登録
- 既存権限の変更
- 既存権限の削除
権限データの取得に関しては、フロントエンド側の管理者サイトで権限一覧を表示するため必要です。
TodoRepository
用意されていないクエリを実行するため、追加処理を定義しています。
-- Todo 情報
CREATE TABLE `todo`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'TodoID',
`user_id` bigint unsigned NOT NULL COMMENT 'ユーザーID',
`title` varchar(32) NOT NULL COMMENT 'やること内容',
`is_check` tinyint(1) NOT NULL COMMENT 'チェックの有無を示す真偽値',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
PRIMARY KEY (`id`),
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
UNIQUE (`user_id`, `title`)
) ENGINE=InnoDB
package com.ozack.todoapp.repository.entity;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Tolerate;
/* Todo 情報 */
@Entity
@Table(name = "todo")
@RequiredArgsConstructor
@Getter
public class Todo {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private final Long id;
@Column(name = "user_id")
private final Long userId;
@Column(name = "title")
private final String title;
@Column(name = "is_check")
private final Boolean isCheck;
@OneToMany(
targetEntity = TodoCategory.class,
mappedBy = "todo",
fetch = FetchType.LAZY)
@JsonManagedReference
private Set<TodoCategory> todoCategories;
@Tolerate
public Todo(){
this.id = null;
this.userId = null;
this.title = null;
this.isCheck = null;
}
}
package com.ozack.todoapp.repository;
import java.util.List;
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 com.ozack.todoapp.repository.entity.Todo;
/* Todo を格納している todo テーブルと対応 */
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
/* userId を検索キーとしてデータを取得 */
@Query("""
SELECT t
FROM Todo t
LEFT JOIN FETCH t.todoCategories tc
LEFT JOIN FETCH tc.category c
WHERE t.userId = :userId
ORDER BY t.id DESC
""")
List<Todo> findAllByUserIdWithCategories(@Param("userId") Long userId);
}
必要な処理
Todo を操作するために、下記4つの処理が必要です。
- 条件に一致する Todo の取得
- 新規 Todo の登録
- 既存 Todo の変更
- 既存 Todo の削除
Todo の取得処理では、Todo データに加えて、設定されたカテゴリーデータも結合して取得しています。
結合順序は、ざっくり説明すると、
- (todo, todo_category)
- (todo_category, category)
といった感じで、ER図の構成に準じています。
詳細なクエリ処理は下記のとおりです。
- (todo, todo_category) テーブル間の (id, todo_id) カラムで結合
- todo_category テーブルのカテゴリーidを取得
- (todo_category, category) テーブル間の (category_id, id) カラムで結合
- category テーブルのカテゴリー名を取得
JPQL について
なお、上記のクエリ処理は JPQL で記載しています。
素の SQL でクエリ定義するネイティブモードもありますが、テーブル結合が困難になるため個人的には JPQL の方が使いやすいです。
なのですが、JPQL 自体は難しいので、なんとなくやってることが分かるようであれば後回しでいいと思ってます。
多少なりとも理解したい方はこの記事オススメです。
ちなみに、自分は後から覚えました。
事後処理について
今回のクエリ処理、データ形式はエンティティ(Todo)のまま返却しています。
/* userId を検索キーとしてデータを取得 */
@Query("""
SELECT t
FROM Todo t
LEFT JOIN FETCH t.todoCategories tc
LEFT JOIN FETCH tc.category c
WHERE t.userId = :userId
ORDER BY t.id DESC
""")
List<Todo> findAllByUserIdWithCategories(@Param("userId") Long userId);
ですが、本来ならば必要なデータを Dto に格納し、不要なデータはそぎ落とすべきです。
現状だと、返却される Todo エンティティ型のデータには重複値が存在します。
結合した Category エンティティのカテゴリー id と TodoCategory エンティティのカテゴリー id 等です。
これらの値は2つも要らないため、データ量を削減するために1つだけ残すべきなのです。
それをしない理由ですが、今回の構成だと下記問題が発生するからです。
Dto は下記で定義します。
package com.ozack.todoapp.dto.response;
import java.util.List;
import com.ozack.todoapp.repository.entity.Category;
/* フロントエンド側にレスポンスする Todo データ */
public record ResponseTodoDto(
Long id,
String title,
Boolean isCheck,
List<Category> categories
){}
Repository は下記で定義します。
これが可能なら Service 側で Dto 変換せずに済むので、非常に楽です。
/* Todo を格納している todo テーブルと対応 */
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
/* userId を検索キーとしてデータを取得 */
@Query("""
SELECT new com.ozack.todoapp.dto.response.ResponseTodoDto(
t.id,
t.title,
t.isCheck,
new com.ozack.todoapp.repository.entity.Category(
c.id,
c.name
)
)
FROM Todo t
LEFT JOIN FETCH t.todoCategories tc
LEFT JOIN FETCH tc.category c
WHERE t.userId = :userId
ORDER BY t.id DESC
""")
List<ResponseTodoDto> findAllByUserIdWithCategories(@Param("userId") Long userId);
}
なのですが、11~14行で Category 型に変換している箇所、本来なら List<Category> 型にすべきなのでエラーです。
ならリスト型に変換すればいいじゃんってなると思いますが、JPQL 内でリスト型を定義する方法は現状ありません。
そのため、リポジトリ内で対策することは不可能になっています。
この問題をリポジトリ内で対策することは Jpa の仕様的に不可能なため、Todo エンティティの Dto 変換は Service 側で行います。
UserRepository
用意されていないクエリ処理を行うため、追加で定義しています。
コッチのクエリ処理は Dto 変換できるため、リポジトリ内で Dto にして返却します。
-- ユーザー情報
CREATE TABLE `user`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'ユーザーID',
`authority_id` bigint unsigned NOT NULL COMMENT '権限ID',
`name` varchar(16) NOT NULL COMMENT 'ユーザー名',
`email` varchar(32) NOT NULL COMMENT 'メールアドレス',
`password` varchar(32) NOT NULL COMMENT 'パスワード',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
PRIMARY KEY (`id`),
FOREIGN KEY (`authority_id`) REFERENCES `authority`(`id`) ON DELETE CASCADE,
UNIQUE (`email`)
) ENGINE=InnoDB
package com.ozack.todoapp.repository.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Tolerate;
/* ユーザー情報 */
@Entity
@Table(name = "user")
@RequiredArgsConstructor
@Getter
public class User {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private final Long id;
@Column(name = "authority_id")
private final Long authorityId;
@Column(name = "name")
private final String name;
@Column(name = "email")
private final String email;
@Column(name = "password")
private final String password;
@ManyToOne(
targetEntity = Authority.class,
fetch = FetchType.LAZY)
@JoinColumn(
name = "authority_id",
insertable = false,
updatable = false)
private Authority authority;
@Tolerate
public User(){
this.id = null;
this.authorityId = null;
this.name = null;
this.email = null;
this.password = null;
}
}
package com.ozack.todoapp.repository;
import java.util.Optional;
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 com.ozack.todoapp.dto.response.ResponseUserDto;
import com.ozack.todoapp.repository.entity.User;
/* ユーザー情報を格納している user テーブルと対応 */
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
/* メールアドレスを検索キーとしてユーザー情報を取得 */
@Query("""
SELECT new com.ozack.todoapp.dto.response.ResponseUserDto(
u.id,
u.name,
u.email,
new com.ozack.todoapp.repository.entity.Authority(
a.id,
a.name
)
)
FROM User u
LEFT JOIN u.authority a
WHERE u.email = :email
""")
Optional<ResponseUserDto> findByEmailWithAuthority(@Param("email") String email);
}
Dto について
不要なデータを排除して、都合のいい値のみを残すための型定義ファイルです。
今回の構成だと、User エンティティではパスワードを管理していますが、これはフロント側に渡すべきではありません。
そのため、パスワードを削ぎ落とすために都合のいい型を定義します。

なお、このファイルは必要なので作成しましょう。
dto/response フォルダを作成して、その中に ResponseUserDto.java ファイルを生成してください。
コード内容に関しては下記を写経してください。
package com.ozack.todoapp.dto.response;
import com.ozack.todoapp.repository.entity.Authority;
/* フロントエンド側にレスポンスする User データ */
public record ResponseUserDto(
Long id,
String name,
String email,
Authority authority
){}
この Dto が保持するデータは下記のとおりです。
- ユーザーID
- ユーザー名
- メールアドレス
- 権限ID
- 権限名
権限情報は Authority エンティティ型として定義しています。
また、Dto はレコードで定義します。
基本的にクラスと同じ使い方ですが、get+変数名の形で値を取得しない点が異なります。
ResponseUserDto record = new ResponseUserDto(
1L,
"user-name-1",
"user-email-1",
new Authority( // ココはクラス定義
1L,
"authority-name-1"
)
);
/* レコードから各変数値を取得 */
record.id();
record.name();
record.email()
record.Authority.getId();
record.Authority.getName();
今回の Todo 開発では最低限コレくらいの知識があれば問題ないです。
以下の記事の方は詳細まで解説されているため、より深く知れるかと。
参考になりました。

必要な処理
下記4つの処理が必要です。
- 条件に一致する Todo の取得
- 新規 Todo の登録
- 既存 Todo の変更
- 既存 Todo の削除
条件(メールアドレス)に一致する Todo データの取得処理は用意されていないため、クエリ処理を追加しています。
/* メールアドレスを検索キーとしてユーザー情報を取得 */
@Query("""
SELECT new com.ozack.todoapp.dto.response.ResponseUserDto(
u.id,
u.name,
u.email,
new com.ozack.todoapp.repository.entity.Authority(
a.id,
a.name
)
)
FROM User u
LEFT JOIN u.authority a
WHERE u.email = :email
""")
Optional<ResponseUserDto> findByEmailWithAuthority(@Param("email") String email);
なお、権限情報に関して、user テーブルは authority_id しか保持していないため、
- (user, authority) テーブル間の (authority_id, id) カラム
で結合処理を行い権限情報を取得しています。
返却値は Optional 型にしておきます。
Optional は、検索結果が0である可能性が存在する場合に使用します。
まとめ
DBテーブルの操作を担当する Repository を作成しました。
main/…/repository
- CategoryRepository.java
- TodoCategoryRepository.java
- TodoRepository.java
- UserRepository.java
main/…/dto/response
- ResponseUserDto.java
プロジェクト構成は GitHub にプッシュしています。
次回は Repository のテストコードについて解説します。