#author("2021-06-25T03:50:46+00:00","","") #author("2021-06-25T03:53:17+00:00","","") [[第2回/ToDo管理]] * ToDo管理アプリケーションの管理者機能の実装 [#z226a8bb] ** 目次 [#xc9fec71] #contents ** UI紙芝居 [#r144bfaf] &attachref(admin_screen.png); ** 実装 [#jae00adf] 管理者のユースケースでは,メンバーの登録(UC02),取得(UC01),削除(UC03)を行う. よって,下記の順番で実装していく - A1: メンバーのエンティティを定義 - A2: メンバーをCRUDするレポジトリを作成 - A3: メンバーの登録フォームを作成 - A4: サービスの例外を作成 - A5: メンバーを登録,取得,削除するサービスを作成 -- 存在チェックや重複チェックも行う - A6: サービスを呼び出すコントローラを作成 -- リクエストマッピング -- 画面生成・遷移 - A7: 画面用のHTMLテンプレートを作成する *** A1: Entity - エンティティ [#r968568e] - entity/Member.java package jp.kobespiral.manda.todo.entity;//適宜変えること import javax.persistence.Entity; import javax.persistence.Id; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @Entity public class Member { @Id String mid; //メンバーID String name; //氏名 } - &color(red){適宜,import文を補うこと}; - 今回管理するメンバ情報は,IDと氏名のみ - @Idは,midをエンティティの''識別子(ID)'' にするという宣言 -- 識別子とはそのエンティティを一意に識別できる文字列や数字のこと(学籍番号,伝票番号など) *** A2: Repository - レポジトリ [#ke3147c2] - repository/MemberRepository.java package jp.kobespiral.manda.todo.repository;//変える import java.util.List; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import jp.kobespiral.manda.todo.entity.Member;//変える @Repository public interface MemberRepository extends CrudRepository<Member, String>{ List<Member> findAll(); } - Repositoryはinterfaceであることに注意 -- Spring Data JPAのCrudRepositoryを継承する -- ジェネリクスは<管理するエンティティクラス, そのクラスのIDの型> で指定すること - MemberのCRUDを行う一通りのメソッドが自動実装される [[SpringBoot/JPA]] Optional<Member> findById(String mid); //null安全な1件取得 (R) Iterable<Member> findAll(); //全メンバー取得 (R) Member save(Member member); //メンバー保存・更新 (C, U) void deleteById(String mid); //メンバー削除 (D) void deleteAll(); //メンバー全権削除 (D) boolean existById(String mid); //存在チェック Long count(); //登録件数 - カスタムメソッド List<Member> findAll(); を陽に定義している -- 定義しなければサービス層でIterableからListに入れなおさなければならず面倒 *** A3: Form - 登録フォーム [#y92e415d] - dto/MemberForm.java package jp.kobespiral.manda.todo.dto; import jp.kobespiral.manda.todo.entity.Member; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data public class MemberForm { String mid; //メンバーID. String name; //名前 public Member toEntity() { Member m = new Member(mid, name); return m; } } - ブラウザからのリクエストを受けるために必要 -- エンティティと同じ属性であっても,違うクラスとして区別した方が良い *** A4: Exception - アプリケーション例外 [#lcbabf9a] - exception/ToDoAppException.java package jp.kobespiral.manda.todo.exception; public class ToDoAppException extends RuntimeException{ public static final int NO_SUCH_MEMBER_EXISTS = 101; public static final int MEMBER_ALREADY_EXISTS = 102; public static final int INVALID_MEMBER_INFO = 103; public static final int INVALID_MEMBER_OPERATION = 104; public static final int NO_SUCH_TODO_EXISTS = 201; public static final int INVALID_TODO_INFO = 202; public static final int INVALID_TODO_OPERATION = 203; int code; public ToDoAppException(int code, String message) { super(message); this.code = code; } public ToDoAppException(int code, String message, Exception cause) { super(message, cause); this.code = code; } public int getCode() { return code; } } - 非検査例外(RuntimeException)として定義 -- 内部にエラーコードを含んでエラーの種類を区別している -- 今回は投げるだけで拾わない.→エラーハンドリングは次回 *** A5: Service - サービス [#w3e6631a] - service/MemberService.java package jp.kobespiral.manda.todo.service; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import jp.kobespiral.manda.todo.dto.MemberForm; import jp.kobespiral.manda.todo.entity.Member; import jp.kobespiral.manda.todo.exception.ToDoAppException; import jp.kobespiral.manda.todo.repository.MemberRepository; @Service public class MemberService { @Autowired MemberRepository mRepo; /** * メンバーを作成する (C) * @param form * @return */ public Member createMember(MemberForm form) { //IDの重複チェック String mid = form.getMid(); if (mRepo.existsById(mid)) { throw new ToDoAppException(ToDoAppException.MEMBER_ALREADY_EXISTS, mid + ": Member already exists"); } Member m = form.toEntity(); return mRepo.save(m); } /** * メンバーを取得する (R) * @param mid * @return */ public Member getMember(String mid) { Member m = mRepo.findById(mid).orElseThrow( () -> new ToDoAppException(ToDoAppException.NO_SUCH_MEMBER_EXISTS, mid + ": No such member exists")); return m; } /** * 全メンバーを取得する (R) * @return */ public List<Member> getAllMembers() { return mRepo.findAll(); } /** * メンバーを削除する (D) */ public void deleteMember(String mid) { Member m = getMember(mid); mRepo.delete(m); } } - 管理者ユースケースに必要なメソッドを実装 -- 基本的にはリポジトリのメソッドをラップしているだけ - ビジネスルールのチェック -- 作成時(C)に重複チェック → 重複があれば例外スロー -- 取得時(R)に存在チェック → 存在しなければ例外スロー -- 削除時(D)はgetMember()を介して存在チェック *** A6: コントローラ - Controller [#yb9a345e] - controller/MemberController.java package jp.kobespiral.manda.todo.controller; import java.util.List; 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.RequestMapping; import jp.kobespiral.manda.todo.dto.MemberForm; import jp.kobespiral.manda.todo.entity.Member; import jp.kobespiral.manda.todo.service.MemberService; @Controller @RequestMapping("/admin") public class MemberController { @Autowired MemberService mService; /** * 管理者用・ユーザ登録ページ HTTP-GET /admin/register * @param model * @return */ @GetMapping("/register") String showUserForm(Model model) { List<Member> members = mService.getAllMembers(); model.addAttribute("members", members); MemberForm form = new MemberForm(); model.addAttribute("MemberForm", form); return "register"; } /** * 管理者用・ユーザ登録確認ページを表示 HTTP-POST /admin/check * @param form * @param model * @return */ @PostMapping("/check") String checkUserForm(@ModelAttribute(name = "MemberForm") MemberForm form, Model model) { model.addAttribute("MemberForm", form); return "check"; } /** * 管理者用・ユーザ登録処理 -> 完了ページ HTTP-POST /admin/register * @param form * @param model * @return */ @PostMapping("/register") String createUser(@ModelAttribute(name = "MemberForm") MemberForm form, Model model) { Member m = mService.createMember(form); model.addAttribute("MemberForm", m); return "registered"; } /** * 管理者用・ユーザ削除処理 HTTP-GET /admin/delete/{mid} * @param mid * @param model * @return */ @GetMapping("/delete/{mid}") String deleteUser(@PathVariable String mid, Model model) { mService.deleteMember(mid); return showUserForm(model); } } *** A7: HTMLテンプレート [#gfddad61] - resources/templates/register.html (管理者・メンバー登録画面) <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>管理者画面</title> </head> <body> <h1>管理者画面</h1> <h2>メンバー新規登録</h2> <p> メンバーIDと氏名を入力して,「確認する」を押してください.</p> <ul> <li>メンバーIDには,アルファベット小文字,数字,ハイフン,アンダーバーのみ使用できます</li> </ul> <form role="form" th:action="@{/admin/check}" th:object="${MemberForm}" method="post"> <table> <tr> <td><label>メンバーID: </label></td> <td><input type="text" required th:field="*{mid}" /></td> </tr> <tr> <td><label>氏名: </label></td> <td><input type="text" required th:field="*{name}" /></td> </tr> </table> <p><input type="submit" value="確認する" /></p> </form> <h2>登録済みメンバー</h2> <table border="1"> <tr> <th>メンバーID</th> <th>氏名</th> <th>コマンド</th> </tr> <tr th:each="m: ${members}"> <td>[[${m.mid}]]</td> <td>[[${m.name}]]</td> <td><a th:href=@{/admin/delete/{m}(m=${m.mid})}>削除</a></td> </tr> </table> </body> </html> - resources/templates/check.html (管理者・メンバー登録確認画面) <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登録確認</title> </head> <body> <h1>登録確認</h1> <p> 以下の情報でメンバーを登録します.よろしければ登録を押してください. </p> <form role="form" th:action="@{/admin/register}" th:object="${MemberForm}" method="post"> <p> <label>メンバーID: </label> [[*{mid}]] </p> <p> <label>氏名: </label> [[*{name}]] </p> <p> <input type="button" value="戻る" onclick="history.back()" /> <input type="submit" value="登録する" /> </p> <input type="hidden" th:field="*{mid}" /> <input type="hidden" th:field="*{name}" /> </form> </body> </html> - resources/templates/registered.html (管理者・メンバー登録完了画面) <!DOCTYPE html> <html lang="ja" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登録完了</title> </head> <body> <h1>登録完了</h1> <p> メンバー登録を完了しました. </p> <div th:object="${MemberForm}"> <p> <label>メンバーID: </label> [[*{mid}]] </p> <p> <label>氏名: </label> [[*{name}]] </p> <p> <a th:href=@{/admin/register}>続けて登録する</a><br /> <a th:href=@{/}>初めに戻る</a> </p> </div> </body> </html> ** ここまでの機能を実行&テストする [#j86754c1] - VSCodeでTodoappApplication.javaを選択し,F5で実行 - ブラウザで http://localhost:18080/admin/register を開く - ユーザの追加が行われているかチェックしよう ** データベースにデータが永続化されているかチェックしよう [#rb4ef0cd] - C:\TAMP\apache-httpd を起動 - http://localhost/phpmyadmin - ユーザ:todouser パス: todotodo でログイン - データベース todoappのmemberテーブルを確認する