読者です 読者をやめる 読者になる 読者になる

Apache Click 2.2.0: DataProvider

Click

ClickのブログでBobが書いてくれた2.2.0の目玉機能のオーバービューを日本語訳してみようと思います。3回にわかれているのですが、まずは初回はDataProviderについてです。意味がわかりやすいようにかなり超訳していますが、原文は以下のエントリです。

DataProviderの利点を理解するために、解決すべき問題を見る必要があります。まずはClickのページのライフサイクルのおさらいをしておきましょう。ページクラスでは以下の順番でイベントが実行されます。

  • <>: ページの作成
  • onInit: コントロールの生成とページへの追加後に呼び出されます
  • onProcess: コントロールへのリクエストパラメータがバインド、バリデーション、アクションリスナの実行後に呼び出されます
  • onRender: データベースの操作はここで行われます

Clickでは、あるイベントの結果によって後続のイベントをスキップさせることができます。たとえば、onRenderはコントロールのアクションリスナがfalseを返した場合はスキップされます。onRenderは、Clickによって呼び出される最後のイベントという事実も相まってデータベースに関する処理を実装する場所として適切です。コントロールのアクションリスナが別のページにリダイレクトする場合、falseを返却することでonRenderで行われる重い処理(データベース処理など)をバイパスすることができます。
CustomerPage.java

public class CustomerPage extends BorderPage {
 
    private Table table = new Table("table");
    private ActionLink editLink = new EditLink("edit");
 
    @Override
    public void onInit() {
        super.onInit();
 
        table.add(new Column("firstname"));
 
        ...
 
        editLink.setActionListener(new ActionListener() {
            public boolean onAction(Control source) {
                Map params = Collections.singletonMap("id", editLink.getValue());
 
               // Redirect to edit customer page and pass the selected customer ID
               setRedirect(EditCustomerPage.class, params);
               return false;
            }
        });
    }
 
    @Override
    public void onRender() {
        // Database intensive operation: retrieving all customers from the database
        List<Customer> customers = getCustomerService.getCustomers();
        table.setRows(customers);
    }
}

このサンプルでは、取得した顧客の一覧をレンダリングしない別のページにリダイレクトする場合は、データベースから顧客の一覧を取得したくないということがわかります。言い換えれば、顧客の一覧がリダイレクトされるページでレンダリングされないので、データベースアクセスのための対価を払いたくないということです。アクションリスナがfalseを返した場合onRenderイベントがスキップされるので、データベースアクセスを行うコードをここに置くことが適切ということになります。

矛盾

残念ながら、このパターンをすべてのコントロールに適用することはできません。いくつかのコントロールはonProcessイベントの前にバリデーションもしくはパラメータバインディングのために値を設定する必要があります。たとえば、Selectコントロールのバリデーションは有効な値を選択したか否かを判定するために選択項目の値が必要になりますし、FormTableはエンティティの値をリクエストパラメータで更新するために初期値を行に設定しておく必要があります。
いくつかのコントロールはonInitで値が設定し、他のコントロールはonRenderで値を設定する、という矛盾があるわけです。これはClickの新しいユーザにとって、どのコントロールにどのイベントで値を設定するかを判断するときの一般的名落とし穴でもあります。

DataProvider

この矛盾の解決策として、オンデマンドでのデータのローディングを可能にするためのDataProviderインターフェースが追加されました。DataProviderは"public List getData()"というメソッドを持っています。コントロールはデータが必要になったタイミングでこのメソッドを呼び出します。たとえば、Tableは自身がレンダリングされるタイミングでgetDataメソッドを呼び出しますし、Selectはバリデーションが実行されるタイミングでgetDataメソッドを呼び出します。また、FormTableはそれが処理されるタイミングでgetDataメソッドを呼び出します。
DataProviderは、すべてのコントロール生成処理をonInitイベントにもしくはページのコンストラクタカプセル化できるという点でより一貫したページの実装を可能にします。以下に例を示します。
CustomerPage.java

public class CustomerPage extends BorderPage {
 
    private Table table = new Table("table");
    private Select select = new Select("markets");
 
    @Override
    public void onInit() {
        super.onInit();
 
        table.add(new Column("firstname"));
 
        ...
 
        table.setDataProvider(new DataProvider() {
            public List<Customer> getData() {
                return getCustomerService().getCustomers();
            }
        });
 
        select.setDataProvider(new DataProvider() {
            public List<Option> getData() {
                List options = new ArrayList();
                for (Market market : getMarketService().getMarkets()) {
                    options.add(new Option(market.getId(), market.getName());
                }
                return options;
            }
        });
    }
}

このサンプルではTableとSelectコントロールの両方にDataProviderを使用しています。Tableのデータはデーブルが表示されるタイミングで取得され、リダイレクトされる場合はスキップされます。ページクラスではすべてのコントロール初期化処理がonInitイベントに集約されており、一貫しています。

onRenderはまだ必要か?

必要です。DataProviderはコントロールの初期化処理をonInitイベントにカプセル化することができますが、すべてのデータがコントロールで表示されるというわけではありません。たとえば、ページで取得した顧客の一覧をページテンプレート(Velocity)でHTMLとしてレンダリングすることもできます。onRenderは顧客の一覧を取得し、それをテンプレートで利用する場合に使用することができます。
CustomerPage.java

public class CustomerPage extends BorderPage {
 
    public void onRender() {
        addModel("customers", getCustomerService().getCustomers());
    }
}

customer.vm

<table>
...
 
#foreach($customer in $customers)
  <tr>
    <td>
      $customer.name
    <td>
    <td>
      $customer.holdings
    <td>
  </tr>
#end
</table>

もしアクションリスナがfalseを返す場合、onRenderはスキップされ、データベース処理は実行されません。これは我々の望んでいる振る舞いになります。