こんにちは。船井総研デジタルのいっちーです。
前回の記事では、サーバーと画面の間でやり取りされるデータの考察と、それに基づいてControllerクラスの修正を行いました。
今回は、画面側の修正を行って、ロジック本体の実装も行っていきたいと思います。
Thymeleafテンプレートの修正
Thymeleafテンプレートを修正して、サーバーからのデータを受け取れるようにしていきます。ControllerクラスのメソッドでModel型引数に設定した属性名と、Thymeleafテンプレートの変数名を合わせることで、その部分にデータが埋め込まれます。
メイン画面
メイン画面は、下記の4箇所を修正します。
つぶやき表示部分
(略)
<!-- posted message start -->
+<div th:each="negiit: ${negiits}">
<div class="row mt-3">
<div class="col">
- <h6>○○○○さん</h6>
+ <h6 th:text="${negiit.registerName}">○○○○さん</h6>
</div>
<div class="col">
- <span class="text-right">投稿日時: 2023年01月01日 00時00分00秒</span>
+ <span class="text-right" th:text="${#dates.format(negiit.registerDatetime, '投稿日時: yyyy年MM月dd日 HH:mm:ss')}">投稿日時: 2023年01月01日 00時00分00秒</span>
</div>
</div>
<div class="row mt-3">
- <div class="col">
+ <div class="col" th:text="${negiit.negiMessage}">
○○ねぎって美味しいね!
</div>
</div>
<hr>
<!-- posted message end -->
+</div>
(略)
ページ移動部分
(略)
<div class="col-3 text-start">
- <a class="btn btn-light">前のページへ</a>
+ <a class="btn btn-light" th:href="@{|/negitter?page=${prevPage}|}">前のページへ</a>
</div>
<div class="col-3"><!-- nothing --></div>
<div class="col-3 text-end">
- <a class="btn btn-light">次のページへ</a>
+ <a class="btn btn-light" th:href="@{|/negitter?page=${nextPage}|}">次のページへ</a>
</div>
(略)
つぶやき投稿・エラーメッセージ部分
(略)
-<form method="POST" action="#" th:action="@{/negiit}"">
+<form method="POST" action="#" th:action="@{/negiit}" th:object="${negiitForm}">
<div class="container">
- <p class="alert alert-danger">ねぎでつぶやいてください!</p>
+ <div class="alert alert-danger" th:if="${not errorMessages.isEmpty()}" th:each="errorMessage: ${errorMessages}">
+ <p th:text="${errorMessage}">ねぎでつぶやいてください!</p>
+ </div>
<div class="row mt-3">
<div class="col">
おなまえ: <input type="text" name="name" class="form-control" placeholder="おなまえ" required="required">
</div>
</div>
<div class="row mt-3">
<div class="col">
メッセージ: <input type="text" name="negiit" class="form-control" placeholder="メッセージ" required="required"><br>
</div>
</div>
<div class="row">
<div class="col">
<button type="submit" class="btn btn-primary btn-block">つぶやく</button>
</div>
</div>
+ <input type="hidden" value="1" id="pageNum" name="pageNum" th:value="${pageNum}"/>
</div>
</form>
(略)
ねぎ登録画面
ねぎ登録画面は、下記の4箇所を修正します。
ねぎ登録・エラーメッセージ部分
(略)
-<form method="POST" action="#" th:action="@{/registerNegi}">
+<form method="POST" action="#" th:action="@{/registerNegi}" th:object="${registerNegiForm}">
<div class="container">
<div class="row mt-3">
<div class="col">
<h3>ねぎを登録する</h3>
</div>
</div>
- <p class="alert alert-danger">そのねぎは登録済みです!</p>
+ <div class="alert alert-danger" th:if="${not errorMessages.isEmpty()}" th:each="errorMessage: ${errorMessages}">
+ <p th:text="${errorMessage}">そのねぎは登録済みです!</p>
+ </div>
<div class="row mt-3">
<div class="col">
<input type="text" name="negiName" placeholder="○○ねぎ" required="required" maxlength="128" size="110">
</div>
<div class="col"></div>
<div class="col">
<button type="submit" class="btn btn-primary btn-block">登録</button>
</div>
</div>
</div>
</form>
(略)
ねぎ一覧・更新部分
(略)
-<form method="POST" id="updateForm" action="#" th:action="@{/updateNegi}">
+<form method="POST" id="updateForm" action="#" th:action="@{/updateNegi}" th:object="${updateNegiForm}">
<div class="container">
<div class="row mt-3">
<div class="col">
<h3>登録済みねぎ一覧</h3>
</div>
</div>
<div class="row mt-3">
<table class="table table-light table-striped table-borderd table-condensed">
<tr>
<th>
No
</th>
<th>
名称
</th>
<th>
編集
</th>
</tr>
<!-- negi info start -->
- <tr>
+ <tr th:each="negi, iterStat: ${negiList}">
+ <td th:text="__${iterStat.count}__">
1
</td>
<td>
- <input type="text" id="negi" name="negi" required maxlength="128" size="100" value="○○ねぎ">
+ <input type="text" th:id="|negiName[__${iterStat.index}__]|" required="required" maxlength="128" size="100" value="○○ねぎ" th:value="${negi.negiName}">
</td>
<td>
- <a class="btn btn-warning">修正</a>
+ <button class="btn btn-warning" name="updateBtn" th:id="|updateBtn[__${iterStat.index}__]|" th:value="__${iterStat.index}__">修正</button>
- <a class="btn btn-warning">削除</a>
+ <button class="btn btn-secondary" name="deleteBtn" th:id="|deleteBtn[__${iterStat.index}__]|" th:value="__${iterStat.index}__">削除</button>
</td>
+ <input type="hidden" th:id="|id[__${iterStat.index}__]|" th:value="${negi.id}">
</tr>
<!-- negi info end -->
</table>
</div>
</div>
+ <input type="hidden" name="id" id="id" value=""/>
+ <input type="hidden" name="process" id="process" value=""/>
+ <input type="hidden" name="negiName" id="negiName" value=""/>
</form>
(略)
ボタンのvalueに繰り返し項目のindexを忍ばせておくことで、どの行のねぎに対して処理を行うのかを特定できるようにしています。ねぎIDは行のindexとは無関係なので、隠し項目に持たせて拾えるようにしています。
また、押下されたボタンに対応する処理とねぎIDを動的に設定できるように、Javascriptに処理を追加します。JQueryを使うので、HTMLにもJQueryのインポートを追加します。
JQueryのインポート追加
(略)
+<script src="https://code.jquery.com/jquery-3.6.3.js" integrity="sha256-nQLuAZGRRcILA+6dMBOvcRh5Pe310sBpanc6+QBmyVM=" crossorigin="anonymous"></script>
(略)
Javascript
$(document).ready(function(){
$('[name="updateBtn"]').on(
'click',
function(e){
e.preventDefault();
var index = e.target.value;
$('#id').val($('#id\\[' + index + '\\]').val());
$('#process').val('UPDATE');
$('#negiName').val($('#negiName\\[' + index + '\\]').val());
$('#updateForm').submit();
}
)
$('[name="deleteBtn"]').on(
'click',
function(e){
e.preventDefault();
var index = e.target.value;
$('#id').val($('#id\\[' + index + '\\]').val());
$('#process').val('DELETE');
$('#negiName').val($('#negiName\\[' + index + '\\]').val());
$('#updateForm').submit();
}
)
})
DBとの連携ができるようにする
前回自動生成したEntityクラスを使ってDBにアクセスできるように、実装を追加していきます。
プロパティファイルの設定追加
application.propertiesに、DBのアクセス情報を追記します。
プロパティファイル
## DB setting
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/negitter?serverTimezone=Asia/Tokyo
spring.datasource.username=user
spring.datasource.password=pass
spring.jpa.database=MYSQL
## MyBatis setting
mybatis.configuration.map-underscore-to-camel-case=true
現時点ではVSCodeからシステムを起動してるので、DBの接続先をlocalhostに向けています。APコンテナ上で動かす際にはDBコンテナに向くように修正します。
mapperクラスの修正
つぶやきとねぎ情報を取得するためのSQLを追加します。
つぶやき取得の設定追加
(略)
+List<MessageTbl> findAll(Integer pageSize, Integer offset);
+Integer count();
(略)
(略)
+<select id="findAll" resultMap="BaseResultMap">
+ select 'false' as QUERYID,
+ <include refid="Base_Column_List" />
+ from message_tbl order by
+ register_datetime desc
+ limit #{pageSize} offset #{offset}
+</select>
+<select id="count" resultType="java.lang.Integer">
+ select count(*)
+ from message_tbl
+</select>
(略)
ねぎ情報取得の設定追加
(略)
+List<NegiMst> findAll();
+int count(NegiMst negiMst);
(略)
(略)
+<select id="findAll" resultMap="BaseResultMap">
+ select 'false' as QUERYID,
+ <include refid="Base_Column_List" />
+ from negi_mst
+ order by register_datetime desc
+</select>
+<select id="count" parameterType="com.example.negitter.entity.NegiMst" resultType="int">
+ select count(*)
+ from negi_mst
+ where negi_name = #{negiName}
+</select>
(略)
Serviceクラスの作成
ロジックの本体となるServiceクラスを作成します。クラスに@Service
アノテーションを付与することで、そのクラスがServiceクラスとみなされます。
Serviceクラス
@Service
public class NegitterService {
@Autowired
private NegiMstMapper negiMstMapper;
@Autowired
private MessageTblMapper messageTblMapper;
/**
* ページ数とページサイズを指定してつぶやきを取得する。
*
* @param pageNum
* @param pageSize
* @return 取得したつぶやき
*/
public List<MessageTbl> findAllNegiits(int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
List<MessageTbl> negiits = messageTblMapper.findAll(pageSize, offset);
return negiits;
}
/**
* つぶやきの件数を取得する。
*
* @return つぶやきの件数
*/
public Integer countNegiits() {
Integer count = messageTblMapper.count();
return count;
}
/**
* つぶやきを登録する。
*
* @param name 投稿者名
* @param negiit つぶやき本文
* @return エラーメッセージ
*/
public List<String> registerNegiit(String name, String negiit) {
List<String> errorMessages = new ArrayList<>();
Date registerdDataTime = Date.from(Instant.now());
MessageTbl messageTbl = new MessageTbl();
try {
messageTbl.setNegiMessage(negiit);
messageTbl.setRegisterName(name);
messageTbl.setRegisterDatetime(registerdDataTime);
messageTblMapper.insert(messageTbl);
} catch (Exception e) {
errorMessages.add("つぶやき登録時にエラーが発生しました。");
}
return errorMessages;
}
/**
* ねぎ情報を取得する
*
* @return ねぎマスタ
*/
public List<NegiMst> findAllNegi() {
List<NegiMst> negiMsts = negiMstMapper.findAll();
return negiMsts;
}
/**
* ねぎ情報を登録する
*
* @param negiMst 登録するねぎ情報
* @return エラーメッセージ
*/
public List<String> registerNegiMst(String negiName) {
List<String> errorMessages = new ArrayList<>();
Date registerdDataTime = Date.from(Instant.now());
NegiMst negiMst = new NegiMst();
try {
negiMst.setNegiName(negiName);
negiMst.setRegisterDatetime(registerdDataTime);
int count = negiMstMapper.count(negiMst);
if (count == 0) {
negiMstMapper.insert(negiMst);
} else {
errorMessages.add(negiName + "は登録済みです!");
}
} catch (Exception e) {
errorMessages.add("ねぎ登録時にエラーが発生しました。");
}
return errorMessages;
}
/**
* ねぎ情報を更新する
*
* @param negiMst 更新するねぎ情報
* @return エラーメッセージ
*/
public List<String> updateNegiMst(NegiMst negiMst) {
List<String> errorMessages = new ArrayList<>();
try {
NegiMst targetNegiMst = negiMstMapper.selectByPrimaryKey(negiMst.getId());
int count = negiMstMapper.count(negiMst);
if (targetNegiMst.getNegiName().equals(negiMst.getNegiName())) {
errorMessages.add(negiMst.getNegiName() + "の名前が修正されていません!");
} else if (count != 0) {
errorMessages.add("修正後のねぎ名称「" + negiMst.getNegiName() + "」が重複しています!");
} else {
targetNegiMst.setNegiName(negiMst.getNegiName());
negiMstMapper.updateByPrimaryKey(targetNegiMst);
}
} catch (Exception e) {
errorMessages.add("ねぎ修正時にエラーが発生しました。");
}
return errorMessages;
}
/**
* ねぎ情報を削除する
*
* @param negiMst 削除するねぎ情報
* @return エラーメッセージ
*/
public List<String> deleteNegiMst(NegiMst negiMst) {
List<String> errorMessages = new ArrayList<>();
try {
negiMstMapper.deleteByPrimaryKey(negiMst.getId());
} catch (Exception e) {
errorMessages.add("ねぎ削除時にエラーが発生しました。");
}
return errorMessages;
}
}
Controllerクラスの修正
Controllerクラスを修正して、Serviceクラスの処理を呼べるようにします。また、メイン画面で必要なページ情報を取る処理も追加します。
Controllerクラス
(略)
+@Autowired
+private NegitterService negitterService;
+private final int pageSize = 10;
(略)
@GetMapping("/")
public String index(@RequestParam(name = "page", required = false, defaultValue = "1") int pageNum, Model model) {
- // TODO つぶやきとページ情報を取得
+ PageInfo pageInfo = this.getPageInfo(pageNum);
model.addAttribute("negiits", new ArrayList<MessageTbl>());
model.addAttribute("pageNum", pageNum);
model.addAttribute("totalPages", 1);
model.addAttribute("prevPage", 1);
model.addAttribute("nextPage", 1);
model.addAttribute("errorMessages", new ArrayList<String>());
return "negitter";
}
(略)
@GetMapping("/register")
public String register(Model model) {
- // TODO ねぎマスタのデータを取得
+ List<NegiMst> negiList = negitterService.findAllNegi();
model.addAttribute("negiList", new ArrayList<NegiMst>());
model.addAttribute("errorMessages", new ArrayList<String>());
return "register";
}
(略)
@GetMapping("/negitter")
public String negitter(@RequestParam(name = "page", required = false, defaultValue = "1") int pageNum, Model model) {
- // TODO つぶやきとページ情報を取得
+ PageInfo pageInfo = this.getPageInfo(pageNum);
- model.addAttribute("negiits", new ArrayList<MessageTbl>());
+ model.addAttribute("negiits", pageInfo.getNegiits());
model.addAttribute("pageNum", pageNum);
- model.addAttribute("totalPages", 1);
- model.addAttribute("prevPage", 1);
- model.addAttribute("nextPage", 1);
+ model.addAttribute("totalPages", pageInfo.getTotalPage());
+ model.addAttribute("prevPage", pageInfo.getPrevPage());
+ model.addAttribute("nextPage", pageInfo.getNextPage());
model.addAttribute("errorMessages", errorMessages);
return "negitter";
}
(略)
@PostMapping("/negiit")
public String negiit(@ModelAttribute @Valid NegiitForm negiitForm, BindingResult bindingResult, Model model) {
+ int pageNum = negiitForm.getPageNum();
+ String name = negiitForm.getName();
+ String negiit = negiitForm.getNegiit();
List<String> errorMessages = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).toList();
- // TODO つぶやき登録処理結果によってページ情報を設定
+ if(errorMessages.isEmpty()){
+ errorMessages = negitterService.registerNegiit(name, negiit);
+ }
+ PageInfo pageInfo = new PageInfo();
+ if (errorMessages.isEmpty()) {
+ // エラーメッセージが無ければ、1ページ目に遷移する。
+ pageInfo = this.getPageInfo(1);
+ } else {
+ // エラーメッセージがあれば、元のページに遷移する。
+ pageInfo = this.getPageInfo(pageNum);
+ }
- model.addAttribute("negiits", new ArrayList<MessageTbl>());
+ model.addAttribute("negiits", pageInfo.getNegiits());
model.addAttribute("pageNum", pageNum);
- model.addAttribute("totalPages", 1);
- model.addAttribute("prevPage", 1);
- model.addAttribute("nextPage", 1);
+ model.addAttribute("totalPages", pageInfo.getTotalPage());
+ model.addAttribute("prevPage", pageInfo.getPrevPage());
+ model.addAttribute("nextPage", pageInfo.getNextPage());
model.addAttribute("errorMessages", errorMessages);
return "negitter";
}
(略)
@PostMapping("/registerNegi")
public String registerNegi(@ModelAttribute @Valid RegisterNegiForm negiForm, BindingResult bindingResult, Model model) {
+ String negiName = negiForm.getNegiName();
List<String> errorMessages = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).toList();
- // TODO ねぎ情報登録結果を設定
+ if (errorMessages.isEmpty()) {
+ errorMessages = negitterService.registerNegiMst(negiName);
+ }
- // TODO ねぎ情報を検索し直す
+ List<NegiMst> negiList = negitterService.findAllNegi();
model.addAttribute("negiList", negiList);
model.addAttribute("errorMessages", errorMessages);
return "register";
}
(略)
@PostMapping("/updateNegi")
public String updateNegi(@ModelAttribute @Valid UpdateNegiForm updateNegiForm, BindingResult bindingResult, Model model) {
+ Integer id = updateNegiForm.getId();
+ String process = updateNegiForm.getProcess();
+ String negiName = updateNegiForm.getNegiName();
+ NegiMst updateNegiMst = new NegiMst();
+ updateNegiMst.setId(id);
+ updateNegiMst.setNegiName(negiName);
List<String> errorMessages = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).toList();
- // TODO ねぎ情報更新結果を設定
+ if (errorMessages.isEmpty()) {
+ switch (process) {
+ case "UPDATE":
+ errorMessages = negitterService.updateNegiMst(updateNegiMst);
+ break;
+ case "DELETE":
+ errorMessages = negitterService.deleteNegiMst(updateNegiMst);
+ break;
+ default:
+ errorMessages.add("処理区分が不正です!");
+ break;
+ }
+ }
- // TODO ねぎ情報を検索し直す
+ List<NegiMst> negiList = negitterService.findAllNegi();
model.addAttribute("negiList", negiList);
model.addAttribute("errorMessages", errorMessages);
return "register";
}
- // TODO ページ情報を管理するprivate methodを作る
+/**
+ * ページング情報を取得する
+ */
+private PageInfo getPageInfo(int pageNum) {
+ PageInfo pageInfo = new PageInfo();
+ List<MessageTbl> negiits = negitterService.findAllNegiits(pageNum, pageSize);
+ int totalCount = negitterService.countNegiits();
+ int totalPages = totalCount == 0 ? 1 : (int) Math.ceil((double) totalCount / pageSize);
+ int prevPage = pageNum > 1 ? pageNum - 1 : 1;
+ int nextPage = pageNum < totalPages ? pageNum + 1 : totalPages;
+ pageInfo.setNegiits(negiits);
+ pageInfo.setTotalPage(totalPages);
+ pageInfo.setPrevPage(prevPage);
+ pageInfo.setNextPage(nextPage);
+ return pageInfo;
+}
+
+/**
+ * ページング情報を保持する
+ */
+@Data
+private class PageInfo {
+ private List<MessageTbl> negiits;
+ private int totalPage;
+ private int prevPage;
+ private int nextPage;
+}
(略)
見ていただけると分かると思いますが、この実装では登録・更新系の処理で発生したエラーのみサポートしています。検索系の処理でエラーが発生した場合の処理は、読者様の方で考えてみてください。
動作確認
Controllerクラス、Serviceクラス、Mapperクラスの実装が終わりましたので、早速動かしてみましょう。
メイン画面
まだ何もつぶやいていないので、空っぽの状態で表示されています。
つぶやく
つぶやいてみましょう。
つぶやくことができました。
この調子でどんどんつぶやいて、11個以上つぶやくとどうなるでしょうか。
ページングの確認
1つ目のつぶやきが見えなくなりました。「次のページへ」を押下してみましょう。
1つ目のつぶやきが2ページ目に表示されました。この状態で「前のページへ」を押下してみましょう。
1ページ目に戻って来れました。「ねぎを登録」を押下して、ねぎ登録画面に移動してみます。
ねぎ登録画面
こちらも、まだ何もねぎを登録していないので空っぽの状態で表示されています。
ねぎを登録する
登録してみます。
登録できました。この調子でいくつか登録してみましょう。
登録できました。
ねぎを修正する
「新里ネギ」を「下仁田ねぎ」に修正してみます。
修正できました。
ねぎを削除する
今度は、これを削除してみます。
削除できました。
「登録します。よろしいですか?」のようなダイアログが出ないので、いきなり登録も修正もできてしまっています。Submitする前にダイアログを出してユーザに確認を取る、といった処理を追加してみても良いでしょう。
まとめ
今回は、画面側にもサーバーとデータをやり取りできる仕組みを追加し、さらにロジック本体も組み込んでつぶやきやねぎ情報をDBから取得できるように仕上げていきました。
あれ?何か忘れているような…。
スッカリ忘れていました。。。
次回はこのロジックを入れて、今度こそ完成、としたいと思います。
それではまた。