かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

RichFaces4.xのDataTableでページングしつつ遅延読み込み

遅延読み込みしたいですよね?10000件とか下手したら無限とも言えるデータを全部メモリに抱え込んでとか、特にサーバーサイドにステートを持つのがデフォのJSFでやったら死んでしまいます。RichFacesには、そんな要望に応えるためのクラスがあります!!
個人的に残念なのは、JSFのクラスに依存してるところ…。POJOPOJOしたいです。

レッツトライ

とりあえず、やり方。NetBeansmavenプロジェクトを追加してpomにrichfacesへの依存関係を追加します。

<dependency>
    <groupId>org.richfaces.ui</groupId>
    <artifactId>richfaces-components-ui</artifactId>
    <version>4.3.1.Final</version>
</dependency>
<dependency>
    <groupId>org.richfaces.core</groupId>
    <artifactId>richfaces-core-impl</artifactId>
    <version>4.3.1.Final</version>
</dependency>

テーブルに表示するデータの準備

とりあえず、テーブルに表示するためのデータを適当に作ります。サンプルなので、別にデータを取得するためのクラスを作るのがめんどくさいため、このクラスにstaticメソッドとしてデータを取得するメソッドも追加します。
データの件数は、とりあえず10000件を想定してます。

package com.mycompany.lazypager;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

public class Person implements Serializable {

    private static final long serialVersionUID = 6101343577835043120L;
    private int id;
    private String name;

    public Person() {
    }

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // 指定した範囲のデータを返すメソッド
    public static Person[] getRange(int start, int count) {
        List<Person> r = new ArrayList<>();
        for (int i = start; i < start + count; i++) {
            r.add(new Person(i, "田中 太郎" + i));
        }
        return r.toArray(new Person[r.size()]);
    }
    // とりあえず一万件のデータを想定

    public static int getLength() {
        return 10000;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

最大のポイント ExtendedDataModelを継承したクラス

ExtendedDataModelクラスを継承して、必要なだけデータをとってくる処理を書きます。こいつがJSFというかRichFacesにべったり依存なのが気に食わない。もうちょっと頑張ったら汎用的な実装はできるかな?JPA用の実装はなんか調べてる途中に落ちてた気がするのできっと可能なんだろう。

package com.mycompany.lazypager;

import javax.faces.context.FacesContext;
import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;

public class PersonDataModel extends ExtendedDataModel<Person> {

    // 現在表示中のページのデータ
    private Person[] currentPageData;
    
    // 現在行を識別するためのキー
    private Integer rowKey;
    
    // ExtendedDataModel<T>で、int以外も行を識別できるようにするために追加されたメソッド…
    // 個人的にはいびつだと思う…。
    @Override
    public void setRowKey(Object o) {
        this.rowKey = (Integer)o;
    }
    @Override
    public Object getRowKey() {
        return this.rowKey;
    }

    // ここで必要なデータだけとって、DataVisitorにRowKeyを渡して処理をよろしくしてもらう
    @Override
    public void walk(FacesContext fc, DataVisitor dv, Range range, Object arg) {
        // Rangeから(SequenceRangeがわたってくるみたい)現在表示対象のデータを取得
        SequenceRange seq = (SequenceRange)range;
        this.currentPageData = Person.getRange(seq.getFirstRow(), seq.getRows());

        for (int i = 0; i < this.currentPageData.length; i++) {
            // 第二引数にrowKeyを渡す点がポイント
            // 今回は取得したデータのインデックス
            dv.process(fc, i, arg);
        }
    }

    @Override
    public boolean isRowAvailable() {
        // getRowDataで取得可能な現在行が有効かどうかを返すらしい。rowKeyがnullじゃなかったらOK
        return this.rowKey != null;
    }

    @Override
    public int getRowCount() {
        // データ件数
        return Person.getLength();
    }

    @Override
    public Person getRowData() {
        // rowKeyを元にデータを取得
        return this.currentPageData[this.rowKey];
    }

    // ここから下は実装しなくてもいい
    // 特にrowIndexはrowKeyにとってかわられたいらない子
    @Override
    public int getRowIndex() {
        return -1; // 何かしら返さないとだめみたい
    }

    @Override
    public void setRowIndex(int rowIndex) {
        throw new UnsupportedOperationException("not supported.");
    }

    @Override
    public Object getWrappedData() {
        // とりあえず使わない
        throw new UnsupportedOperationException("not supported.");
    }

    @Override
    public void setWrappedData(Object data) {
        // とりあえず使わない
        throw new UnsupportedOperationException("not supported.");
    }
    
}

管理ビーンを作る

もう一息です。管理ビーンを作ります。こいつは、単純にPersonDataModelを公開するだけの管理ビーンです。なんてことはないクラスですが、ManagedBeanやRequestScopeアノテーションはパッケージ名を確認して別の同名のアノテーションをつけないようにしましょう!

package com.mycompany.lazypager;

import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

@ManagedBean
@RequestScoped
public class IndexPage implements Serializable {
    private static final long serialVersionUID = 1L;

    private PersonDataModel people = new PersonDataModel();

    public PersonDataModel getPeople() {
        return people;
    }

    public void setPeople(PersonDataModel people) {
        this.people = people;
    }
}

ページ作成

あとはdataTableとdataScrollerを配置した画面を作って完了です。ポイントはdataTableにrowsプロパティできちんと1ページに表示する行を指定してやるところです。dataScrollerは、forプロパティでテーブルのidを指定します。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:rich="http://richfaces.org/rich"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head>
        <title>Lazy</title>
    </h:head>
    <h:body>
        <h:form>
            <rich:dataTable id="table" var="person" value="#{indexPage.people}" rows="10">
                <rich:column>
                    <f:facet name="header">
                        ID
                    </f:facet>
                    <h:outputText value="#{person.id}" />
                </rich:column>
                <rich:column>
                    <f:facet name="header">
                        Name
                    </f:facet>
                    <h:outputText value="#{person.name}" />
                </rich:column>
                <f:facet name="footer">
                    <rich:dataScroller for="table" />
                </f:facet>
            </rich:dataTable>
        </h:form>
    </h:body>
</html>

実行

実行してページ切り替えしたところです。キャプチャじゃわかりませんがサクサク動きます。