データベース
エンティティクラス(テーブルに相当)を作成
package com.example.demo;
import jakarta.persistence.Column; //追記
import jakarta.persistence.Entity; //追記
import jakarta.persistence.GeneratedValue; //追記
import jakarta.persistence.GenerationType; //追記
import jakarta.persistence.Id; //追記
import jakarta.persistence.Table; //追記
@Entity
@Table(name="recipes")
public class Recipe {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column
private long id;
@Column(length=50,nullable=false)
private String name; //レシピと料理名は一対一なのでリレーションなし
@Column
private String comment; //レシピとコメントの記述は一対多なのでdescriptionsテーブルにrecipe_id
@Column
private int cooking_time; //レシピと料理時間は一対一なのでリレーションなし
@Column
private int servings; //レシピと人数は一対一なのでリレーションなし
@Column
private int user_id; //レシピと投稿ユーザーは多対一なのでrecipesテーブルがuser_idをもつ
@Column
private String main_image; //レシピとメイン画像は一対一なのでリレーションなし
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public int getCookingTime() {
return cooking_time;
}
public void setCookingTime(int cooking_time) {
this.cooking_time = cooking_time;
}
public int getServings() {
return servings;
}
public void setServings(int servings) {
this.servings = servings;
}
public int getUser() {
return user_id;
}
public void setUser(int user_id) {
this.user_id = user_id;
}
public String getMainImg() {
return main_image;
}
public void setMainImg(String MainImg) {
this.main_image = MainImg;
}
}
対応するDBを作成
方法は二つ
- MySQLなどで手動でSQLかいてテーブル作成
- pring Boot に自動でテーブルを作らせる(推奨)
以下を追記
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/your_db_name?useSSL=false&serverTimezone=UTC
spring.datasource.username=your_user
spring.datasource.password=your_pass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
ただし
- MySQLに接続できる状態になっていること(DBとユーザーが作られている)が前提
- 本番運用では ddl-auto=none か validate を推奨
リポジトリ階層でエラー
アプリ起動→エラーでた!
原因はリポジトリの階層
com.example.demo ← 起点(@SpringBootApplicationがある)
├── HelloController.java
├── Recipe.java
com.example.recipe_appRepository ← リポジトリ(別階層)
├── CookingRepository.java
→これだとrecipe_appRepository が demo パッケージの外にあるので、Spring Boot はデフォルトのスキャンでは見つけてくれない
→CookingRepository を demo 配下に移動して解決
サービスクラス
コントローラにはリクエストやレスポンス対応、サービスクラスにはDB操作などビジネスロジックを集中させる
package com.example.demo.Services;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.Recipe;
import com.example.demo.repository.CookingRepository;
@Service
public class RecipeService {
private final CookingRepository repository;
public RecipeService(CookingRepository repository) {
this.repository = repository;
}
public Optional<Recipe> findById(long id) {
//リポジトリの既存メソッドを引数渡して呼び出す
return repository.findById(id);
}
@Transactional
public void deleteById(long id) {
repository.deleteById(id);
}
}
Servicesパッケージの中にサービスクラス作成
コンストラクタインジェクション
Spring Boot はアプリ起動時に次のことをする
- @Service や @Repository, @Controller(Beanとして登録されたって証) などが付いたクラスを見つける
- それらを自動でインスタンス化(new みたいなこと)する(この作成されたインスタンスを「Bean(ビーン)」と呼ぶ)
- それを使いたい場所に「注入(インジェクション)」する→RecipeService recipeService という引数を見て、Springは「あっ、RecipeService のインスタンス(Bean)なら持ってるから、ここに渡してあげよう」ってする
今までは@Autowiredを使って@RepositoryがついたCookingRepositoryのインスタンスを自動的に注入してた
→コンストラクタインジェクションに変更
→final でプロパティ(フィールド)を宣言しておいて、コンストラクタで注入するのが今の主流の書き方
例:DeleteControllerとサービスクラスを分離したときのインジェクション
package com.example.demo;
import java.util.List;
import java.util.Optional;
import jakarta.transaction.Transactional;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import com.example.demo.Recipe;
import com.example.demo.repository.CookingRepository;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.demo.Services.RecipeService;
@Controller
public class DeleteController {
//コンストラクタインジェクションでRecipeServiceを扱えるようにする
private final RecipeService recipeService;
public DeleteController(RecipeService recipeService) {
this.recipeService = recipeService;
}
//特定のIDのリソースを画面表示
@RequestMapping(value="/delete/{id}",method= RequestMethod.GET)
public ModelAndView delete(@PathVariable int id,ModelAndView mav) {
mav.setViewName("delete");
mav.addObject("msg","どのレシピを削除しますか?");
Optional<Recipe> data = recipeService.findById((long)id);
if(data.isPresent()) {
mav.addObject("formModel",data.get());
}else if(data.isEmpty()){
mav.addObject("formModel",null);
mav.addObject("message","データがみつかりません");
}
return mav;
}
@PostMapping("/delete_clicked")
public ModelAndView remove(@RequestParam long id,ModelAndView mav) {
recipeService.deleteById(id);
return new ModelAndView("redirect:/");
}
}
検索(リダイレクトなし)
フロー
[ユーザーのブラウザ]
│
▼
index.html(検索フォーム)
┌──────────────────────────────┐
│ <form method="get" action="/search"> │
│ <input name="dish-name"> │
│ <button>検索</button> │
└──────────────────────────────┘
★★ クエリパラメータのしくみ ★★
inputタグのname属性=クエリパラメータのキー名とすることで
inputタグに入れられた検索キーワード=クエリパラメータの値とすることができる
│
▼
GETリクエスト(/search?dish-name=〇〇)
│
▼
[Spring Bootのコントローラー]
SearchController#search()
┌─────────────────────────────────────────────┐
│ @RequestParam("dish-name") → 値はkeywordに格納
│ mod.addAttribute("hoge", keyword) hogeに値が渡る│
│ return "index"; → index.htmlに戻る │
└─────────────────────────────────────────────┘
│
▼
Thymeleafテンプレートエンジンが index.html を処理
┌───────────────────────────────────────────────┐
│ <p th:text="${hoge} + 'に関する検索結果を表示します'> │
│ → 例:「カレーに関する検索結果を表示します」 │
└───────────────────────────────────────────────┘
│
▼
[ユーザーのブラウザに表示]
「カレーに関する検索結果を表示します」
Model
- Modelとはコントローラとビューの間でデータをやり取りする役割を持つオブジェクト
- コントローラで生成したデータをビューに渡せる
- addAttributeメソッドでViewで使う変数とその変数に格納したいデータを定義
@RequestParam(クエリパラメータ)
- クエリパラメータの役割を提供
- ブラウザからサーバーに検索キーワード、ページ番号、並び順などの追加条件やオプションを渡すときに使用
コントローラーからの変数の受け取り
タイムリーフ構文のなかで${変数}とする
コード
<body>
<p>レシピを検索してください</p>
<form method="get" action="/search">
<input type="text" name="dish-name" placeholder="料理名をどうぞ">
<button type="submit">検索</button>
</form>
<p th:text="${hoge}+'に関する検索結果を表示します'"></p>
</body>
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; //追記
import org.springframework.web.bind.annotation.RequestParam; ///追記
@Controller
public class SearchController {
@RequestMapping(value="/search",method=RequestMethod.GET)
//クエリパラメータ dish-name を keyword 変数に格納
public String search(@RequestParam("dish-name") String keyword,Model mod) {
mod.addAttribute("hoge",keyword);
return "index";
}
}
Create
フロー
[ユーザーのブラウザ]
│
▼
GET /create リクエスト
│
▼
[NewController#create()]
┌─────────────────────────────────────────────┐
│ ModelAndView: │
│ - View指定: "create" │
│ - 変数title: "新しいレシピを作ってください!"│
│ - formModel: new Recipe() │
└─────────────────────────────────────────────┘
空のformModel変数にRecipeクラスのインスタンスを作成&格納してビューに渡す
│
▼
[create.html](Thymeleafで表示)
┌──────────────────────────────────────┐
│ <form th:object="${formModel}"> │
│ <input th:field="*{name}"> │ ← Recipe.name
│ <input th:field="*{comment}"> │ ← Recipe.comment
└──────────────────────────────────────┘
│
▼
[ユーザーがフォームに入力](Recipeのプロパティを入力してもらう)
例:name=カレー, comment=簡単に作れるよ
│
▼
POST /create リクエスト(form送信)
FormModel.name=カレー,FormModel.comment=簡単に作れるよなどをformModel(Recipeオブジェクト)に格納してコントローラーに送信
│
▼
[NewController#post()]
@ModelAttributeでFormModel→Recipeオブジェクトに変換して
recipe.name,recipe.commentにする
┌─────────────────────────────────────────────┐
│ @ModelAttribute("formModel") Recipe recipe │
│ repository.saveAndFlush(recipe); │ ← DB保存
│ return redirect:/ │ ← トップへリダイレクト
└─────────────────────────────────────────────┘
│
▼
[データベース]
┌────────────────────────────┐
│ Recipeエンティティに保存 │
│ id, name="カレー", comment=...│
└────────────────────────────┘
│
▼
[トップページへリダイレクト]
ModelAttribute(フォームをオブジェクトに)
- リクエストパラメータ(フォームの値)をオブジェクトに変換し、Controllerの引数として受け取れるアノテーション
- HTMLフォームを th:object="formModel"とすることで、そのフォームは formModel という名前のオブジェクトとつながる
→formModel.name, formModel.comment としてコントローラーにデータが送信される
→送信されたデータをRecipe型変数recipe の name, comment プロパティにマッピングしたいので@ModelAttribute("フォームのオブジェクト名") 型 受け取る変数 とすることで
formModel.name(=カレー), formModel.comment(=簡単に作れるよ)が
recipe.name(=カレー), recipe.comment(=簡単に作れるよ)になる - あとはリポジトリを通じて(=リポジトリのメソッドで)それを保存すればDB保存ができる
コード
リポジトリ
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.Recipe;
public interface CookingRepository extends JpaRepository<Recipe,Long>{
//リポジトリと接続したいエンティティクラスとその主キーの型を指定
}
ビュー
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${title}"></title>
</head>
<body>
<form name="new" action="/create" method="post" th:object="${formModel}">
<label for="dish-name">料理名</label>
<input type="text" name="name" id="dish-name" th:field="*{name}">
<label for="dish-comment">コメント</label>
<input type="text" name="comment" id="dish-comment" th:field="*{comment}">
<input type="submit" class="btn btn-primary"value="作成"/>
</form>
</body>
</html>
- inputタグのid属性とlabelのfor属性は基本的に一致させる
→ラベルをクリックしたときに対応する入力欄にフォーカスが当たるようになる - nullable = false=「データベースでNULL禁止」似ているけどrequired = trueは画面(HTMLフォームなど)レベルで「この入力フィールドは必須」というバリデーションを意味する
コントローラー
package com.example.demo;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import com.example.demo.repository.CookingRepository;
@Controller
public class NewController {
@Autowired
CookingRepository repository;
//Createページを表示
@RequestMapping(value="/create")
public ModelAndView create(ModelAndView mav) {
mav.setViewName("create");
mav.addObject("title","新しいレシピを作ってください!");
mav.addObject("formModel", new Recipe()); // ← これを追加
return mav;
}
//サーバーに新しいデータを送信
@Transactional
@PostMapping("/create")
public ModelAndView post(@ModelAttribute("formModel") Recipe recipe, ModelAndView mav) {
mav.setViewName("create");
repository.saveAndFlush(recipe);
return new ModelAndView("redirect:/");
}
}
Update
フロー
【① 編集画面の表示(GET /edit/{id})】
[ ユーザーがURLにアクセス ]
|
/edit/5
↓
┌────────────────────────────┐
│ UpdateController.edit(...) │ ← @PathVariable で id=5 を受け取る
└────────────────────────────┘
↓
repository.findById(5) でデータ取得
↓
Optional<Recipe> data に格納
↓ ↓
[あれば] data.get() で取り出す
[なければ] null + メッセージ表示
↓
formModel として View に渡す
↓
┌────────────────┐
│ edit.html │
└────────────────┘
↓
th:object="${formModel}" を使い
th:field="*{name}", *{comment} で
料理名・コメントを初期表示!
---------------------------------------------------
【② 編集内容を送信して更新(POST /edit)】
[ ユーザーが編集して「更新」押下 ]
↓
フォームが POST /edit を送信
name, comment, id を含む
↓
┌────────────────────────────┐
│ UpdateController.update(...)│
└────────────────────────────┘
↓
@ModelAttribute Recipe recipe に
フォームの内容がバインドされる
recipe.id = 5, name = ..., comment = ...
↓
repository.saveAndFlush(recipe) により
→ 既存IDのレコードが更新される!
↓
redirect:/ にリダイレクト(トップへ)
PathVariable
- ユーザーIDや記事IDなど特定のリソースへのアクセス時に使用
- URLの一部(=パスパラメータ)をメソッド引数にバインドするためのアノテーション
- リクエストマッピングのルート指定の部分で{}部分をパスパラメータとして取得→@PathVariableのついた変数に格納
ユーザーがアクセス: /edit/5
↓
URLマッピング: /edit/{id}
↓
@PathVariable で受け取る: int id = 5
ModelAndView
- データと画面の指定を同時に行えるクラス
- データと画面を指定して最後にreturnすれば指定したテンプレートに指定したデータが渡る
コード
コントローラー
package com.example.demo;
import java.util.List;
import java.util.Optional;
import jakarta.transaction.Transactional;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.Recipe;
import com.example.demo.repository.CookingRepository;
@Controller
public class UpdateController {
@Autowired
CookingRepository repository;
@RequestMapping("/edit/{id}")//パスパラメータ
public ModelAndView edit(ModelAndView mav,@PathVariable int id) {
mav.setViewName("edit");
Optional<Recipe> data = repository.findById((long)id);
if(data.isPresent()) {
mav.addObject("formModel",data.get());
}else if(data.isEmpty()){
mav.addObject("formModel",null);
mav.addObject("message","データが見つかりません");
}
return mav;
}
//保存
@PostMapping("/edit")
@Transactional
public ModelAndView update(@ModelAttribute Recipe recipe,ModelAndView mav) {
repository.saveAndFlush(recipe);
return new ModelAndView("redirect:/");
}
}
- Optional は「値があるかもしれないし、ないかもしれない」ということを表すJavaのクラス
→レコードが見つかれば Optional に中身が入っていて、見つからなければ中身が「空(empty)」 - data は Optional 型
→data.get() をすると、中にある Recipe インスタンスを取り出すことができる
ビュー
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>レシピの変更</title>
</head>
<body>
<div th:if="${formModel==null}">
<p th:text="${message}"></p>
</div>
<div th:if="${formModel != null}">
<form name="update" action="/edit" method="post" th:object="${formModel}">
<input type="hidden" name="id" th:field="*{id}"/>
<label for="dish-name">料理名</label>
<input type="text" name="name" id="dish-name" th:field="*{name}">
<label for="dish-comment">コメント</label>
<input type="text" name="comment" id="dish-comment" th:field="*{comment}">
<input type="submit" class="btn btn-primary"value="更新"/>
</form>
</div>
</body>
</html>
Delete
フロー
【① 削除画面の表示(GET /delete/{id})】
[ ユーザーがURLにアクセス ]
|
/delete/3
↓
┌────────────────────────────┐
│ DeleteController.delete(...) │ ← @PathVariable で id=3 を受け取る
└────────────────────────────┘
↓
recipeService.findById(3) でデータ取得
↓
Optional<Recipe> data に格納
↓ ↓
[あれば] data.get() で取り出す
[なければ] null + エラーメッセージ
↓
formModel として View に渡す
↓
┌────────────────┐
│ delete.html │
└────────────────┘
↓
th:object="${formModel}" によりフォーム構成
th:text="*{name}", *{comment} でレシピ情報を表示!
【② 削除実行(POST /delete_clicked)】
[ ユーザーが「削除」ボタン押下 ]
↓
フォームが POST /delete_clicked を送信
(hidden の id を含む)
↓
┌────────────────────────────┐
│ DeleteController.remove(...) │ ← @RequestParam long id で受け取る
└────────────────────────────┘
↓
recipeService.deleteById(id)
→ IDに対応するレコードを削除!
↓
new ModelAndView("redirect:/")
→ トップページへリダイレクト!
コード
package com.example.demo;
import java.util.List;
import java.util.Optional;
import jakarta.transaction.Transactional;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import com.example.demo.Recipe;
import com.example.demo.repository.CookingRepository;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.demo.Services.RecipeService;
@Controller
public class DeleteController {
private final RecipeService recipeService;
public DeleteController(RecipeService recipeService) {
this.recipeService = recipeService;
}
//特定のIDのリソースを画面表示
@RequestMapping(value="/delete/{id}",method= RequestMethod.GET)
public ModelAndView delete(@PathVariable int id,ModelAndView mav) {
mav.setViewName("delete");
mav.addObject("msg","どのレシピを削除しますか?");
Optional<Recipe> data = recipeService.findById((long)id);
if(data.isPresent()) {
mav.addObject("formModel",data.get());
}else if(data.isEmpty()){
mav.addObject("formModel",null);
mav.addObject("message","データがみつかりません");
}
return mav;
}
@PostMapping("/delete_clicked")
public ModelAndView remove(@RequestParam long id,ModelAndView mav) {
recipeService.deleteById(id);
return new ModelAndView("redirect:/");
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>削除画面</title>
</head>
<body>
<p th:text="${msg}"></p>
<div th:if="${formModel == null}">
<p th:text="${message}"></p>
</div>
<div th:if="${formModel != null}">
<form name="eliminate" method="post" action="/delete_clicked" th:object="${formModel}">
<input type="hidden" name="id" th:field="*{id}"/>
<ul>
<li>料理名: <span th:text="*{name}"></span></li>
<li>コメント: <span th:text="*{comment}"></span></li>
</ul>
<input type="submit" class="btn btn-primary"value="削除"/>
</form>
</div>
</body>
</html>
🔄 補足:もし @ModelAttribute を使ったら?
フォームで複数フィールド(nameやcomment)を送信する場合は、こういう形にもできる:
@PostMapping("/delete_clicked")
public String remove(@ModelAttribute Recipe formModel) {
repository.deleteById(formModel.getId());
return "redirect:/";
}
フォームの各フィールド(idやnameなど)の値をRecipeクラスの該当するフィールドに自動でセットして、できあがったRecipeオブジェクトをformModelという変数名で使えるようにしている
(POST送信されたらようやく一人前のRecipeオブジェクトに進化して、formModelという変数に格納して使えるようになったイメージ)
変数の値がnullの場合はテキストを非表示にする
- JSによる解決はどうか?
→th:text="${hoge}+'に関する検索結果を表示します'" はサーバーサイド(Thymeleaf)で処理されるため、JavaScriptで操作する前にHTMLとして出力されてしまう
→JavaScriptで非表示にするには、HTMLを一度出力してから非表示にすることになるため、不自然(SEO的にもよくない) - Thymeleafでは ${hoge} が null や空文字かどうかを条件分岐できる
→th:if 属性を使えば、条件を満たさないときは そのタグ自体が出力されない
<p th:if="${hoge ! = null and !#strings.isEmpty(hoge)}"
th:text="${hoge}+'に関する検索結果を表示します'"></p>
- ${...}:Thymeleafの式評価。Model に渡された値をここで参照
- hoge ! = null→モデル変数 hoge が null でないことを確認
- #strings は Thymeleaf のビルトインオブジェクト「文字列ユーティリティ」
- isEmpty(...) は Javaの String.isEmpty() と同様で、文字列が ""(長さ0)かどうかを判定
- !#strings.isEmpty(hoge) は 「hogeが空文字じゃない」という意味
- 「かつ」は&&ではなくandなので書き間違い注意
コントローラー側でnullチェックして、必要なときだけ値を渡す方法もある
<!-- 例: JSPの場合 -->
<c:if test="${showResult}">
<p>${hoge}に関する検索結果を表示します</p>
</c:if>
@RequestMapping("/search")
public String search(@RequestParam(value = "dish-name", required = false) String keyword, Model model) {
boolean showResult = keyword != null && !keyword.trim().isEmpty();
model.addAttribute("showResult", showResult);
model.addAttribute("hoge", keyword);
return "index";
}
- required = false→このパラメータが無くてもエラーにしない という意味
- デフォルトでは @RequestParam は必須(required = true)なので、URLに ?dish-name= が無いと Spring はエラーを出す
例えばhttp://localhost:8080/searchというアクセスしたらdish-name が指定されていないため、required = false をつけておかないとエラーになる