学習23日目:アプリを作ってMVCやRESTを少し理解。

今日の学習時間。

  • Day:23
  • Today:13h
  • Total:142h

学習内容について。

昨日はサーブレットを作成する前までの準備を終えた。

  • 今回はRESTの概念を意識したサーブレットの作成を行なっていく
    • index
    • new
    • create
    • show
    • edit
    • update
    • destroy

まずは一覧表示のindexを作成。

  • IndexServletをコントローラとする
    • データベースから複数のメッセージ情報を取得して一覧表示するサーブレット
  • find()メソッド(一件取得する)に近い形で複数件のデータを取得するメソッドは存在していない
    • なのでJPQLと呼ばれる少し特殊なSQL文(SELECT文)をMessageクラスに用意する必要がある(下記)
  • @NamedQuery アノテーションを使い、16行目のSELECT文にgetAllMessagesという名前をつけたのが上記の記述の内容
  • SELECT mは通常のSQLでいうところのSELECT *と同じ
    • JPQLの特殊な点
  • JPQLの文につけた名前getAllMessagescreateNamedQueryメソッドの引数に指定
    • データベースへの問い合わせが実行できるように
  • その問い合わせ結果をgetResultList()メソッドを使ってリスト形式で取得
    • データベースに保存されたデータはHibernateによって自動でMessageクラスのオブジェクトになってこのリストの中に格納される
http://localhost:8090/message_board/index

0
  • Tomcatを再起動して、上記のURLにアクセス
    • データの登録件数である0が表示される
    • IndexServletの40行目が実行された結果
  • 上記にアクセスした時点でMySQLのmessage_boardデータベースにmessagesテーブルが追加される(下記)
    • eclipseのコンソールで確認できる
    • persistence.xmlのスキーマ生成の設定でDatabase actionを「作成」に設定したから
Hibernate: 
    
    create table messages (
       id integer not null auto_increment,
        content varchar(255) not null,
        created_at datetime not null,
        title varchar(255) not null,
        updated_at datetime not null,
        primary key (id)
    ) engine=MyISAM

MySQLで一応確認(下記)。

mysql> use message_board;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+-------------------------+
| Tables_in_message_board |
+-------------------------+
| messages                |
+-------------------------+
1 row in set (0.00 sec)

mysql> select * from messages;
Empty set (0.00 sec)
データベース関連のファイルをGitにコミット
$ git add .
$ git commit -m "Add getAllMessages"

文字化け防止のため、EncodingFilterを追加。

  • filtersパッケージの中にEncodingFilterというクラス名で新規フィルタを作成
  • フィルター・マッピングは/*
    • すべてのサーブレットに適応

新規登録のnewを作成。

  • controllersパッケージにNewServletというクラスを追加
  • URLマッピング:/NewServlet/newに変更
  • doPostのチェックのみ外す
  1. 40行目でMessageのインスタンスを生成
  2. 自動採番されるID以外のプロパティに値をセット
  3. em.persist(m);でデータベースに保存
  4. 55行目はデータの新規登録を確定させる命令
  • Tomcatを再起動し http://localhost:8080/message_board/new にアクセス
  • 画面に「1」と表示される
    • データベースへ1件登録された際にauto_incrementで自動採番されたid列の値
  • eclipseのコンソール画面には、下記のようなSQL文が表示される
    • このINSERT文が実行されたことを表す
Hibernate: 
    insert 
    into
        messages
        (content, created_at, title, updated_at) 
    values
        (?, ?, ?, ?)

valuesのところが?となっているがINSERT文が実行される際にtarohelloが動的に当てはめられているので問題ないらしい。心配なので、MySQLで確認(下記)。

mysql> select * from messages;
+----+---------+---------------------+-------+---------------------+
| id | content | created_at          | title | updated_at          |
+----+---------+---------------------+-------+---------------------+
|  1 | hello   | 2020-08-02 08:23:14 | taro  | 2020-08-02 08:23:14 |
+----+---------+---------------------+-------+---------------------+
1 row in set (0.01 sec)

ちゃんと登録されていた。

ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add NewServlet.java"

indexとnewのビューを作成。

ここまで
  • とりあえずデータベースへのアクセスを試しただけ
  • RESTの概念に従う
    • indexはデータの一覧を表示する画面
    • new は新規登録用のフォームを表示する画面
  • まだ、MVCの「ビュー」を作成していない
    • 次はサーブレットのコードを調整しつつ、ビューになるJSPを用意
    • indexnewを完成させる
  • 画面が必要なファイル
    • index
    • new
    • show
    • edit
  • 共通のひな形の役割をもつ「レイアウトファイル」を作成すると修正が楽

レイアウトファイルを作成

WebContentWEB-INFの中にviews、さらにその中にlayoutフォルダを作り、その中に app.jspを作成。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>メッセージボード</title>
    </head>
    <body>
        <div id="wrapper">
            <div id="header">
                <h1>メッセージボード アプリケーション</h1>
            </div>
            <div id="content">
                ${param.content}
            </div>
            <div id="footer">
                by Keiten Kiki.
            </div>
        </div>
    </body>
</html>

${param.content}に各ページのビューの内容が入る。

indexのビューを作成。

データベースから取得したメッセージ一覧(messages)をリクエストスコープにセットし、IndexServletdoGetから、ビューとして/index.jspを呼び出す。

続いてindex.jspを作成(下記)。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:import url="../layout/app.jsp">
    <c:param name="content">
        <h2>メッセージ一覧</h2>
        <ul>
            <c:forEach var="message" items="${messages}">
                <li>
                    <a href="${pageContext.request.contextPath}/show?id=${message.id}">
                        <c:out value="${message.id}" />
                    </a>
                    :<c:out value="${message.title}"></c:out> > <c:out value="${message.content}" />
                </li>
            </c:forEach>
        </ul>

        <p><a href="${pageContext.request.contextPath}/new">新規メッセージの投稿</a></p>

    </c:param>
</c:import>
  • <c:import>url属性に指定したファイルの内容をその位置で読み込むことができる
  • 4行目のタグの中の記述内容がapp.jsp${param.content}に当てはまる
  • 17行目のように書くことで、その部分が自動的に/message_boardというコンテキストパスの文字列に置き換わる
    • コンテキストパスの設定を変更してもJSPファイルに修正が必要なくなるので有効な書き方
    • <c:url> タグを使ってURLの指定を行っても自動で/message_boardのコンテキストパスの文字列が挿入される(下記例)
<a href="${pageContext.request.contextPath}/new">新規メッセージの投稿</a>
// 上下は同様の内容
<a href="<c:url value='/new' />">新規メッセージの投稿</a>
動作確認
  • http://localhost:8080/message_board/index にアクセス
  • 画面に「hello」が表示されていればOK

newのビューを作成。

  • 35行目はCSRF対策
    • フォームからhidden要素で送られた値とセッションに格納された値が同一であれば送信を受け付ける
      • サイト外からPOST送信された投稿を拒否できる
      • セッションIDを利用
  • 次にビューを作成
  • ここで使うフォームはeditでも利用したい
    • レイアウトファイルと同じように共通ファイル化
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<label for="title">タイトル</label><br />
<input type="text" name="title" value="${message.title}" />
<br /><br />

<label for="content">メッセージ</label><br />
<input type="text" name="content" value="${message.content}" />
<br /><br />

<input type="hidden" name="_token" value="${_token}" />
<button type="submit">投稿</button>
  • タイトルとメッセージのテキストボックス:value="${message.title}"
    • リクエストスコープのmessageオブジェクトからデータを参照して、入力内容の初期値として表示
    • このあと作成するeditや、入力値エラーがあってフォームのページを再度表示する際に役立つ
    • リクエストスコープにmessageが入っていなければエラーが表示されるので注意
    • NewServletの38行目を記述したのは、画面表示時のエラー回避のため、とりあえず「文字数0のデータ」をフォームに渡すため
  • _form.jspがフォームの共通レイアウト
    • 新たにnew.jspを作成して、_form.jspを取り込むように記述(下記)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:import url="../layout/app.jsp">
    <c:param name="content">
        <h2>メッセージ新規作成ページ</h2>

        <form method="POST" action="${pageContext.request.contextPath}/create">
            <c:import url="_form.jsp" />
        </form>

        <p><a href="${pageContext.request.contextPath}/index">一覧に戻る</a></p>

    </c:param>
</c:import>
ここまでの状態をGitにコミット
$ git add .
$ git commit -m "Modify Index/NewServlet"

挿入処理のcreateを作成。

  • CreateServletを作成
    • パッケージ名:controllers
    • クラス名:CreateServlet
    • URLマッピング:/CreateServlet/createに変更
    • doGetのチェックのみ外す
  • 37行目でCSRF対策のチェック
    • _tokenに値がセットされていないときやセッションIDと値が異なるときはデータの登録ができない仕様に
    • 意図しない不正なページ遷移によって/createへアクセスされた場合に、ここのチェックがtrueにならない
  • idはMySQLのauto_incrementの採番に任せる
  • titlecontentはフォームから入力された内容をセット
  • created_atupdated_atは、48行目のような記述をすることで現在日時の情報を持つ日付型のオブジェクトを取得できる
    • Javaでは日時情報もオブジェクトで管理
    • そのオブジェクトを2つのカラムにセット
    • 必要な情報をセットしたMessageクラスのオブジェクトをpersistメソッドを使ってデータベースにセーブ
      • commitを忘れない
  • データベースへの保存が完了したら、indexページへリダイレクト
ここまでの状態をGitにコミット
$ git add .
$ git commit -m "Add CreateServlet.java"

詳細画面のshowを作成。

indexページのID番号に貼ったリンクをクリックすると、該当のメッセージの詳細情報を表示するページを作成していく。

  • コントローラ(サーブレット)のShowServletを作成
    • URLマッピングは/ShowServletから/showへ変更
    • メソッドはdoGetを残す
  • em.find()メソッドを利用
    • 1件のみデータを取得できれば良いから
    • IDはURLに追記されているidから取得
  • /show?id=1にアクセスすると、idが1のメッセージ情報を表示する必要がある
    • クエリ・パラメータのidrequest.getParameter("id")で取得できる
      • しかしrequest.getParameter()はどのようなデータもString型のデータとして取得するので注意
    • 今回のデータベースのidは整数値である
    • Interger.parseInt()メソッドを利用してString型の「1」を整数値の1に変えてからfindメソッドの引数にした

次にビューとなるshow.jspを作成。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<c:import url="../layout/app.jsp">
    <c:param name="content">

        <h2>id : ${message.id} のメッセージ詳細ページ</h2>

        <p>タイトル:<c:out value="${message.title}" /></p>
        <p>メッセージ:<c:out value="${message.content}" /></p>
        <p>作成日時:<fmt:formatDate value="${message.created_at}" pattern="yyyy-MM-dd HH:mm:ss" /></p>
        <p>更新日時:<fmt:formatDate value="${message.updated_at}" pattern="yyyy-MM-dd HH:mm:ss" /></p>

        <p><a href="${pageContext.request.contextPath}/index">一覧に戻る</a></p>

    </c:param>
</c:import>
  • 日情報は基本的にオブジェクトの形をしており、単純な文字列ではない
    • <fmt:formatDate>タグで作成日時や更新日時をpattern属性で指定した年-月-日 時:分:秒の形式で表示
    • Tag formatDateを参照した
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add ShowServlet"

編集画面のeditを作成。

  • メッセージを修正する機能
    •  edit(編集画面)とupdate(更新処理)のサーブレットが必要

show.jspeditへのリンクを追加した。

  • EditServletを作成
    • URLマッピングは/EditServletから/editへ変更
    • メソッドはdoGetのみ残す
  • リクエストスコープにメッセージのIDを入れた場合
    • このあと作成する/updateへデータを送信する際に<input type="hidden">を使ってメッセージIDの情報をフォームに追加する必要がある
  • しかし今回はセッションスコープへメッセージのIDの情報を保存して、/updateへ渡す仕様に

次にviewになるedit.jspを作成。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:import url="../layout/app.jsp">
    <c:param name="content">
        <h2>id : ${message.id} のメッセージ編集ページ</h2>

        <form method="POST" action="${pageContext.request.contextPath}/update">
            <c:import url="_form.jsp" />
        </form>

        <p><a href="${pageContext.request.contextPath}/index">一覧に戻る</a></p>

    </c:param>
</c:import>
  • データベースに保存されていたメッセージやタイトルが初期値としてテキストボックスに格納される
    • _form.jspの3行目のような記述にある、value="${message.title}"の効果
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add EditServlet"

更新処理のupdateを作成。

  • 更新処理のUpdateServletを作成
    • URLマッピングは/UpdateServletから/updateへ変更
    • メソッドはdoPostのみ残す
  • idEditServletでセッションスコープに保存したmessage_idのデータを使用
    • 42行目でセッションスコープから取得したデータは汎用的なObject型になっているので、(Integer)でキャスト
  • あとの流れはCreateServletと同じ
    • 更新処理なのでcreated_atは変更しない
    • em.persist(m);は不要
      • データベースから取得したデータに変更をかけてコミットすれば変更が反映されるから
  • 更新が完了した時点でセッションスコープ上のデータは不要になる
    • 60行目で不要なデータを削除
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add UpdateServlet.java"

削除処理のdestroyを作成。

まず、edit.jspに削除機能のリンクを貼る。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:import url="../layout/app.jsp">
    <c:param name="content">
        <h2>id : ${message.id} のメッセージ編集ページ</h2>

        <form method="POST" action="${pageContext.request.contextPath}/update">
            <c:import url="_form.jsp" />
        </form>

        <p><a href="${pageContext.request.contextPath}/index">一覧に戻る</a></p>
        <p><a href="#" onclick="confirmDestroy();">このメッセージを削除する</a></p>
        <form method="POST" action="${pageContext.request.contextPath}/destroy">
            <input type="hidden" name="_token" value="${_token}" />
        </form>
        <script>
        function confirmDestroy() {
            if(confirm("本当に削除してよろしいですか?")) {
                document.forms[1].submit();
            }
        }
        </script>

    </c:param>
</c:import>
  • DestroyServletクラスを作成
    • URLマッピングは/DestroyServletから/destroyへ変更
    • メソッドはdoPostのみ残す
  • 基本的な流れはUpdateServletと同様
  • データベースからデータを削除する際
    • em.findで取得したオブジェクトを引数に入れてem.remove();を実行
    • さらにem.getTransaction().commit();でコミットする必要あり
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add DestroyServlet.java"

データが無かった場合に表示内容を変える。

show.jspedit.jspについて、該当するIDのメッセージデータが無かった場合に「お探しのデータは見つかりませんでした。」と表示させるように変更する。

  • 条件分岐には下記のタグを使用
    • <c:choose>
    • <c:when>
    • <c:otherwise>
  • また、EditServlet内の48行目のせいで、該当するIDのメッセージデータがない場合にNullPointerExceptionが出てしまう
    • なので例外を回避するための修正が必要(下記49行目)
動作確認
  1. Tomcatを再起動
  2. クエリ・パラメータとなるid=??を「存在していないメッセージIDの数値」にしてアクセス
  3. 「お探しのデータは見つかりませんでした」と表示されればOK
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Modify null control"

画面を装飾。

  • CSSを追加する
    • app.jsp<head>内に下記の<link>タグ2行(8、9行目)を追加
    • <c:url>を利用しているので、2行目のコードを追加
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>メッセージボード</title>
        <link rel="stylesheet" href="<c:url value='/css/reset.css' />">
        <link rel="stylesheet" href="<c:url value='/css/style.css' />">
    </head>
    <body>
        <div id="wrapper">
            <div id="header">
                <h1>メッセージボード アプリケーション</h1>
            </div>
            <div id="content">
                ${param.content}
            </div>
            <div id="footer">
                by Keiten Kiki.
            </div>
        </div>
    </body>
</html>
  • WebContentフォルダ直下css2つのCSSファイルを追加
    • reset.css
      • CSSの調整をしやすくするためのもの
    • style.css
      • メッセージボード独自のCSS指定をするためのもの

show.jspでのタイトルとメッセージ、作成日時および変更日時の表示をテーブルでの表示に変更する(下記)。

ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add stylesheets"

フラッシュメッセージを出す。

  • フラッシュメッセージ
    • 「登録が完了しました」のような文言
    • その文言を表示する場所を、リダイレクト先のindexにする
      • 下記の<c:if>タグを追記(5 – 9行目)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:import url="../layout/app.jsp">
    <c:param name="content">
        <c:if test="${flush != null}">
            <div id="flush_success">
                <c:out value="${flush}"></c:out>
            </div>
        </c:if>
        <h2>メッセージ一覧</h2>
        <ul>
            <c:forEach var="message" items="${messages}">
                <li>
                    <a href="${pageContext.request.contextPath}/show?id=${message.id}">
                        <c:out value="${message.id}" />
                    </a>
                    :<c:out value="${message.title}"></c:out> > <c:out value="${message.content}" />
                </li>
            </c:forEach>
        </ul>

        <p><a href="${pageContext.request.contextPath}/new">新規メッセージの投稿</a></p>

    </c:param>
</c:import>
  • /create/update/destroyの各サーブレットで、データベースに対する処理が完了したときにフラッシュメッセージをセット
  • しかし、そこから/indexへリダイレクトし、さらにindex.jspを呼び出すという複数の遷移が発生するため、リクエストスコープにフラッシュメッセージをセットすると途中で削除されてしまう
  • 解決策としてフラッシュメッセージをセッションスコープに保存し、index.jspを呼び出したときにセッションスコープから取り出して表示する
  • 3つのサーブレットそれぞれに命令を追加(下記)
  • 問題点として、セッションスコープに入れっぱなしだと、indexにアクセスするたび表示されてしまう
  • なのでIndexServletでセッションスコープからリクエストスコープに移し替え、そのあとセッションスコープから除去する(下記)

追記した部分をcssで装飾(下記)

#flush_success {
    width: 100%;
    padding-top: 28px;
    padding-left: 2%;
    padding-bottom: 28px;
    margin-bottom: 15px;
    color: #155724;
    background-color: #d4edda;
}
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add flush message"

ページネーションをつける

  • ページネーション
    • 1ページの件数を指定して、「次のページへ進む」や「前のページへ戻る」リンクのある機能
  • 最初に作成したgetAllMessagesのJPQLはそのまま利用できる
  • あとはデータベースにメッセージのデータが何件入っているかを知るためのJPQLが必要(下記)

次にIndexServletを変更(下記)。

  • page=2とあってもrequest.getParameter()(42行目)で取得できるのはString型の"2"
    • なのでInteger.parseInt()で文字列から数値に変更
  • pageのパラメータが無かったりpage=aのように数値ではないものが指定される可能性もある
    • trycatchで囲って処理が止まらないようした
    • 本来ならしっかりした形でパラメータが数値かどうか調べるべきではある
  • 48行目は何件目からデータを取得するかを設定(0から始まる点に注意)
  • 49行目はデータの最大取得件数を設定
  • getAllMessagesは複数のデータが結果として戻ってくる可能性がある
    • getResultList()で問い合わせ結果を取得
    • getMessagesCountは全件数という1つの結果のみが戻ってくるので不十分
    • なので最後にgetSingleResult()(1件だけ取得する)という命令を指定
  • 最後にメッセージデータのリストの他、全件数の数値と表示するページ数の数値もリクエストスコープにセット
    • index.jspに送信
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add pagination"

バリデーションを追加。

modelsパッケージの中にvalidatorsパッケージを新規で追加し、その中に MessageValidatorという名前の通常クラスを作成する。

  • 上記を利用し、createupdateのそれぞれにバリデーションを実装する(下記)
    • タイトルやメッセージが空欄だった場合に処理を受け付けず、フォーム画面に表示を戻す処理を追加
  • フォーム画面が表示された際にエラー内容を表示するように変更する(下記)
    • JSTLタグを利用するので2行目の記述を忘れない
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:if test="${errors != null}">
    <div id="flush_error">
        入力内容にエラーがあります。<br />
        <c:forEach var="error" items="${errors}">
            ・<c:out value="${error}" /><br />
        </c:forEach>

    </div>
</c:if>
<label for="title">タイトル</label><br />
<input type="text" name="title" value="${message.title}" />
<br /><br />

<label for="content">メッセージ</label><br />
<input type="text" name="content" value="${message.content}" />
<br /><br />

<input type="hidden" name="_token" value="${_token}" />
<button type="submit">投稿</button>

装飾としてstyle.cssに下記を追加。

#flush_error {
    width: 100%;
    padding-top: 28px;
    padding-left: 2%;
    padding-bottom: 28px;
    margin-bottom: 15px;
    color: #721c24;
    background-color: #f8d7da;
}
ここまでの内容をGitにコミット
$ git add .
$ git commit -m "Add validation"

これでとりあえず終了。。

今日の反省と明日の目標。

始めにTomcatの調子が悪いのか、404エラーを連発していたけど、ただ単にビューのファイルをWEB-INFではなくMETA-INFに入れていたことが原因でした。もっと別な深い原因があるのかと思って色々調べたのにな。。灯台下暗し。

あと、なぜかTomcatを再起動すると「情報: 不正なアクセス: このWebアプリケーションのインスタンスは既に停止されています」という文字列がコンソールに出力されます。一応サーバーは稼働してるけど、なんだか気持ち悪いので早急に原因を突き止めたいところです。

今日は超集中して一気に簡易的なアプリを作成してみました。MVCモデルとかRESTの概念を理解するのが目標だったので、それ自体はなんとなく分かった感じがします。しかし、このままでは腱鞘炎になりそうな勢い。。

明日はタスク管理アプリケーションを作っていこうかと思います。今日はGitにコミットするのを忘れて先に進めたりしたので1つずつ丁寧にバージョン管理していくのが目標です。

閉じる