第十一戒: モデルの真理 〜 IModel こそ Wicket の魂なり
モデル(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 の表示も変わる
モデルの利点:
- 動的データ: 描画のたびに最新のデータを取得できる
- 双方向バインディング: フォームでの値の読み書きが自動的に行われる
- 遅延読み込み: データが必要になるまでDBアクセスを遅延できる
- メモリ効率:
detach()でリクエスト間のメモリ使用量を削減できる
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 との比較:
| 観点 | PropertyModel | LambdaModel |
|---|---|---|
| 型安全性 | 文字列指定(実行時エラー) | メソッド参照(コンパイル時チェック) |
| リファクタリング | IDE で自動追従しない | IDE で自動追従する |
| ネストプロパティ | "address.city" で簡単 |
自分でチェーンする必要あり |
| 簡潔さ | 文字列1つで済む | getter/setter の2つ必要 |
LoadableDetachableModel
LoadableDetachableModel(LDM)は、データの遅延読み込みとメモリ効率の最適化を行うモデルです。
DB からのデータ取得に最適です。
LDM の動作:
- 最初に
getObject()が呼ばれた時にload()を実行してデータを取得 - 同一リクエスト内では
load()の結果をキャッシュして再利用 - リクエスト終了時に
detach()でキャッシュをクリア(メモリ解放) - 次のリクエストで再び
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 前提が崩れ、不要な参照が残っている。解放されるべきオブジェクトが保持され続ける。対策「必要なときにロードし、リクエスト終端で破棄する」方針でモデルを設計する。重いオブジェクトは保持しない。
-
問題同じ画面操作でも入力結果が安定しない。利用者にはランダム不具合に見える。原因同一モデルを複数入力部品で共有し、更新順序の影響を強く受けている。どの値が最終採用されるかが曖昧。対策モデルを分離し、共有が必要なら更新順序を明確に制御する。順序依存の有無をテストで固定化する。