この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

更新系ユースケースにおけるフィールド初期値の復元

確認画面付きの更新系のユースケースにおいては、入力画面を表示するときに最初に一度だけ初期値を復元する必要があります。

このとき、入力フィールドに初期値を表示するために(#initialValueOfHogeはどこかから持ってきた値とする)

<s:textfield name="hoge" value="#initialValueOfHoge" />

と書いてしまうとダメです。これだと

  1. 入力画面を表示する。DBから読み出した初期値が表示されている
  2. abcを入力し、確認画面へ進む (まだDBへの書き込みは行われていない)
  3. 確認画面で「戻る」ボタンで入力画面に戻る

としたとき、入力した値abcは失われ、再びDBから読み出した初期値が表示されてしまうからです。

基本的に、value属性で値を指定すると、それは初期値ではなく指定値として扱われます。
従ってBeanに保持されている値は完全に無視されます。


それでは、初期値を復元するにはどうすれば良いでしょうか。
大きく分けて2つのアプローチがあると思います。

アプローチ1: セッションに初期値復元済みマークを保持しておく

一番単純なアプローチは、復元済みのマークをセッションに保持しておくことで、一度だけ初期値を表示させることです。

<%-- 初期値を最初に一度だけ復元する --%>
<s:if test="!#session.initialValueRestored">
  <s:textfield name="hoge" value="#initialValueOfHoge" />
  
  <%-- 初期値を復元済みであるとマークする --%>
  <s:set value="#session.initialValueRestored = true" />
</s:if>

<%-- 初期値が復元済みなら何もしない --%>
<s:else>
  <s:textfield name="hoge" />
</s:else>


しかしこのアプローチには2つの問題点があります。

問題1: メンテナンス性の問題

if-elseで殆ど同じようなコード(valueがあるかないかの違いだけ)をコピペして使うことになるため、メンテナンス性に問題があります。

1つの画面で入力フィールドが何十個もある場合は、エライことになります。

問題2: 復元済みのマークをクリアするタイミングが難しい

DBに書きこむと同時に完了画面(確認画面の次の画面)にて復元済みのマークをクリアするのが原則です。が…

マークをクリアするタイミングをスキップされてしまったら? (例:確認画面から他のユースケースへ直接ジャンプするとか)
とかそういうコトを考え出すと結構面倒になってきます。

アプローチ2: 入力値を保持するBeanに一度だけ初期値を復元する

入力値を保持するBean(大抵の場合はActionクラスそのもの)に一度だけ初期値を復元するアプローチです。

こちらでは、アプローチ1のような問題点はありません。

メンテナンス性の問題

value属性を指定する必要がないため1パターンで記述できます。

復元済みのマークをクリアするタイミングが難しい

セッションにマークを残すわけでないので、Beanのライフサイクルにだけ気をつければよいです。

大抵の場合では「Bean = アクションクラス」だと思いますが、この場合アクションクラスが
ユースケース終了と同時に破棄されるようなライフサイクルとして設定する必要があります。
いわゆるconversationスコープってやつですね。


サンプルコードはこんな感じですかね。

import java.util.*;

public class HogeInputAction {

    private String hoge;

    
    public String execute() {
    //初期値を一度だけ復元する
        ActionUtil.restoreInitialValueOnce(
            this, readInitialValues());
    
        return SUCCESS;
    }
    
    //データストアから初期値を読み出す
    private Map<String, Object> readInitialValues() {
        ...
    }

    public String getHoge() {
        return hoge;
    }

    public void setHoge(String hoge) {
        this.hoge = hoge;
    }
}
import java.util.*;

import org.apache.commons.beanutils.PropertyUtils;

public final class ActionUtil {

    //初期値復元済みのアクションを覚えておく
    private static final Map<Object, Boolean> RESTORED_ACTIONS = 
        Collections.synchronizedMap(new WeakHashMap<Object, Boolean>());

    
    //初期値を一度だけ復元する
    public static final void restoreInitialValueOnce(Object action, Map<String, Object> initialValues) {
        if (!RESTORED_ACTIONS.containsKey(action)) {
            RESTORED_ACTIONS.put(action, true);
            PropertyUtils.copyProperties(initialValue, action);
        }
    }
}


ちなみにR42FWではこちらの方式を採用しています。