こんにちは、かつコーチです。
EntityとRepositoryの基本を押さえたところで、今回は実際にCRUD(作成・読み取り・更新・削除)をControllerからDBまで一通り繋げて実装してみます。
理屈は分かっても、実際に手を動かしてみないと感覚がつかめない部分なので、書籍管理を例にコードを追っていきましょう。
CRUDとは
4つの操作の意味
CRUDとは、Create(作成)・Read(読み取り)・Update(更新)・Delete(削除)の頭文字を取った言葉で、データベース操作の基本4パターンを指します。
Webアプリケーションのほとんどの機能は、この4つの組み合わせで成り立っています。
レイヤー構成の役割分担
Spring Bootでは、Controller・Service・Repositoryの3層に処理を分けるのが一般的です。
- Controller:HTTPリクエストの受付とレスポンスの返却
- Service:ビジネスロジック(バリデーションや複数Repositoryをまたぐ処理など)
- Repository:データベースへのアクセス
今回はServiceに前回作ったBookServiceを使い、そこにCRUDの各メソッドを追加していきます。
実装手順
Create:登録処理
// BookService.java
public Book createBook(Book book) {
return bookRepository.save(book);
}
// BookController.java
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@PostMapping
public Book createBook(@RequestBody Book book) {
return bookService.createBook(book);
}
}
save()は、主キーが未設定(null)の場合はINSERT、既に値がある場合はUPDATEとして動作します。
Read:一覧・詳細取得
@GetMapping
public List<Book> getAllBooks() {
return bookService.findAllBooks();
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
return bookService.findBookById(id);
}
Update:更新処理
// BookService.java
public Book updateBook(Long id, Book updatedBook) {
Book book = bookRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("該当する書籍が見つかりません: " + id));
book.setTitle(updatedBook.getTitle());
book.setAuthor(updatedBook.getAuthor());
book.setPrice(updatedBook.getPrice());
return bookRepository.save(book);
}
// BookController.java
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
return bookService.updateBook(id, book);
}
更新処理では、いったん既存データをfindById()で取得してからフィールドを書き換え、その状態でsave()する流れが基本です。
Delete:削除処理
// BookService.java
public void deleteBook(Long id) {
if (!bookRepository.existsById(id)) {
throw new IllegalArgumentException("該当する書籍が見つかりません: " + id);
}
bookRepository.deleteById(id);
}
// BookController.java
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
}
つまずきやすいポイント
更新のつもりが新規レコードを作ってしまう
❌ Before:新しいEntityインスタンスをそのまま保存する
public Book updateBook(Long id, Book updatedBook) {
updatedBook.setId(id);
return bookRepository.save(updatedBook);
}
一見動きそうに見えますが、リクエストボディに含まれないフィールドがnullのまま上書きされ、他のカラムのデータが消えてしまうことがあります。
筆者も検証中に、価格だけを更新するつもりが著者名まで空になってしまい、リクエストの中身をログに出して原因を突き止めた経験があります。
✅ After:既存Entityを取得してからフィールドを更新する
public Book updateBook(Long id, Book updatedBook) {
Book book = bookRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("該当する書籍が見つかりません: " + id));
book.setTitle(updatedBook.getTitle());
book.setAuthor(updatedBook.getAuthor());
book.setPrice(updatedBook.getPrice());
return bookRepository.save(book);
}
必要なフィールドだけを個別にセットすることで、意図しないデータ消失を防げます。
応用・一歩先の使い方
DTOを使ってリクエストとEntityを分離する
今回はController・ServiceでEntity(Book)をそのまま受け渡ししていますが、実務ではリクエスト用のDTO(Data Transfer Object)を別に用意するのが一般的です。
Entityをそのまま公開すると、内部の実装変更がAPIの仕様変更に直結してしまうリスクがあるためです。
この設計の考え方は、後続の「DTOとEntityを使い分ける設計の考え方」で詳しく扱います。
まとめ
この記事のポイント
- CRUDはController→Service→Repositoryの3層で実装するのが基本
- Create/Updateはどちらも
save()を使うが、更新時は既存Entityを取得してから値を書き換える - 削除前に
existsById()で存在確認をすると、エラーメッセージが分かりやすくなる - 実務ではEntityを直接公開せず、DTOで分離することが多い
次に読むべき記事
- Repositoryインターフェースの基本
- @OneToMany・@ManyToOneでリレーションを組む
- DTOとEntityを使い分ける設計の考え方
タグ: Spring Boot, 初心者向け, データベース