フロントエンド側とAPI通信を行うControllerの作成-SpringBoot入門⑨

SpringBoot(Todo):サムネイル画像9 Todoアプリ(SpringBoot)

前回は 業務ロジックを実行する Service を作成しました。

今回はフロント側と API 通信を行う Controller を作成します。

SpringBootでTodo開発:システム構成図1
システム構成図

Controller(コントローラー)とは?

フロントエンド側と API 通信をを行うファイルです。

アクセス用の url を設定し、フロントエンド側からリクエストが送信されると Service から処理結果を受け取り、レスポンスとしてフロント側へ返却します。

デフォルトだとレスポンスの返却値は JSON データに変換されます。

Controller 作成

作成する Controller は下記4つです。

  1. AuthorityController
  2. CategoryController
  3. TodoCategoryController
  4. TodoController

Userに関する Controller はまだ作成しません。

SpringSecurity 関連の処理が混ざるので、その回で作成します。

ファイル作成

todoapp フォルダ内に controller フォルダを作成します。

さらに、作成した controller フォルダ内に下記ファイルを作成してください。

  1. AuthorityController.java
  2. CategoryController.java
  3. TodoController.java
  4. TodocategoryController.java
SpringBootでToDo開発9:Controller の作成
Controller の作成

各Controllerの内容

下記2つを見ながら各 Service ファイルの中身を書いていきます。

  1. 第6回で作成した Repository
  2. 第8回で作成した Service
  3. 第2回で作成した以下の ER 図
2_userPK`user_id` bigint unsigned NOT NULL AUTO_INCREMENTFK`authority_id` int unsigned NOT NULL `name` varchar(16) NOT NULLUK`email` varchar(32) NOT NULL`password` varchar(32) NOT NULL`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP3_todoPK`todo_id` bigint unsigned NOT NULL AUTO_INCREMENTFK`user_id` bigint unsigned NOT NULLUK`title` varchar(32) NOT NULL`is_check` tinyint(1) NOT NULL`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP1_authorityPK`authority_id` int unsigned NOT NULL AUTO_INCREMENTUK`name` varchar(16) NOT NULL`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP1_cateoryPK`category_id` int unsigned NOT NULL AUTO_INCREMENTUK`name` varchar(16) NOT NULL`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP4_todo_categoryPK`todo_category_id` bigint unsigned NOT NULL AUTO_INCREMENTFK`todo_id` bigint unsigned NOT NULLFK`category_id` int unsigned NOT NULL`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

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 形式で返却されていることが分かります。

SpringBootでToDo開発9:AuthorityController(GETリクエスト) の返却値
GETリクエスト時の返却値

今回のように、フロントエンド側とバックエンド側で分離しているプロジェクトでは大体使います。

@GetMapping

先ほどの説明と全く同じになってしまいますが、下記状況で作動します。

  1. http://localhost:8080/authority にアクセスした
  2. フロント側からのリクエストが GET である

作動するといっても、このアノテーションが付与されてるメソッドが実行されるだけです。

要するに、AuthoriryController では下記メソッドが実行されます。

/* データの取得 */
@GetMapping
public ResponseEntity<List<Authority>> getAuthorities() {
    List<Authority> res = authorityService.selectAllAuthorities();
    return ResponseEntity.ok().body(res);
}

返却値に関しては、権限情報(res)と、処理が正常終了したことを示す 200(OK) を設定します。

実際に GET リクエストでアクセスすると⬇️のように返却されます。

SpringBootでToDo開発9:AuthorityController(GETリクエスト) の返却値
GETリクエスト時の返却値

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

HTTPステータスコード一覧とリクエストとレスポンスの意味を解説 | ITコラム|アイティーエム株式会社
HTTPステータスコードとはHTTPレスポンス行で表示される3桁の番号のことです。Webを利用する上でHTTPという通信規則のもと接続ができ、クライアントが送るリクエストとサーバが返すレスポンスによっ...

企業サイトですが、説明が分かりやすいです。

@PostMapping

下記状況で作動します。

  1. http://localhost:8080/authority にアクセスした
  2. フロント側からのリクエストが 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 リクエストでアクセスすると⬇️のように返却されます。

SpringBootでToDo開発9:AuthorityController(POSTリクエスト) の返却値
POSTリクエスト時の返却値

画面上部の JSON データは @RequestBody の res 変数に渡されるリクエストデータです。

画面下部の JSON データが返却されたレスポンスデータです。

@RequestBody

ついでに説明しときます。

名前のとおり、バックエンド側に渡されるデータです。

@RequestBody Authority req

上記の例では、フロント側から Authority エンティティが渡されます。

このデータ(req)はリクエストデータと呼ばれ、バックエンド側で登録したり更新したりします。

@PutMapping

下記状況で作動します。

  1. http://localhost:8080/authority にアクセスした
  2. フロント側からのリクエストが 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) を設定します。

ちなみに、更新終了を示すステータスコードは存在しません。

データ返すなら OK, 返さないなら No Content を設定することが多いです。

実際に PUT リクエストでアクセスすると⬇️のように返却されます。

SpringBootでToDo開発9:AuthorityController(PUTリクエスト) の返却値
PUTリクエスト時の返却値

画面上部の JSON データは @RequestBody の res 変数に渡されるリクエストデータです。

画面下部の JSON データが返却されたレスポンスデータです。

@DeleteMapping

下記状況で作動します。

  1. http://localhost:8080/authority にアクセスした
  2. フロント側からのリクエストが PUT である

AuthoriryController では下記メソッドが実行されます。

/* データの削除 */
@DeleteMapping
public ResponseEntity<Void> deleteAuthority(@RequestParam Long id) throws TodoAppException {
    authorityService.deleteAuthority(id);
    return ResponseEntity.noContent().build();
}

返却値に関しては、処理が正常終了し、レスポンスデータが空であることを示す 204(No Content) を設定します。

実際に Delete リクエストでアクセスすると⬇️のように返却されます。

SpringBootでToDo開発9:AuthorityController(DELETEリクエスト) の返却値
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 の内容を変更します。

SpringBootでToDo開発9: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 に記載しているロードファイルのパスも変更します。

SpringBootでToDo開発9: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 フォルダを作成してください。

SpringBootでToDo開発9:data フォルダを追加.
data フォルダを追加.

次に、⬇️の SQL ファイル4つを data フォルダ内にコピペしてください。

SpringBootTodoServer/src/main/resources/db/migration/data at main · o-zack-0390/SpringBootTodoServer
Todo データを処理するSpringプロジェクト. Contribute to o-zack-0390/SpringBootTodoServer development by creating an...

なお、これらデータは、このままフロント側に表示されます。

我流データにしたい場合は適当にいじってください。

これで事前準備は完了です。

まとめ

フロントエンド側と API 通信を行う Controller を作成しました。

main/…/controller

  • フォルダ内の全ファイル

src/

  • build.gradle

src/main/resources/

  • application.properties

main/…/migration/data

  • フォルダ内の全ファイル

プロジェクト構成は GitHub にプッシュしています。

GitHub - o-zack-0390/SpringBootTodoServer: Todo データを処理するSpringプロジェクト
Todo データを処理するSpringプロジェクト. Contribute to o-zack-0390/SpringBootTodoServer development by creating an...

これでフロントエンド側とデータの受け渡しができるようになります。

次回は例外ハンドリング用の Controller を作成します。

タイトルとURLをコピーしました