レシピApp開発備忘録(DBの用意・サービスクラスの用意・CRUD基礎)

## データベース
### エンティティクラス（テーブルに相当）を作成
```Recipe.java
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 に自動でテーブルを作らせる（推奨）
以下を追記
```application.properties
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操作などビジネスロジックを集中させる
```RecipeService.java
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とサービスクラスを分離したときのインジェクション
```DeleteController.java
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(クエリパラメータ)
- クエリパラメータの役割を提供
- ブラウザからサーバーに検索キーワード、ページ番号、並び順などの追加条件やオプションを渡すときに使用
### コントローラーからの変数の受け取り
タイムリーフ構文のなかで${変数}とする
### コード
```index.html
<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>
```

```SearchControeller.java
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保存ができる
### コード
### リポジトリ
```CookingRepository.java
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.Recipe;

public interface CookingRepository extends JpaRepository<Recipe,Long>{
//リポジトリと接続したいエンティティクラスとその主キーの型を指定
}
```
### ビュー
```create.html
<!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フォームなど）レベルで「この入力フィールドは必須」というバリデーションを意味する

### コントローラー
```NewController.java
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:/");
	}
}
```
![スクリーンショット (171).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932683/c8d15034-a66c-4aff-b1da-4fd747c3bb22.png)
![スクリーンショット (172).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932683/ced991e2-5ba2-41d8-80df-52b74cebd685.png)
## 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すれば指定したテンプレートに指定したデータが渡る 
### コード
### コントローラー
```UpdateController.java
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<Recipe> 型
→data.get() をすると、中にある Recipe インスタンスを取り出すことができる

### ビュー
```edit.html
<!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:/")
           → トップページへリダイレクト！

```
### コード
```DeleteController.java
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:/");
	}
}
```
```delete.html
<!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）を送信する場合は、こういう形にもできる：
```ex.java
@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 属性を使えば、条件を満たさないときは そのタグ自体が出力されない
```ex.html
<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チェックして、必要なときだけ値を渡す方法もある
```ex.html
<!-- 例: JSPの場合 -->
<c:if test="${showResult}">
  <p>${hoge}に関する検索結果を表示します</p>
</c:if>
```
```Controller.java
@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 をつけておかないとエラーになる