前回は 業務ロジックを実行する Service を作成しました。
今回はフロント側と API 通信を行う Controller を作成します。

Controller(コントローラー)とは?
フロントエンド側と API 通信をを行うファイルです。
アクセス用の url を設定し、フロントエンド側からリクエストが送信されると Service から処理結果を受け取り、レスポンスとしてフロント側へ返却します。
デフォルトだとレスポンスの返却値は JSON データに変換されます。
Controller 作成
作成する Controller は下記4つです。
- AuthorityController
- CategoryController
- TodoCategoryController
- TodoController
Userに関する Controller はまだ作成しません。
SpringSecurity 関連の処理が混ざるので、その回で作成します。
ファイル作成
todoapp フォルダ内に controller フォルダを作成します。
さらに、作成した controller フォルダ内に下記ファイルを作成してください。
- AuthorityController.java
- CategoryController.java
- TodoController.java
- TodocategoryController.java

各Controllerの内容
下記2つを見ながら各 Service ファイルの中身を書いていきます。
AuthorityController
赤色は今回作成する Controller です。
青色は第8回で作成した Service です。
紫色は第6回で作成した Repository です。
package com.ozack.todoapp.controller;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.repository.entity.Authority;
import com.ozack.todoapp.service.authority.AuthorityService;
/* 権限情報を操作するコントローラー */
@RequestMapping("authority")
@RestController
public class AuthorityController {
private final AuthorityService authorityService;
public AuthorityController(AuthorityService authorityService) {
this.authorityService = authorityService;
}
/* データの取得 */
@GetMapping
public ResponseEntity<List<Authority>> getAuthorities() {
List<Authority> res = authorityService.selectAllAuthorities();
return ResponseEntity.ok().body(res);
}
/* データの登録 */
@PostMapping
public ResponseEntity<Authority> postAuthoriry(@RequestBody Authority req) throws TodoAppException {
Authority res = authorityService.insertAuthority(req);
URI location = URI.create("/category/" + res.getId());
return ResponseEntity.created(location).body(res);
}
/* データの更新 */
@PutMapping
public ResponseEntity<Authority> putAuthoriry(@RequestBody Authority req) throws TodoAppException {
Authority res = authorityService.insertAuthority(req);
return ResponseEntity.ok().body(res);
}
/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteAuthority(@RequestParam Long id) throws TodoAppException {
authorityService.deleteAuthority(id);
return ResponseEntity.noContent().build();
}
}
package com.ozack.todoapp.service.authority;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ozack.todoapp.exception.DeleteException;
import com.ozack.todoapp.exception.InsertException;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.exception.UpdateException;
import com.ozack.todoapp.repository.AuthorityRepository;
import com.ozack.todoapp.repository.entity.Authority;
/* 権限に関する処理を行うサービス */
@Service
public class AuthorityServiceImpl implements AuthorityService {
private final String loadErrorMessage = "authority テーブルのデータ取得に失敗しました。";
private final String insertErrorMessageByDataAccess = "authority データ登録時にデータベース関連のエラーが発生しました。";
private final String updateErrorMessageByDataAccess = "authority データ更新時にデータベース関連のエラーが発生しました。";
private final String deleteErrorMessageByLoad = "authority データを削除できませんでした。";
private final AuthorityRepository authorityRepository;
public AuthorityServiceImpl(AuthorityRepository authorityRepository) {
this.authorityRepository = authorityRepository;
}
/* 全ての権限一覧を取得するメソッド */
public List<Authority> selectAllAuthorities() {
return authorityRepository.findAll();
}
/* データを登録するメソッド */
@Transactional(rollbackFor = TodoAppException.class)
public Authority insertAuthority(Authority authority) throws TodoAppException {
try {
authorityRepository.save(authority);
Authority res = authorityRepository.findById(authority.getId()).orElse(null);
if (res == null) throw new InsertException(loadErrorMessage);
return res;
} catch (DataAccessException e) {
throw new InsertException(insertErrorMessageByDataAccess, e);
}
}
/* データを更新するメソッド */
@Transactional(rollbackFor = TodoAppException.class)
public Authority updateAuthority(Authority authority) throws TodoAppException {
try {
authorityRepository.save(authority);
Authority res = authorityRepository.findById(authority.getId()).orElse(null);
if (res == null) throw new UpdateException(loadErrorMessage);
return res;
} catch (DataAccessException e) {
throw new UpdateException(updateErrorMessageByDataAccess, e);
}
}
/* データを削除するメソッド */
@Transactional(rollbackFor = TodoAppException.class)
public void deleteAuthority(Long authorityId) throws TodoAppException {
authorityRepository.deleteById(authorityId);
Authority res = authorityRepository.findById(authorityId).orElse(null);
if (res != null) throw new DeleteException(deleteErrorMessageByLoad);
}
}
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> {
}
アノテーションが分からないと理解できないため、@を重点的に説明します。
逆に各アノテーションの意味が分かれば結構簡単です。
@RequestMapping
コードでは下記で定義してます。
@RequestMapping("authority")
権限情報を操作したい場合は⬇️にアクセスしてという意味です。
- http://localhost:8080/authority
@RestController
返却値を JSON データとしてフロントエンド側に返却するためのアノテーションです。
各 CRUD メソッドの返却値に注目してほしいのですが、どれも ResponseEntity~ という形式で統一しています。
/* データ取得の場合 */
@GetMapping
public ResponseEntity<List<Authority>> getAuthorities() {
List<Authority> res = authorityService.selectAllAuthorities();
return ResponseEntity.ok().body(res);
}
削除(D)に関しては何も返却しませんが、それ以外は res をフロント側へ返却しており、@RestController を付与すると、デフォルトで res が JSON データに変換されます。
⬇️のような感じです。
Authority エンティティ型の res が JSON 形式で返却されていることが分かります。

今回のように、フロントエンド側とバックエンド側で分離しているプロジェクトでは大体使います。
@GetMapping
先ほどの説明と全く同じになってしまいますが、下記状況で作動します。
- http://localhost:8080/authority にアクセスした
- フロント側からのリクエストが GET である
作動するといっても、このアノテーションが付与されてるメソッドが実行されるだけです。
要するに、AuthoriryController では下記メソッドが実行されます。
/* データの取得 */
@GetMapping
public ResponseEntity<List<Authority>> getAuthorities() {
List<Authority> res = authorityService.selectAllAuthorities();
return ResponseEntity.ok().body(res);
}
返却値に関しては、権限情報(res)と、処理が正常終了したことを示す 200(OK) を設定します。
実際に GET リクエストでアクセスすると⬇️のように返却されます。

なお、200 とかのステータスコード一覧は下記サイトが参考になります。

企業サイトですが、説明が分かりやすいです。
@PostMapping
下記状況で作動します。
- http://localhost:8080/authority にアクセスした
- フロント側からのリクエストが POST である
AuthoriryController では下記メソッドが実行されます。
/* データの登録 */
@PostMapping
public ResponseEntity<Authority> postAuthoriry(@RequestBody Authority req) throws TodoAppException {
Authority res = authorityService.insertAuthority(req);
URI location = URI.create("/category/" + res.getId());
return ResponseEntity.created(location).body(res);
}
返却値に関しては、登録する権限情報(res)と、登録処理が正常終了したことを示す 201(Created) を設定します。
実際に POST リクエストでアクセスすると⬇️のように返却されます。

画面上部の JSON データは @RequestBody の res 変数に渡されるリクエストデータです。
画面下部の JSON データが返却されたレスポンスデータです。
@RequestBody
ついでに説明しときます。
名前のとおり、バックエンド側に渡されるデータです。
@RequestBody Authority req
上記の例では、フロント側から Authority エンティティが渡されます。
このデータ(req)はリクエストデータと呼ばれ、バックエンド側で登録したり更新したりします。
@PutMapping
下記状況で作動します。
- http://localhost:8080/authority にアクセスした
- フロント側からのリクエストが PUT である
AuthoriryController では下記メソッドが実行されます。
/* データの更新 */
@PutMapping
public ResponseEntity<Authority> putAuthoriry(@RequestBody Authority req) throws TodoAppException {
Authority res = authorityService.insertAuthority(req);
return ResponseEntity.ok().body(res);
}
返却値に関しては、更新する権限情報(res)と、処理が正常終了したことを示す 200(OK) を設定します。
実際に PUT リクエストでアクセスすると⬇️のように返却されます。

画面上部の JSON データは @RequestBody の res 変数に渡されるリクエストデータです。
画面下部の JSON データが返却されたレスポンスデータです。
@DeleteMapping
下記状況で作動します。
- http://localhost:8080/authority にアクセスした
- フロント側からのリクエストが PUT である
AuthoriryController では下記メソッドが実行されます。
/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteAuthority(@RequestParam Long id) throws TodoAppException {
authorityService.deleteAuthority(id);
return ResponseEntity.noContent().build();
}
返却値に関しては、処理が正常終了し、レスポンスデータが空であることを示す 204(No Content) を設定します。
実際に Delete リクエストでアクセスすると⬇️のように返却されます。

URL にはリクエストパラメーターを設定しています。
@RequestParam
@RequestBody と役割が似ていますが、こちらは URL に値を含める記法です。
下記 URL でアクセスします。
すると、⬇️の id 変数には URL で指定した値が勝手に設定されます。
/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteAuthority(@RequestParam Long id) throws TodoAppException {
authorityService.deleteAuthority(id);
return ResponseEntity.noContent().build();
}
CategoryController
AuthorityController と全く同じ構成です。
package com.ozack.todoapp.controller;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.repository.entity.Category;
import com.ozack.todoapp.service.category.CategoryService;
/* カテゴリー情報を操作するコントローラー */
@RequestMapping("category")
@RestController
public class CategoryController {
private final CategoryService categoryService;
public CategoryController(CategoryService categoryService) {
this.categoryService = categoryService;
}
/* データの取得 */
@GetMapping
public ResponseEntity<List<Category>> getCategories() {
List<Category> res = categoryService.selectAllCategories();
return ResponseEntity.ok().body(res);
}
/* データの登録 */
@PostMapping
public ResponseEntity<Category> postCategory(@RequestBody Category req) throws TodoAppException {
Category res = categoryService.insertCategory(req);
URI location = URI.create("/category/" + res.getId());
return ResponseEntity.created(location).body(res);
}
/* データの更新 */
@PutMapping
public ResponseEntity<Category> putCategory(@RequestBody Category req) throws TodoAppException {
Category res = categoryService.updateCategory(req);
return ResponseEntity.ok().body(res);
}
/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteCategory(@RequestParam Long id) throws TodoAppException {
categoryService.deleteCategory(id);
return ResponseEntity.noContent().build();
}
}
package com.ozack.todoapp.service.category;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ozack.todoapp.exception.DeleteException;
import com.ozack.todoapp.exception.InsertException;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.exception.UpdateException;
import com.ozack.todoapp.repository.CategoryRepository;
import com.ozack.todoapp.repository.entity.Category;
/* カテゴリーに関する処理を行うサービス */
@Service
public class CategoryServiceImpl implements CategoryService {
private final String loadErrorMessage = "category テーブルのデータ取得に失敗しました。";
private final String insertErrorMessageByDataAccess = "category データ登録時にデータベース関連のエラーが発生しました。";
private final String updateErrorMessageByDataAccess = "category データ更新時にデータベース関連のエラーが発生しました。";
private final String deleteErrorMessageByLoad = "category データを削除できませんでした。";
private final CategoryRepository categoryRepository;
public CategoryServiceImpl(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
/* 全てのカテゴリー一覧を取得するメソッド */
@Override
public List<Category> selectAllCategories() {
return categoryRepository.findAll();
}
/* データを登録するメソッド */
@Override
@Transactional(rollbackFor = TodoAppException.class)
public Category insertCategory(Category category) throws TodoAppException {
try {
categoryRepository.save(category);
Category res = categoryRepository.findById(category.getId()).orElse(null);
if (res == null) throw new InsertException(loadErrorMessage);
return res;
} catch (DataAccessException e) {
throw new InsertException(insertErrorMessageByDataAccess, e);
}
}
/* データを更新するメソッド */
@Override
@Transactional(rollbackFor = TodoAppException.class)
public Category updateCategory(Category category) throws TodoAppException {
try {
categoryRepository.save(category);
Category res = categoryRepository.findById(category.getId()).orElse(null);
if (res == null) throw new UpdateException(loadErrorMessage);
return res;
} catch (DataAccessException e) {
throw new UpdateException(updateErrorMessageByDataAccess, e);
}
}
/* データを削除するメソッド */
@Override
@Transactional(rollbackFor = TodoAppException.class)
public void deleteCategory(Long categoryId) throws TodoAppException {
categoryRepository.deleteById(categoryId);
Category res = categoryRepository.findById(categoryId).orElse(null);
if (res != null) throw new DeleteException(deleteErrorMessageByLoad);
}
}
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> {
}
解説は省略します。
TodoController
GET処理の getTodos メソッドでは、任意ユーザーの Todo だけを取得するのでリクエストパラメーターに userId を指定します。
package com.ozack.todoapp.controller;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ozack.todoapp.dto.response.ResponseTodoDto;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.repository.entity.Todo;
import com.ozack.todoapp.service.todo.TodoService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
/* 任意のユーザーが登録した Todo を操作するコントローラー */
@RequestMapping("todo")
@RestController
public class TodoController {
private final TodoService todoService;
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
/* データの取得 */
@GetMapping
public ResponseEntity<List<ResponseTodoDto>> getTodos(@RequestParam Long userId) {
List<ResponseTodoDto> res = todoService.selectAllTodosByUserIdWithCategories(userId);
return ResponseEntity.ok().body(res);
}
/* データの登録 */
@PostMapping
public ResponseEntity<ResponseTodoDto> postTodo(@RequestBody Todo req) throws TodoAppException {
ResponseTodoDto res = todoService.insertTodo(req);
URI location = URI.create("/todo/" + res.id());
return ResponseEntity.created(location).body(res);
}
/* データの更新 */
@PutMapping
public ResponseEntity<ResponseTodoDto> putTodo(@RequestBody Todo req) throws TodoAppException {
ResponseTodoDto res = todoService.updateTodo(req);
return ResponseEntity.ok().body(res);
}
/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteTodo(@RequestParam Long id) throws TodoAppException {
todoService.deleteTodo(id);
return ResponseEntity.noContent().build();
}
}
package com.ozack.todoapp.service.todo;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ozack.todoapp.dto.response.ResponseTodoCategoryDto;
import com.ozack.todoapp.dto.response.ResponseTodoDto;
import com.ozack.todoapp.exception.DeleteException;
import com.ozack.todoapp.exception.InsertException;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.exception.UpdateException;
import com.ozack.todoapp.repository.TodoRepository;
import com.ozack.todoapp.repository.entity.Category;
import com.ozack.todoapp.repository.entity.Todo;
/* カテゴリーに関する処理を行うサービス */
@Service
public class TodoServiceImpl implements TodoService {
private final String loadErrorMessage = "todo テーブルのデータ取得に失敗しました。";
private final String insertErrorMessageByDataAccess = "todo データ登録時にデータベース関連のエラーが発生しました。";
private final String updateErrorMessageByDataAccess = "todo データ更新時にデータベース関連のエラーが発生しました。";
private final String deleteErrorMessageByLoad = "todo データを削除できませんでした。";
private final TodoRepository todoRepository;
public TodoServiceImpl(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
/* 全てのカテゴリー一覧を取得するメソッド */
public List<ResponseTodoDto> selectAllTodosByUserIdWithCategories(Long userId) {
List<Todo> res = todoRepository.findAllByUserIdWithCategories(userId);
return convertResponseTodoDto(res);
}
/* データを登録するメソッド */
@Transactional(rollbackFor = TodoAppException.class)
public ResponseTodoDto insertTodo(Todo todo) throws TodoAppException {
try {
Todo resTodo = todoRepository.save(todo);
if (resTodo == null) throw new InsertException(loadErrorMessage);
// Dto 変換
ResponseTodoDto res = new ResponseTodoDto(
resTodo.getId(),
resTodo.getTitle(),
resTodo.getIsCheck(),
null // フロント側で結合するため null で返却
);
return res;
} catch (DataAccessException e) {
throw new InsertException(insertErrorMessageByDataAccess, e);
}
}
/* データを更新するメソッド */
@Transactional(rollbackFor = TodoAppException.class)
public ResponseTodoDto updateTodo(Todo todo) throws TodoAppException {
try {
Todo resTodo = todoRepository.save(todo);
if (resTodo == null) throw new UpdateException(loadErrorMessage);
// Dto 変換
ResponseTodoDto res = new ResponseTodoDto(
resTodo.getId(),
resTodo.getTitle(),
resTodo.getIsCheck(),
null // フロント側で結合するため null で返却
);
return res;
} catch (DataAccessException e) {
throw new UpdateException(updateErrorMessageByDataAccess, e);
}
}
/* データを削除するメソッド */
@Transactional(rollbackFor = TodoAppException.class)
public void deleteTodo(Long todoId) throws TodoAppException {
todoRepository.deleteById(todoId);
Todo res = todoRepository.findById(todoId).orElse(null);
if (res != null) throw new DeleteException(deleteErrorMessageByLoad);
}
/* リスト型の Todo エンティティをリスト型の Dto に変換するメソッド */
public List<ResponseTodoDto> convertResponseTodoDto(List<Todo> todos) {
return todos.stream()
.map(todo -> new ResponseTodoDto(
todo.getId(),
todo.getTitle(),
todo.getIsCheck(),
todo.getTodoCategories() == null ? new ArrayList<>() : // カテゴリーが設定されてない場合は空リストを設定
todo.getTodoCategories()
.stream()
.sorted(Comparator.comparing(todoCategory -> todoCategory.getCategory().getName())) // 名前順にソート
.map(todoCategory -> new ResponseTodoCategoryDto( // カテゴリー型に変換
todoCategory.getId(),
new Category(
todoCategory.getCategoryId(),
todoCategory.getCategory().getName()
)
))
.collect(Collectors.toList())
))
.collect(Collectors.toList());
}
}
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);
}
TodoCategoryController
Service と同じ理由で CUD メソッドだけ実装しています。
package com.ozack.todoapp.controller;
import java.net.URI;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ozack.todoapp.dto.response.ResponseTodoCategoryDto;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.repository.entity.TodoCategory;
import com.ozack.todoapp.service.todocategory.TodoCategoryService;
/* Todo と Category のマッピング情報を操作するコントローラー */
@RequestMapping("todocategory")
@RestController
public class TodoCategoryController {
private final TodoCategoryService todoCategoryService;
public TodoCategoryController(TodoCategoryService todoCategoryService) {
this.todoCategoryService = todoCategoryService;
}
/* データの登録 */
@PostMapping
public ResponseEntity<List<ResponseTodoCategoryDto>> postTodoCateories(
@RequestBody List<TodoCategory> req) throws TodoAppException
{
List<ResponseTodoCategoryDto> res = todoCategoryService.insertTodoCategories(req);
URI location = URI.create("/todocategories");
return ResponseEntity.created(location).body(res);
}
/* データの更新 */
@PutMapping
public ResponseEntity<List<ResponseTodoCategoryDto>> putTodoCateories(
@RequestBody List<TodoCategory> req) throws TodoAppException
{
List<ResponseTodoCategoryDto> res = todoCategoryService.updateTodoCategories(req);
return ResponseEntity.ok().body(res);
}
/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteTodoCateories(@RequestParam List<Long> ids) throws TodoAppException {
todoCategoryService.deleteTodoCategories(ids);
return ResponseEntity.noContent().build();
}
}
package com.ozack.todoapp.service.todocategory;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ozack.todoapp.dto.response.ResponseTodoCategoryDto;
import com.ozack.todoapp.exception.DeleteException;
import com.ozack.todoapp.exception.InsertException;
import com.ozack.todoapp.exception.TodoAppException;
import com.ozack.todoapp.exception.UpdateException;
import com.ozack.todoapp.repository.TodoCategoryRepository;
import com.ozack.todoapp.repository.entity.Category;
import com.ozack.todoapp.repository.entity.TodoCategory;
/* Todo と Category のマッピング情報を処理するサービス */
@Service
public class TodoCategoryServiceImpl implements TodoCategoryService {
private final String loadErrorMessage = "todo_category テーブルのデータ取得に失敗しました。";
private final String insertErrorMessageByDataAccess = "todo_category データ登録時にデータベース関連のエラーが発生しました。";
private final String updateErrorMessageByDataAccess = "todo_category データ更新時にデータベース関連のエラーが発生しました。";
private final String deleteErrorMessageByLoad = "todo_category データを削除できませんでした。";
private final TodoCategoryRepository todoCategoryRepository;
public TodoCategoryServiceImpl(TodoCategoryRepository todoCategoryRepository) {
this.todoCategoryRepository = todoCategoryRepository;
}
/* データを登録するメソッド */
@Override
@Transactional(rollbackFor = TodoAppException.class)
public List<ResponseTodoCategoryDto> insertTodoCategories(List<TodoCategory> todoCategories) throws TodoAppException {
try {
List<TodoCategory> resTodoCategories = todoCategoryRepository.saveAll(todoCategories);
List<ResponseTodoCategoryDto> res = convertResponseTodoCategoryDto(resTodoCategories);
if (res == null) throw new InsertException(loadErrorMessage);
return res;
} catch (DataAccessException e) {
throw new InsertException(insertErrorMessageByDataAccess, e);
}
}
/* データを更新するメソッド */
@Override
@Transactional(rollbackFor = TodoAppException.class)
public List<ResponseTodoCategoryDto> updateTodoCategories(List<TodoCategory> todoCategories) throws TodoAppException {
try {
List<TodoCategory> resTodoCategories = todoCategoryRepository.saveAll(todoCategories);
List<ResponseTodoCategoryDto> res = convertResponseTodoCategoryDto(resTodoCategories);
if (res == null) throw new UpdateException(loadErrorMessage);
return res;
} catch (DataAccessException e) {
throw new UpdateException(updateErrorMessageByDataAccess, e);
}
}
/* データを削除するメソッド */
@Override
@Transactional(rollbackFor = TodoAppException.class)
public void deleteTodoCategories(List<Long> todoCategoryIds) throws TodoAppException {
todoCategoryRepository.deleteAllById(todoCategoryIds);
List<TodoCategory> res = todoCategoryRepository.findAllById(todoCategoryIds);
if (!res.isEmpty()) throw new DeleteException(deleteErrorMessageByLoad);
}
/* Dto 変換メソッド */
public List<ResponseTodoCategoryDto> convertResponseTodoCategoryDto(List<TodoCategory> todoCategories) {
return todoCategories
.stream()
.map(todoCategory ->
new ResponseTodoCategoryDto(
todoCategory.getId(),
new Category(todoCategory.getCategoryId(), null)
)
)
.toList();
}
}
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> {
}
事前準備
次回で例外用のコントローラーを実装し、その次の回からはフロントエンド側の開発を行います。
そのため、最後に環境を整えるための準備を行います。
手順1:build.gradle の変更
build.gradle の内容を変更します。

spring-boot-starter-security を無効化します。
以下のように37行をコメントアウトしてください。
// gradle スクリプトを実行するために必要な依存関係を追加
buildscript {
dependencies {
// Flyway の MySQL ライブラリをビルドスクリプトに追加
classpath 'org.flywaydb:flyway-mysql:10.0.0'
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.4'
id 'io.spring.dependency-management' version '1.1.7'
id "org.flywaydb.flyway" version "10.0.0"
}
group = 'com.ozack' // ここはプロジェクト生成時に決めた名前にする
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
//implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
// マイグレーションツールの設定
flyway {
// データベースへの接続 URL を指定
url = 'jdbc:mysql://127.0.0.1:3306/tododatabase'
// データベースに接続するためのユーザー名を指定
user = 'myuser'
// データベースに接続するためのパスワードを指定
password = 'secret'
// Flywayが適用されるスキーマ(データベース)を指定
schemas = ['tododatabase']
// Flywayのクリーンアップ(マイグレーションを全て取り消し、データベースを初期状態に戻す設定)を無効
cleanDisabled = false
}
// 起動時にはテストを実行しない
tasks.named('test') {
enabled = false
}
// ./gradlew を実行したときに起動するタスクを設定
defaultTasks 'clean', 'build', 'bootRun'
手順2:application.properties
application.properties に記載しているロードファイルのパスも変更します。

下記内容に変更してください。
spring.application.name=todoapp
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tododatabase?serverTimezone=Asia/Tokyo
spring.datasource.username=myuser
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.flyway.locations=filesystem:src/main/resources/db/migration
今までは migration/table 配下にある SQL ファイルしかロードしていなかったのですが、ロード範囲を migration 配下まで拡張します。
初期データ登録
データベースに初期データとして登録するデータを作成します。
main/resources フォルダ内に data フォルダを作成してください。

次に、⬇️の SQL ファイル4つを data フォルダ内にコピペしてください。
これで事前準備は完了です。
まとめ
フロントエンド側と API 通信を行う Controller を作成しました。
main/…/controller
- フォルダ内の全ファイル
src/
- build.gradle
src/main/resources/
- application.properties
main/…/migration/data
- フォルダ内の全ファイル
プロジェクト構成は GitHub にプッシュしています。
これでフロントエンド側とデータの受け渡しができるようになります。
次回は例外ハンドリング用の Controller を作成します。