Actionの作り方 (HTMLスタイル)

実装の流れ

ちょっと前提

ここでは、docksidestage.org というドメイン、つまりパッケージは org.docksidestage を想定し、harbor というアプリ名であることを想定して説明していきます。

ハンズオンで Action を作ってみよう!

LastaFluteでの実装のやり方を学んでいる最中であれば、まずは、Exampleプロジェクト harbor で、実際にクラスを作りながらやっていってもよいでしょう。コードを真似て書きながら(コピーでもOK)、画面を作っていくと流れとコツがわかってくるかと思います。 (それができるようにドキュメントを作っています)

harborプロジェクトは、組み込みの H2 Database を使っているので、データベースのインストールは不要です。 ただし、clone した後、ReplaceSchema を叩くの忘れないように。(Quick Trial の欄を参考に)

まずはラフスケッチ実装

一行一行、しっかり書いていくのではなく、まずは流れを実装して、全体像を構築してから細かいところを固めていくというスタイルをオススメしています。 最初の一ターン目はラフスケッチです。わからないことがあったら保留して次に進み、全体像を把握できる状態になってからつまづいたところを解決していきましょう。

  1. URLを決める
  2. Actionクラスを作る
  3. Executeメソッドを準備
  4. Formを作る
  5. HTMLテンプレートを作る
  6. HTML Bean を作る
  7. Executeメソッドを実装
  8. ラフスケッチできた

URLを決める

まずは、URLを決めましょう。

ここでは /sea/land/3?pay=HAN (/sea/land/[商品ID]?pay=[支払方法]) というGETリクエストの対応する Action を作ることにしましょう。

/3 の部分はPathパラメーターで商品IDと想定し、ログイン会員(自分)とフォローしている会員の、指定された商品に対応する購入一覧 を表示するとします。もし、payが指定されていたら、指定された方法で支払された購入に絞ります。(maihamadb を参考に、よくわからなければ後で)

Actionクラスを作る

Actionの名前を決める

/3 はPathパラメーターとするならば、Actionを識別する部分が /sea/land/ となり、

  • A. SeaAction#land()
  • B. SeaLandAction#index()

の、どちらかとなります。(規約)

基本的には、一つの画面に付き一つのActionがオススメです。ですが、業務規模などの都合で微調整してもよいでしょう。 (とりあえず漠然と決めて、あとでやっぱりこっちだった、となればリファクタリングすればOKです。変更してもURLには影響しません)

ここでは、landという画面があると想定して、SeaLandAction#index() を作りましょう。

Actionのパッケージ(配置場所)を決める

SeaLandAction であれば...

  • A. ...app.web.SeaLandAction
  • B. ...app.web.sea.SeaLandAction
  • C. ...app.web.sea.land.SeaLandAction

のどれかになります。(規約)

web直下に置くのは、よほどのメジャー感のあるものなので、基本的には B, C となるでしょう。 seaの仲間がすごく多いことが想定される場合は C ですが、まずは B にしておいて、後から移動してもよいでしょう。 (あとでパッケージを移動しても、URLやプログラムに影響はありません)

ここでは、...app.web.sea に置くことにしましょう。

実際に作る

実際に org.docksidestage.app.web.sea.SeaLandAction にクラスを作ってみましょう。

e.g. Action class location @Package
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-sea
 |  |  |  |  |-SeaLandAction.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

Executeメソッドを準備

Baseクラスを継承

まずは、作成したActionにて、そのアプリのBaseクラスを継承しましょう。アプリ名が harbor であれば、 HarborBaseAction を継承します。(そのアプリのBaseActionが用意されているはずです)

e.g. Action class extends Base class @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {
}

Executeメソッドを定義

ここでは、HTMLスタイルの Execute メソッド index() を作ってみましょう。

Executeアノテーションを付けて戻り値は HtmlResponse, そして、Pathパラメーター /3 を受け取る引数を最初に定義して、GETパラメーターを受け取る Form を最後の引数に定義します。(規約)

e.g. Action class extends Base class @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {

    @Execute
    public HtmlResponse index(Integer productId, SeaLandForm form) {
    	// まだ、Formがない、returnがないのでコンパイルエラーです!
    	// この後、Formを作って戻り値を指定しますので、ちょっとそのままで。
    }
}

もし、Pathパラメーターの /3 が非必須の要素であれば、productId の型を OptionalThing<Integer> にします。省略されたときは empty になります。そもそも、PathパラメーターもFormパラメーターもないのであれば、引数なしでOKです。

Formを作る

Formクラスを定義

SeaLandForm は定義してみましたが、まだ存在しないのでコンパイルエラーです。

Formのクラス名は Form で終わる 必要があります(規約)。その前の名前は任意ですが、Actionとイメージの近い名前がオススメです。 ここでは、SeaLandForm という名前で作りましょう。

パッケージ(置き場所)は、Actionクラスと同じ (つまり、Actionの隣に置く) がオススメです。

e.g. Form class location @Directory
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-sea
 |  |  |  |  |-SeaLandAction.java
 |  |  |  |  |-SeaLandForm.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

Formの実装

publicフィールドで受け取るパラメーターのプロパティを定義します。フリー入力項目でなければ、Stringではなく、Integer や LocalDate や CDef (区分値) など、ネイティヴな型で宣言してOKです。 (変換できない値だったら、それはシステム上のミス、もしくは、いたずらということで 400 になる)

ここでは、pay=HANというGETパラメーターに対応して、payプロパティを定義しましょう。 商品ステータスの区分値なので、CDef 型で宣言します。

e.g. Form class @Java
/**
 * @author yourname
 */
public class SeaLandForm {

    public CDef.PaymentMethod pay;
}

もし、バリデーションを行うなら、Validatorアノテーションを付けます。

必須チェック
@Required (String, Integerなんでも使える)
文字列の最大長
@Length
数値の最大値
@Max
などなど
いろいろ

ここでは、payプロパティを必須にしてしまいましょう。(何も付けないと説明しづらいので...)

e.g. Form class @Java
/**
 * @author yourname
 */
public class SeaLandForm {

    @Required
    public CDef.PaymentMethod pay;
}

HTMLテンプレートを作る

Actionの実装の前に、HTMLテンプレートのファイルを作って、自動生成だけやっておきましょう。

ここでは、Thymeleafテンプレートを想定します。

HTMLテンプレートのファイル名

関連するActionクラスを識別できる名前にします。(規約)

SeaLandAction であれば、sea_land.html になります。 (名前は任意になりますが、合わせておくのがオススメです)

ここでは、sea_land.html にします。

HTMLテンプレートのディレクトリ

HTMLは、src/main/webapp/WEB-INF/view の下に作成します。(規約)

そこからのディレクトリ構成は、Actionクラスと同じで、sea_land.html なら、sea ディレクトリか sea/land ディレクトリの下に配置します。 (規約)

ここでは、src/main/webapp/WEB-INF/view/sea ディレクトリにします。

HTMLの実装...はさておいて

まだ、ラフスケッチ中なので、中身はシンプルな状態で。この時点では真っ白でもOKです。 ここでは、何も表示されないと悲しい ので、一覧を表示するHTMLだけちょこっと書いておきましょう。

LastaFluteオリジナルの属性が利用されていますので、ぜひ注目しましょう。

e.g. execute FreeGen @Command
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<!--/*/ <th:block th:include="/common/layout.html :: head (title='List of Purchase')"> /*/-->
	<meta charset="utf-8"/>
	<link rel="stylesheet" type="text/css" href="../../../css/reset.css" />
	<link rel="stylesheet" type="text/css" href="../../../css/common.css" />
	<link rel="stylesheet" type="text/css" href="../../../css/individual.css" />
	<title>Preview Title</title>
<!--/*/ </th:block> /*/-->
<!--/* individual Css write to after */-->
</head>
<body>
<header th:replace="/common/layout.html :: header">
	<section class="nav-main cf">
		<div class="wrap">
			<h1 class="main-title"><a href="#">Harbor<span> (LastaFlute Example)</span></a></h1>
			<ul class="nav-home">
				<li><a href="../product/product_list.html"><span class="link-block">Products</span></a></li>
				<li><a href="../member/member_list.html"><span class="link-block">Members</span></a></li>
				<li><a href="../withdrawal/withdrawal.html"><span class="link-block">Withdrawal</span></a></li>
			</ul>
			<ul class="nav-user">
				<li>
					<p class="nameHeader">Welcome, Mr.Guest</p>
					<ul class="child">
						<li><a href="#">Profile</a></li>
						<li><a href="#">Sign out</a></li>
					</ul>
				</li>
			</ul>
		</div>
	</section>
</header>
<main>
<div class="contents">
	<h2 class="content-title">List of Purchase</h2>
	<section class="product-search-box">
        <h3 class="content-title-second">Search Condition</h3>
        <p>
        	Payment Method: <span th:text="${pay}"/>
        	<span la:errors="pay"/>
        </p>
	</section>
    <section class="product-result-box">
        <h3 class="content-title-second">Search Results</h3>
        <table class="list-tbl">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Member Name</th>
                    <th>Product Name</th>
                    <th>Product Handle Code</th>
                    <th>Purchase Date</th>
                    <th>Purchase Price</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="bean : ${beans}">
                    <td th:text="${bean.purchaseId}">bean.purchaseId</td>
                    <td th:text="${bean.memberName}">bean.memberName</td>
                    <td th:text="${bean.productName}">bean.productName</td>
                    <td th:text="${bean.productHandleCode}">bean.productHandleCode</td>
                    <td th:text="${bean.purchaseDate}">bean.purchaseDate</td>
                    <td th:text="${bean.purchasePrice}">bean.purchasePrice</td>
                </tr>
            </tbody>
        </table>
    </section>
</div>
</main>
<footer th:replace="/common/layout.html :: footer">
	<div class="wrap">
		<ul class="footer-link">
			<li><a href="http://dbflute.seasar.org/">DBFlute Top</a></li>
			<li><a href="http://dbflute.seasar.org/lastaflute">LastaFlute Top</a></li>
		</ul>
		<p class="copyright">© LastaFlute project</p>
	</div>
</footer>
<!--/*/ <th:block th:include="/common/layout.html :: afterScript"> /*/-->
<!-- script contents -->
<script src="../../../js/jquery-2.1.3.min.js" ></script>
<script src="../../../js/common.js" ></script>
<!--/*/ </th:block> /*/-->
<!--/* individual Script write to after */-->
</body>
</html>

FreeGenを叩いてパス自動生成

それでは、HTMLテンプレートもできたことなので、そのファイルをプログラムから指定するためのパス定義を自動生成してみましょう。 LastaFluteでは、ファイルパスをハードコードしたりしません。

manageタスク (DBFluteクライアントの manage.bat|sh) の 12 (freegen) を叩いて、FreeGen を実行すると、そのHTMLテンプレートに対応するパス定義が自動生成されます。 ここでは HarborHtmlPathpath_Sea_SeaLandHtml というパス定義が追加されるでしょう。

e.g. execute FreeGen @Command
...$ sh manage.sh

// メニューが表示されるので、12 (freegen) を入力してEnter
// Windowsなら manage.bat, Eclipseからでも直接叩ける (ctrl+shift+Rから叩こう)

ひとまずreturnを書いておく

Executeメソッドの実装はこの後ですが、せっかく HarborHtmlPath にパス定義ができたことですし、 コンパイルエラーのまま実装するのはつらいので、とりあえず解決しておきましょう。

asHtml([自動生成されたHTMLテンプレートのパス定義]) を return します。

e.g. return HtmlResponse as HTML template @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    return asHtml(path_Sea_SeaLandHtml); 
}

もし、別の Action へのリダイレクトなら、redirect([リダイレクト先のAction]) を return します。

e.g. return HtmlResponse as redirect @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    return redirect(PiariDstoreAction.class); 
}

(jflute備忘録: デモンストレーションのとき、ここで Boot で画面アクセス)

HTML Bean を作る

HTMLテンプレートへ表示データを渡すための HTML Bean クラスを作成し、検索されたデータを詰め替えます。

HTML Beanの定義

名前に関しては、特に制約はありませんが、慣習として Bean という名前のクラスをActionの隣に作っています。 ここでは、SeaLandRowBean というクラスを作りましょう。

e.g. Bean class location @Directory
src/main/java
 |-org.docksidestage
 |  |-app
 |  |  |-logic
 |  |  |-web
 |  |  |  |-sea
 |  |  |  |  |-SeaLandAction.java
 |  |  |  |  |-SeaLandForm.java
 |  |  |  |  |-SeaLandRowBean.java
 |  |  |  |-...
 |  |  |-...
 |  |-bizfw
 |  |-dbflute
 |  |-...

HTML Beanの実装

プロパティの宣言の仕方は、基本的に Form と同じです。

ここでは、HTMLテンプレートの中で表示する一行のデータ項目を表現しましょう。

e.g. properties for display in HTML Bean class @Java
/**
 * @author yourname
 */
public class SeaLandRowBean {

    @Required
    public Long purchaseId;
    @Required
    public String memberName;
    @Required
    public String productName;
    @Required
    public String productHandleCode;
    @Required
    public LocalDate purchaseDate;
    @Required
    public Integer purchasePrice;
}

HTML Bean でも Validation

HTML Bean でも Validator Annotation を付けましょう(@Required や @NotNull など基本的なものだけ)。 HTMLテンプレートに対する入力チェックと解釈することができます。 HTMLテンプレート内で発生する例外はデバッグしづらいので事前に防ぐとともに、ドキュメント的な意味合いも兼ねます。

Executeメソッドを実装

validate()を呼ぶ

FormにValidatorアノテーションを一つでも付けているなら、validate() を呼ぶ必要があります。 (今回、付けているのでもちろん呼びましょう)

e.g. validate() in Action class @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    return asHtml(path_Sea_SeaLandHtml); 
}

Eclipseで、DBFlute秘伝の EditorTemplate が設定されていれば、魔法のように補完できます。

相関バリデーションやDBを使ったバリデーションなど、アノテーションでは実現できないものは、第二引数の Lambda の中で実装します。 (if文でチェックしてメッセージを追加)

DBFluteで検索しよう

DBFluteを使って検索・更新などを行うのであれば...基点テーブルの Behavior を DI しましょう。

ここでは、購入の一覧でしたから、基点テーブルは PURCHASE です。

e.g. DBFlute Behavior's DI @Java
@Resource
private PurchaseBhv purchaseBhv;

@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    ...
}

そして、ConditionBean で検索しましょう。

e.g. ログイン会員(自分)とフォローしている会員の、指定された商品に対応する購入一覧 @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        cb.setupSelect_Member();
        cb.setupSelect_Product();
        cb.query().setProductId_Equal(productId);
        cb.orScopeQuery(orCB -> {
            orCB.query().setMemberId_Equal(userId);
            orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                followingCB.query().setMyMemberId_Equal(userId);
            });
        });
        cb.query().existsPurchasePayment(paymentCB -> {
            paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
        });
        cb.query().addOrderBy_PurchaseDatetime_Desc();
    });
    return asHtml(path_Sea_SeaLandHtml); 
}

※実際、商品IDが固定なので、商品は単独データ取得してリストでは要らないはずですが、簡略化しています

ログインしているユーザーの情報は、getUserBean() で取得できます。 ここでは、ログインしていることが前提なので、戻り値の OptionalThing は問答無用で get() してしまいます。

まだ、ラフスケッチ中なので、完璧な実装じゃなくてもOKです。とりあえずなんか検索できれば。

HTML Beanにマッピング

検索したデータ (Entity) を、HTML Bean にマッピングします。(いわゆる詰替え)

e.g. mapping entity to HTML bean in Action class @Java

@Resource
private PurchaseBhv purchaseBhv;

@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        ...
    });
    List<SeaLandRowBean> beans = purchaseList.stream().map(purchase -> {
        SeaLandRowBean bean = new SeaLandRowBean();
        bean.purchaseId = purchase.getPurchaseId();
        purchase.getMember().alwaysPresent(member -> {
            bean.memberName = member.getMemberName();
        });
        purchase.getProduct().alwaysPresent(product -> {
            bean.productName = product.getProductName();
            bean.productHandleCode = product.getProductHandleCode();
        });
        bean.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
        bean.purchasePrice = purchase.getPurchasePrice();
        return bean;
    }).collect(Collectors.toList());
    return asHtml(path_Sea_SeaLandHtml); 
}

ちなみに、リフレクションで詰め替えるとかは強烈に非推奨です。

表示データをResponseに設定

asHtml()を使っている場合、HTMLテンプレートに表示データを渡します。

asHtml()に続いて、renderWith()メソッドを呼び、Lambda引数の data の register() を使って、HTMLテンプレート上で参照する名前をキーに、表示データを登録します。

e.g. renderWith() in Action class @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandHtml);
    });
    Integer userId = getUserBean().get().getUserId();
    ListResultBean<Purchase> purchaseList = ...
    List<SeaLandRowBean> beans = ...
    return asHtml(path_Sea_SeaLandHtml).renderWith(data -> {
        data.register("beans", beans);
    });
}

ラフスケッチできた

さて、これでラフスケッチ実装は終了です。全体像が、見えてきたでしょうか?

とりあえずActionはこんな感じ

この時点では、このような感じで Action ができあがっているはずです。

e.g. rough sketch finished @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {

    @Resource
    private PurchaseBhv purchaseBhv;

    @Execute
    public HtmlResponse index(int productId, SeaLandForm form) {
        validate(form, messages -> {}, () -> {
            return asHtml(path_Sea_SeaLandHtml);
        });
        Integer userId = getUserBean().get().getUserId();
        ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
            cb.setupSelect_Member();
            cb.setupSelect_Product();
            cb.query().setProductId_Equal(productId);
            cb.orScopeQuery(orCB -> {
                orCB.query().setMemberId_Equal(userId);
                orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                    followingCB.query().setMyMemberId_Equal(userId);
                });
            });
            cb.query().existsPurchasePayment(paymentCB -> {
                paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
            });
            cb.query().addOrderBy_PurchaseDatetime_Desc();
        });
        List<SeaLandRowBean> beans = purchaseList.stream().map(purchase -> {
            SeaLandRowBean bean = new SeaLandRowBean();
            bean.purchaseId = purchase.getPurchaseId();
            purchase.getMember().alwaysPresent(member -> {
                bean.memberName = member.getMemberName();
            });
            purchase.getProduct().alwaysPresent(product -> {
                bean.productName = product.getProductName();
                bean.productHandleCode = product.getProductHandleCode();
            });
            bean.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
            bean.purchasePrice = purchase.getPurchasePrice();
            return bean;
        }).collect(Collectors.toList());
        return asHtml(path_Sea_SeaLandHtml).renderWith(data -> {
            data.register("beans", beans);
        });
    }
}

Bootしてアクセスしてみましょう

Bootクラスを起動して、実際にそのURLでアクセスしてみましょう。 恐らく非常にそっけない画面しか表示されないと思いますが、ログをみてちゃんと動いていればこの先に進めます。

ここでは、HarborBoot クラスの main() を実行しましょう。

e.g. HarborBoot @Java
/**
 * @author jflute
 */
public class HarborBoot { // #change_it_first

    public static void main(String[] args) { // e.g. java -Dlasta.env=production -jar harbor.war
        new JettyBoot(8090, "/harbor").asDevelopment(isDevelopment()).bootAwait();
    }

    private static boolean isDevelopment() {
        return System.getProperty("lasta.env") == null;
    }
}
e.g. boot log @Console
INFO  (...@showBoot():105) - _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
INFO  (...@showBoot():106) -  the system has been initialized:
INFO  (...@showBoot():107) - 
INFO  (...@showBoot():108) -   -> Harbor (Local Development)
INFO  (...@showBoot():109) - _/_/_/_/_/_/_/_/_/_/
Boot successful as development: url -> http://localhost:8090/harbor

ここで、http://localhost:8090/harbor/sea/land/3?pay=HAN にアクセスしてみましょう。 さて、ブラウザで何が表示されたでしょうか?

ただ、この時点で単純にアクセスするとログイン必須エラーでログイン画面に飛ばされます。 harborプロジェクトはログイン機能を使っているのでActionクラスはデフォルトでログイン必須になっていますし、処理の中でログインユーザーの情報を利用しているのでログインが前提です。

ログイン画面で、'Pixy', 'sea' と入力してログインしてみましょう。

サーバーのログでも確認してみましょう

サーバーのログ (IDEのコンソールなど) にも着目してみましょう。 想定通りのSQLが実行されて、想定通しのJSONレスポンスが戻っていることを確認してみてください。

LastaFluteでこのようにログが出る、ということを知っておくことも大切です。うまく活用してスムーズなデバッグライフを送ってください。

ちょいっとリファクタリング

ちょっとメソッド長いですね。実際に業務だと、もっと大きくなる可能性があります。

IDEのショートカットを使って、稲妻のようにリファクタリングして、確認のためもう一度アクセスしてみましょう。 (jflute備忘録: デモンストレーションのとき稲妻のように)

e.g. after refactoring @Java
/**
 * @author yourname
 */
public class SeaLandAction extends HarborBaseAction {

    // ===================================================================================
    //                                                                           Attribute
    //                                                                           =========
    @Resource
    private PurchaseBhv purchaseBhv;

    // ===================================================================================
    //                                                                             Execute
    //                                                                             =======
    @Execute
    public HtmlResponse index(int productId, SeaLandForm form) {
        validate(form, messages -> {}, () -> {
            return asHtml(path_Sea_SeaLandHtml);
        });
        ListResultBean<Purchase> purchaseList = selectPurchaseList(productId, form);
        List<SeaLandRowBean> beans = mappingToBeans(purchaseList);
        return asHtml(path_Sea_SeaLandHtml).renderWith(data -> {
            data.register("beans", beans);
        });
    }

    // ===================================================================================
    //                                                                              Select
    //                                                                              ======
    private ListResultBean<Purchase> selectPurchaseList(int productId, SeaLandForm form) {
        Integer userId = getUserBean().get().getUserId();
        ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
            cb.setupSelect_Member();
            cb.setupSelect_Product();
            cb.query().setProductId_Equal(productId);
            cb.orScopeQuery(orCB -> {
                orCB.query().setMemberId_Equal(userId);
                orCB.query().queryMember().existsMemberFollowingByYourMemberId(followingCB -> {
                    followingCB.query().setMyMemberId_Equal(userId);
                });
            });
            cb.query().existsPurchasePayment(paymentCB -> {
                paymentCB.query().setPaymentMethodCode_Equal_AsPaymentMethod(form.pay);
            });
            cb.query().addOrderBy_PurchaseDatetime_Desc();
        });
        return purchaseList;
    }

    // ===================================================================================
    //                                                                             Mapping
    //                                                                             =======
    private List<SeaLandRowBean> mappingToBeans(ListResultBean<Purchase> purchaseList) {
        List<SeaLandRowBean> beans = purchaseList.stream().map(purchase -> {
            SeaLandRowBean bean = new SeaLandRowBean();
            bean.purchaseId = purchase.getPurchaseId();
            purchase.getMember().alwaysPresent(member -> {
                bean.memberName = member.getMemberName();
            });
            purchase.getProduct().alwaysPresent(product -> {
                bean.productName = product.getProductName();
                bean.productHandleCode = product.getProductHandleCode();
            });
            bean.purchaseDate = purchase.getPurchaseDatetime().toLocalDate();
            bean.purchasePrice = purchase.getPurchasePrice();
            return bean;
        }).collect(Collectors.toList());
        return beans;
    }
}

ホットデプロイ体験

例えば、cb.query().setProductId_Equal(productId) を一時的にコメントアウトして、(再起動せずに)アクセスしてみましょう。 検索結果が変わるはずです。(終わったら戻しておきましょう)

appパッケージ配下のクラスは、ホットデプロイ (HotDeploy) が効きますので、修正したらすぐに反映されます。 それの特徴をうまく活用して、開発効率を上げていってください。 (ただし、FreeGenなどDBFluteの自動生成を挟んだときは再起動が必要です)

DefTestでポリシーチェック

"Action定義のテスト" のドキュメントをよく読み...

HarborActionDefTest を実行してみましょう。

現時点で特に何か落ちるような不備はないはずなので、結果はgreenになるはずです。 何か一つでも、わざと落ちるような修正をしてみて、結果がredになるようにして例外メッセージを読んでみましょう。 (読み終わったら元に戻して、greenになることを確認しましょう)

LastaFluteには、最初からこのような横断的なプログラムのポリシーチェックをする UnitTest が用意されています。UnitTestは気軽に実行できるものですから、"Actionを作りながら" や "Actionを作り終わったとき" などに、実行してみてポリシー崩れがないかどうか確認するようにしましょう。

LastaDocの自動生成

"Actionのドキュメント自動生成" のドキュメントをよく読み...

LastaDocを自動生成し直して、新しく自分で作った SeaLandAction が LastaDoc に反映されることを確認してみましょう。

LastaFluteには、このようにActionに関するドキュメントを自動生成する機能が備わっています。 DBFluteで言えば、SchemaHTML のようなものです。ぜひ、有効活用していってください。

ちょこっとTips

スーパークラスのメソッド、何が使える?

Actionクラスの中で、this.docu...() と document メソッドを補完して、JavaDocを表示してみてください。すると...

e.g. document methods @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    ...
    this.docu // 補完して、IDE上でJavaDoc表示
    ...
}

実装しながら、ふとActionの規約を忘れちゃったときとか、ブラウザで検索してドキュメントを探す...ではなく、 ササッと this.docu... ってやってみるといいかもですね。

事務連絡

harbor で、実際に SeaLandAction を作っていくと、HarborHtmlPath など幾つかのリソースに修正マークが付いてしまいます。 (Actionとかは、gitignoreしてますが、どうしても無視設定できない修正が...)