今回は以前学習したSpring Data JPAの内容をおさらいするべく検索機能を作成しようとしたところ、思った以上に躓き四苦八苦したので、失敗した内容と改善点を反省も兼ねてまとめていこうと思います。
完成形
※今回目標とした処理は
①検索項目にID番号を入力
②ヒットするデータを1つ取得する
③別の画面に遷移する
という処理です。
Controller
package com.example.demo.controller;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@Controller
@RequiredArgsConstructor
public class UserController {
@Autowired
private final UserRepository userRepository;
/*
* トップ画面へ
*/
@GetMapping("/")
public String showList(@ModelAttribute("user") User user, Model model) {
/*
* model.addAttribute("user", user);
*/
model.addAttribute("users", userRepository.findAll());
return "top";
}
/*
* 新規登録画面へ
*/
@GetMapping("/add")
public String insert(@ModelAttribute User user, Model model) {
/*
* model.addAttribute("user", user);
*/
return "new";
}
/*
* 登録後、トップへ
*/
@PostMapping("/add")
public String create(@ModelAttribute("user") User user, Model model) {
userRepository.save(user);
return "redirect:/";
}
/*
* 1件取得
*/
@PostMapping("/edit")
public String selectOne(@RequestParam("id") Integer id, Model model) {
Optional<User> list = userRepository.findById(id);
model.addAttribute("users",list.get());
return "edit";
}
/*
* 削除
*/
@PostMapping("/delete/{id}")
public String delete(@PathVariable("id") Integer id) {
userRepository.deleteById(id);
return "redirect:/";
}
}
Repository
package com.example.demo.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.example.demo.model.User;
@Repository
//JpaRepositoryの戻り値は、クラスとそのクラスの主キーの型
public interface UserRepository extends JpaRepository<User, Integer> {
}
Model
package com.example.demo.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@NotNull
@Size(min = 5, max = 20)
private String name;
@NotNull
@Size(min = 5, max = 20)
private String password;
@NotNull
private Integer age;
}
遷移元画面(top.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>検索用フォーム</h1>
<form th:action="@{/edit}" method="post">
ID検索:<input type="text" name="id">
<button type="submit">送信</button>
</form>
<ul th:each="user:${users}">
<li>
[[${user.id}]] - [[${user.name}]] - [[${user.password}]] - [[${user.age}]]
</li>
<li>
<form th:action="@{/delete/{id}(id=${user.id})}" th:method="delete">
<input type="submit" value="削除">
</form>
</li>
</ul>
<a href="/add">新規作成</a>
</body>
</html>
遷移先画面(edit.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<table>
<thead>
<tr>
<th>ID</th>
<th>名前</th>
<th>パスワード</th>
<th>年齢</th>
</tr>
</thead>
<tbody>
<tr th:each="user:${users}">
<td th:text="${user.id}"></td>
<td th:text="${user.name}"></td>
<td th:text="${user.password}"></td>
<td th:text="${user.age}"></td>
</tr>
</tbody>
</table>
</body>
</html>
画面表示(遷移元)
画面表示(遷移先)
※新規登録、削除、DBへの設定などの説明・解説は省略します。
失敗1
Controllerにて、Optionalインスタンスをそのままmodelにセットする。
/*
* 1件取得
*/
@PostMapping("/edit")
public String selectOne(@RequestParam("id") Integer id, Model model) {
Optional<User> list = userRepository.findById(id);
model.addAttribute("users",list);
return "edit";
}
一番最初ですがが一番原因がわからず、つまづいたところでした。
Optionalをそのまま入れると例外が遷移先で例外が発生し、弾かれました。内容は以下の通り。
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'id' cannot be found on object of type 'java.util.Optional' - maybe not public or not valid?
Optionalにidというフイールドがありません的なエラーになります。
Optionalに格納されている値を取得するには、変数名.get()メソッドを使用する必要があるみたいです。
失敗2 パラメータの値の渡し方が違う①
Controller
/*
* 1件取得
*/
@GetMapping("/edit/{id}")
public String selectOne(@RequestParam("id") Integer id, Model model) {
Optional<User> list = userRepository.findById(id);
model.addAttribute("users",list.get());
return "edit";
}
View
<h1>検索用フォーム</h1>
<form th:action="@{/edit/{id}(id=${id})}" method="get">
ID検索:<input type="text" name="${id}">
<button type="submit">送信</button>
</form>
そもそも@RequestParamと@PathVariableの使い分けと文法がわかっていなかったこともあり、色々混ざってしまっていました。
th:action="@{/edit/{id}(id=${id})}"
この記法はRest形式のURLに値を含ませる記法でした。URLでは、
/edit/1
という形で表示されます。
この記法では、Controller側では、@PathVariableで受け取りをするとのことでした。
@PostMapping("/edit/{id}")
public String selectOne(@PathVariable("id") Integer id, Model model) {
return "edit";
今回の失敗では、Controller側に受け取り先がないので、404エラーが発生しました。
失敗3 パラメータの値の渡し方が違う②
先ほど上記で説明した、REST形式用の正しい記載に直しても受け渡しには失敗しました。
理由は、URLにありました。
/edit/?id=1
URLはREST形式ではなく、クエリ文字列で渡していたので、結局Controller側で受け取る体制が整っておらず、404でエラーが発生します。
※inputタグの値を渡して検索等の処理を行うときは基本的にクエリになるのでしょうか。この辺は特に設定等はしておらず、詳細は不明です。
クエリ文字列のURLを受け取るには、@PathVariableではなく、@RequestParamを利用する必要があるので、下記の通りに修正。
Controller
@GetMapping("/edit")
public String selectOne(@RequestParam("id") Integer id, Model model) {
Optional<User> list = userRepository.findById(id);
model.addAttribute("users",list.get());
return "edit";
}
View
<h1>検索用フォーム</h1>
<form th:action="@{/edit}" method="get">
ID検索:<input type="text" name="id">
<button type="submit">送信</button>
</form>
※@RequestParamだけ変更しても同様にうまく渡せないので、全て変更すること。
失敗4 Modelがセットされていない
thymeleafの構文が理解できておらず、最初はこの様に書いていました。
今となっては何をしたいのかわかりませんが...
<h1>検索用フォーム</h1>
<form th:action="@{/edit}" method="get" th:object="${user}">
ID検索:<input type="text" name="id" th:value="*{id}">
<button type="submit">送信</button>
</form>
<ul th:each="user:${users}">
<li>
[[${user.id}]] - [[${user.name}]] - [[${user.password}]] - [[${user.age}]]
</li>
<li>
<form th:action="@{/delete/{id}(id=${user.id})}" th:method="delete">
<input type="submit" value="削除">
</form>
</li>
</ul>
その時のControllerの設定は以下の通りです。
/*
* トップ画面へ
*/
@GetMapping("/")
public String showList(Model model) {
model.addAttribute("users", userRepository.findAll());
return "top";
}
最初にviewにリクエストを送る際、usersでしかmodelにセットしておらず、userをセットしていなかったのでエラーが発生。
Caused by: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'user' available as request attribute
UserクラスをModelにセットしたいので、Controllerを以下のように修正。
@GetMapping("/")
public String showList(@ModelAttribute("user") User user, Model model) {
/*
* model.addAttribute("user", user);
*/
model.addAttribute("users", userRepository.findAll());
return "top";
}
@ModelAttributeとすることで、以下のmodelに対する処理が省略でき、かつ先ほどセットできていなかったUserクラスがmodelにセットされた状態でtop.htmlに遷移することができます。
@GetMapping("/")
public String showList(@ModelAttribute("user") User user, Model model) {
/*
* model.addAttribute("user", user);
*/
}
最後に
今回の1件で thymeleafの構文とそれぞれの意味を以前よりしっかり把握することができました。
かなり頭を抱えましたが、とても良い経験だったと思います。参考になりましたら幸いです。
参考文献