第11章: モデル

モデル(IModel)は Wicket の心臓部です。 コンポーネントとデータを結びつける抽象層であり、 Wicket のコンポーネントが動的なデータを表示・編集するための仕組みです。 モデルを正しく理解することが、Wicket を使いこなす鍵です。

モデルとは

IModel<T> は、コンポーネントが表示するデータを取得・設定するためのインターフェースです。 コンポーネントは直接データを保持するのではなく、モデルを介してデータにアクセスします。

public interface IModel<T> extends IDetachable {
    T getObject();             // データを取得
    void setObject(T object);  // データを設定
    void detach();             // リクエスト終了時にリソースを解放
}

なぜモデルが必要なのか

モデルを使わず直接値を渡すこともできますが、以下の問題があります:

// 悪い例: 値が固定されてしまう
add(new Label("name", user.getName()));
// user.setName("新しい名前") しても Label は更新されない!

// 良い例: モデルを使うと常に最新値が表示される
add(new Label("name", new PropertyModel<>(user, "name")));
// user.setName("新しい名前") すると Label の表示も変わる

モデルの利点:

Model.of() - 静的モデル

Model.of() は、固定値を保持するシンプルなモデルを作成します。 値が変更されない場合や、初期値として使う場合に適しています。

// 文字列の静的モデル
IModel<String> nameModel = Model.of("田中太郎");
add(new Label("name", nameModel));

// フォームの入力値を保持するモデル
IModel<String> inputModel = Model.of("");
form.add(new TextField<>("input", inputModel));
// フォーム送信後: inputModel.getObject() で入力値を取得

// 数値の静的モデル
IModel<Integer> countModel = Model.of(0);
add(new Label("count", countModel));

ラムダ式でのモデル(読み取り専用)

IModel は関数型インターフェースなので、ラムダ式で読み取り専用モデルを作成できます。

// ラムダ式による読み取り専用モデル
add(new Label("currentTime",
    () -> LocalDateTime.now().toString()));

// 別のモデルの値を変換
add(new Label("nameUpper",
    () -> user.getName().toUpperCase()));

PropertyModel

PropertyModel は、オブジェクトのプロパティ(getter/setter)にアクセスするモデルです。 プロパティ名を文字列で指定します。

User user = new User("田中太郎", "[email protected]");

// user.getName() / user.setName() にアクセス
add(new Label("name", new PropertyModel<>(user, "name")));

// user.getEmail() / user.setEmail() にアクセス
add(new TextField<>("email", new PropertyModel<>(user, "email")));

// ネストしたプロパティにもアクセス可能(ドット表記)
add(new Label("city", new PropertyModel<>(user, "address.city")));
// → user.getAddress().getCity() を呼び出す

PropertyModel はプロパティ名を文字列で指定するため、 リファクタリング時にプロパティ名を変更すると実行時エラーになります。 IDE のリファクタリング機能では検出できないので注意してください。 型安全を重視する場合は LambdaModel を使いましょう。

CompoundPropertyModel

CompoundPropertyModel は、コンポーネントの wicket:id を そのままプロパティ名として使うモデルです。 フォームの各フィールドに個別にモデルを設定する手間を省けます。

public class UserEditPage extends WebPage {
    public UserEditPage() {
        User user = new User();

        Form<User> form = new Form<>("form",
            new CompoundPropertyModel<>(user));
        add(form);

        // wicket:id がそのままプロパティ名として使われる
        form.add(new TextField<>("name"));       // → user.getName()
        form.add(new TextField<>("email"));      // → user.getEmail()
        form.add(new TextField<>("age"));        // → user.getAge()
        form.add(new TextArea<>("bio"));         // → user.getBio()
    }
}

CompoundPropertyModel を使わない場合、各フィールドに個別にモデルを設定する必要があります:

// CompoundPropertyModel を使わない場合(冗長)
form.add(new TextField<>("name", new PropertyModel<>(user, "name")));
form.add(new TextField<>("email", new PropertyModel<>(user, "email")));
form.add(new TextField<>("age", new PropertyModel<>(user, "age")));
form.add(new TextArea<>("bio", new PropertyModel<>(user, "bio")));

CompoundPropertyModel はフォームでのデータバインディングに最も効率的です。 wicket:id をプロパティ名に合わせて設計すると、コードが大幅に簡潔になります。

LambdaModel

LambdaModel は、getter と setter をラムダ式で指定する型安全なモデルです。 PropertyModel の文字列ベースのプロパティ指定を避けられます。

// 読み書き両方のモデル
IModel<String> nameModel = LambdaModel.of(
    user::getName,      // getter
    user::setName       // setter
);
form.add(new TextField<>("name", nameModel));

// 読み取り専用モデル
IModel<String> displayModel = LambdaModel.of(user::getFullName);
add(new Label("fullName", displayModel));

PropertyModel との比較:

観点PropertyModelLambdaModel
型安全性 文字列指定(実行時エラー) メソッド参照(コンパイル時チェック)
リファクタリング IDE で自動追従しない IDE で自動追従する
ネストプロパティ "address.city" で簡単 自分でチェーンする必要あり
簡潔さ 文字列1つで済む getter/setter の2つ必要

LoadableDetachableModel

LoadableDetachableModel(LDM)は、データの遅延読み込みとメモリ効率の最適化を行うモデルです。 DB からのデータ取得に最適です。

LDM の動作:

  1. 最初に getObject() が呼ばれた時に load() を実行してデータを取得
  2. 同一リクエスト内では load() の結果をキャッシュして再利用
  3. リクエスト終了時に detach() でキャッシュをクリア(メモリ解放)
  4. 次のリクエストで再び load() を実行して最新データを取得
// DB からユーザーリストを取得する LDM
IModel<List<User>> usersModel =
    new LoadableDetachableModel<>() {
        @Override
        protected List<User> load() {
            // リクエストごとに1回だけ実行される
            return userRepository.findAll();
        }
    };

add(new ListView<>("users", usersModel) {
    @Override
    protected void populateItem(ListItem<User> item) {
        item.add(new Label("name", item.getModelObject().getName()));
    }
});

IDベースの LDM

エンティティの ID だけをセッションに保存し、必要な時に DB から再取得するパターンです。 セッションのメモリ使用量を最小限に抑えられます。

public class UserModel extends LoadableDetachableModel<User> {
    private final Long userId;

    public UserModel(Long userId) {
        this.userId = userId;
    }

    @Override
    protected User load() {
        // ID からエンティティを再取得
        return userRepository.findById(userId);
    }
}

// 使い方
add(new Label("userName",
    new PropertyModel<>(new UserModel(userId), "name")));

LDM を使うべき場面:

  • DB やリモートサービスからデータを取得する場合
  • 重いオブジェクトをセッションに保存したくない場合
  • データが他のリクエストで変更される可能性がある場合

静的な文字列や小さなオブジェクトには Model.of() で十分です。

リスト用モデル

// リストのモデル
IModel<List<String>> listModel = Model.ofList(
    Arrays.asList("りんご", "みかん", "ぶどう"));

// Set のモデル
IModel<Set<String>> setModel = Model.ofSet(
    new HashSet<>(Arrays.asList("A", "B", "C")));

// Map のモデル
IModel<Map<String, Integer>> mapModel = Model.ofMap(
    new HashMap<>());

モデルの連鎖(Model Chaining)

モデルを連鎖させて、値の変換やフィルタリングを行うことができます。 IModel.map() メソッドを使います。

IModel<User> userModel = Model.of(user);

// ユーザー名を大文字に変換するモデル
IModel<String> upperNameModel = userModel
    .map(User::getName)
    .map(String::toUpperCase);

add(new Label("name", upperNameModel));

// 条件付きの表示テキスト
IModel<String> statusModel = userModel
    .map(u -> u.isActive() ? "有効" : "無効");
add(new Label("status", statusModel));

モデルの使い分け

モデル用途特徴
Model.of() 固定値、フォームの入力バッファ シンプル、値を直接保持
PropertyModel オブジェクトのプロパティアクセス 文字列ベース、ネスト可
CompoundPropertyModel フォーム全体のバインディング wicket:id=プロパティ名
LambdaModel 型安全なプロパティアクセス メソッド参照、リファクタリング安全
LoadableDetachableModel DB / 外部リソースからのデータ取得 遅延読み込み、メモリ効率
ラムダ式 () -> ... 読み取り専用の動的な値 最も簡潔、読み取り専用

迷ったときの選び方:

  • フォームのバインディング → CompoundPropertyModel
  • DB からのデータ → LoadableDetachableModel
  • 表示だけ → ラムダ式 () -> ...
  • 個別プロパティの読み書き → LambdaModel(型安全)

実務でハマりやすい注意点

  • 問題
    フォーム値がどこに紐付いているか追いづらく、保守時の理解コストが高い。改修で副作用を出しやすい。
    原因
    CompoundPropertyModel の暗黙バインディングに依存しすぎている。短く書ける一方で、対応関係がコードから読み取りにくい。
    対策
    重要な項目は PropertyModel や LambdaModel を明示し、どの値を扱うかをコードで表現する。意図を先に可視化する。
  • 問題
    PropertyModel の値が、実行タイミングによって期待とズレる。初期表示だけおかしい、といった症状になりやすい。
    原因
    コンポーネント階層が確定する前提で式解決している。親子関係が未確定の段階だと正しく参照できない。
    対策
    コンポーネント階層が確定した後(onInitialize/onConfigure 以降)で評価する。初期化順を意識して実装する。
  • 問題
    セッションが膨らんでメモリ使用量が増え、長時間運用で性能低下する。ピーク時に顕在化しやすい。
    原因
    LoadableDetachableModel の detach 前提が崩れ、不要な参照が残っている。解放されるべきオブジェクトが保持され続ける。
    対策
    「必要なときにロードし、リクエスト終端で破棄する」方針でモデルを設計する。重いオブジェクトは保持しない。
  • 問題
    同じ画面操作でも入力結果が安定しない。利用者にはランダム不具合に見える。
    原因
    同一モデルを複数入力部品で共有し、更新順序の影響を強く受けている。どの値が最終採用されるかが曖昧。
    対策
    モデルを分離し、共有が必要なら更新順序を明確に制御する。順序依存の有無をテストで固定化する。