Seasar DI Container with AOP

S2Pagerとは

S2Daoを使ってページングをを実現する機能です。 S2Daoで検索した結果に対して、開始位置と最大取得件数を指定して結果の一部のみを取得することができます。 これにより、Googleの検索結果のように、大量の検索結果をページ単位で表示することが可能になります。
以前はS2PagerはS2Daoとは別のJARファイルとして配布されていましたが、 S2Dao1.0.31よりS2Dao本体に同梱されて配布されるようになりました。 1.0.31以降のバージョンではS2DaoのJARファイルのみでS2Pagerを使用することができます。

S2Pagerの使い方

S2Pagerを使用するには次の手順に従ってください。

  1. ページングの方法を選択します。
    ページングの方法として次の3つが用意されています。選択する方法に合わせてj2ee.dicon(Seasar2.4を利用の場合はjdbc.dicon)の設定を変更してください。 DBMSにより使用できる方法が限られます。
    1. ResultSetの機能を使用する方法(すべてのDBMSで利用可)
    2. Limit/Offset句を使用する方法(MySQLやPostgresSQLなどLimit/Offset句をサポートしたDBMSで利用可)
    3. ROWNUMを使用する方法(Oracleでのみ利用可)
  2. 検索条件に利用するDTOを作成します。
    このDTOはorg.seasar.dao.pager.PagerConditionインターフェイスをimplementsする必要があります。 DTOにはS2Pager用のプロパティ(offsetlimitcount)と、WHERE句に含める検索条件となるプロパティを保持してください。 offsetlimitcountのプロパティがあらかじめ実装されたorg.seasar.dao.pager.DefaultPagerConditionを継承して作成するのがよいでしょう。次のコードはDTOの作成例です。
    public class CategoryPagerCondition extends DefaultPagerCondition {
        private String category;
    
        public String getCategory() {
            return category;
        }
    
        public void setCategory(String category) {
            this.category = category;
        }
    }
  3. Daoのメソッドを定義します。
    上記で作成したDTOをパラメータとして受け取るメソッドをDaoに定義します。次のコードはDaoの作成例です。
    public interface BookDao {
        public Class BEAN = Book.class;
    
        public static final String findByCategoryPagerCondition_QUERY = "ORDER BY ID";
    
        public List findByCategoryPagerCondition(CategoryPagerCondition dto);
    }
  4. 開始位置(offset)と最大取得数(limit)をセットしてDaoのメソッドを呼び出します。
    次のコードはDaoのメソッドの呼び出し例です。
    CategoryPagerCondition condition = new CategoryPagerCondition();
    condition.setLimit(10);
    condition.setOffset(20);
    condition.setCategory("Java");
    List result = bookDao.findByCategoryPagerCondition(condition);

ページングの方法

使用するページングの方法ごとにj2ee.dicon(Seasar2.4を利用の場合はjdbc.dicon)を変更する必要があります。

ResultSetの機能を使用する方法(すべてのDBMSで利用可)

ResultSetのスクロール可能カーソル、もしくは空回しを使用することでページングを実現する方法です。
S2Daoの配布ファイルに含まれるj2ee.dicon(Seasar2.4を利用の場合はjdbc.dicon)ではこの方法を使用するための設定があらかじめ有効になっています。 したがって設定を変更しない限りこの方法が使用されます。

j2ee.diconの設定

...
<!-- 全DBMS対応のページング処理用設定 -->
<component 
    name="resultSetFactory"
    class="org.seasar.dao.pager.PagerResultSetFactoryWrapper">
    <arg>
        <component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
    </arg>
    <property name="useScrollCursor">true</property>
</component>
<component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
    <arg>
        <component class="org.seasar.dao.pager.PagerStatementFactory"/>
    </arg>
    <property name="fetchSize">100</property>
    <!--
    <property name="maxRows">100</property>
    -->
</component>
...

スクロール可能カーソル - スクロール可能カーソルのON/OFF

スクロール可能カーソルを使用することで、大量データ検索時のパフォーマンスが向上します。 ただし、DBやJDBCドライバの実装によっては、スクロール可能カーソルが使用できない場合があります。 その場合、上記のuseScrollCursorプロパティにfalseを指定してください。 ResultSetの空回しによりカーソルの移動が行われるようになります。

Limit/Offset句を使用する方法(MySQLやPostgresSQLなどLimit/Offset句をサポートしたDBMSで利用可)

DBMSのLimit/Offset句を使用する方法です。
この方法には推奨の設定と非推奨の2つ設定方法があります。非推奨の方法は1.0.45以前のバージョンとの互換性のためにあります。 非推奨の設定方法を使用した場合の問題点には次の2点があります。

  1. PreparedStatementが開放されない
  2. 実際には実行されないSQLがログ出力される

j2ee.diconの設定:LimitOffsetPagingSqlRewriterを使う場合(推奨)

この設定を有効にするには、他のページング用の設定はコメントアウトしてください。

...
<!-- LIMIT OFFSET句を使ったSQL書き換えによるページング処理用設定 -->
<component name="pagingSqlRewriter" class="org.seasar.dao.pager.LimitOffsetPagingSqlRewriter"/>
<component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
<component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
    <arg>
        <component class="org.seasar.extension.jdbc.impl.BasicStatementFactory"/>
    </arg>
    <property name="fetchSize">100</property>
    <!--
    <property name="maxRows">100</property>
    -->
</component>
...

この設定では、全件数を取得するSQLの実行後に、結果セットを取得するSQLを実行します。org.seasar.dao.pager.LimitOffsetPagingSqlRewriterのcountSqlCompatibilityプロパティでこのタイミングを変更できますが、通常この設定を変更することはありません。

MySQL用の設定(オプション)

バージョン1.0.50以降ではMySQL専用の設定が用意されています。この設定ではMySQLのFOUND_ROWS関数を使用することにより、標準設定よりもパフォーマンスの向上が期待できます。

...
<!-- LIMIT OFFSET句を使ったSQL書き換えによるページング処理用設定(MySQL用) -->
<component name="pagingSqlRewriter" class="org.seasar.dao.pager.MySQLLimitOffsetPagingSqlRewriter"/>
<component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
<component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
    <arg>
        <component class="org.seasar.extension.jdbc.impl.BasicStatementFactory"/>
    </arg>
    <property name="fetchSize">100</property>
    <!--
    <property name="maxRows">100</property>
    -->
</component>
...

この設定では、結果セットを取得するSQLの実行後に、全件数を取得するSQLを実行します。

j2ee.diconの設定:PagerResultSetFactoryLimitOffsetWrapperを使う場合(非推奨)

この設定を有効にするには、他のページング用の設定はコメントアウトしてください。

...
<!-- LIMIT OFFSET句を使ったSQL書き換えによるページング処理用設定(非推奨) -->
<component 
    name="resultSetFactory"
    class="org.seasar.dao.pager.PagerResultSetFactoryLimitOffsetWrapper">
    <arg>
        <component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
    </arg>
    <arg>dataSource</arg>
</component>
<component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
    <arg>
        <component class="org.seasar.dao.pager.PagerStatementFactory"/>
    </arg>
    <property name="fetchSize">100</property>
    <!--
    <property name="maxRows">100</property>
    -->
</component>
...

PagerResultSetFactoryLimitOffsetWrapperを使った場合に動作確認ができているJDBCドライバの種類とバージョンは次の表のとおりです。

DBの種類 動作確認されたJDBC Driverのバージョン 備考
MySQL mysql-connector-java-3.1.8 limitを使用する場合、MySQLはサブクエリーを使えるバージョン(4.1.x以降?)が必要になります。
PostgreSQL 8.1-404-jdbc3 接続文字列にオプション「protocolVersion=2」を付ける必要があります。
jdbc:postgresql://localhost:5432/postgres?protocolVersion=2
通常、PostgreSQL の Version8 のドライバを使用する際、それまでのバージョ ンと比べ、型などについての挙動がかなり異なる為、オプション「protocolVersion=2」を付けて利用します。 オプション「protocolVersion=2」がない場合、PareparedStatement.toStringメソッドの挙動が変わるため、 S2Pagerは動作しません。

実行されるSQL

「SELECT * FROM DEPARTMENT ORDER BY DEPTNO」というSQLが次のように書き換えられて実行されます。

SELECT * FROM DEPARTMENT ORDER BY DEPTNO LIMIT 10 OFFSET 55

ROWNUMを使用する方法(Oracleでのみ利用可)

OracleのROWNUMを使用する方法です。

j2ee.diconの設定

この設定を有効にするには、他のページング用の設定はコメントアウトしてください。

...
<!-- ORACLE ROWNUM句を使ったSQL書き換えによるページング処理用設定 -->
<component name="pagingSqlRewriter" class="org.seasar.dao.pager.OracleRownumPagingSqlRewriter"/>
<component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
<component class="org.seasar.extension.jdbc.impl.ConfigurableStatementFactory">
    <arg>
        <component class="org.seasar.extension.jdbc.impl.BasicStatementFactory"/>
    </arg>
    <property name="fetchSize">100</property>
    <!--
    <property name="maxRows">100</property>
    -->
</component>
...

実行されるSQL

「SELECT * FROM DEPARTMENT ORDER BY DEPTNO」というSQLが次のように書き換えられて実行されます(見やすくするために改行を加えています)。

SELECT * FROM 
  (SELECT ROWNUM AS S2DAO_ROWNUMBER, S2DAO_ORIGINAL_DATA.* FROM 
    (SELECT * FROM DEPARTMENT ORDER BY DEPTNO) S2DAO_ORIGINAL_DATA) 
WHERE 
  S2DAO_ROWNUMBER BETWEEN 56 AND 65 
  AND 
  ROWNUM <= 10 
ORDER BY 
  S2DAO_ROWNUMBER

この設定では、全件数を取得するSQLの実行後に、結果セットを取得するSQLを実行します。org.seasar.dao.pager.OracleRownumPagingSqlRewriterのcountSqlCompatibilityプロパティでこのタイミングを変更できますが、通常この設定を変更することはありません。

S2Pagerのユーティリティクラス群

S2Pagerにはデータベースアクセスとは離れたレイヤでページングをサポートするいくつかのユーティリティクラスを持ちます。これらのクラスの使用例はサンプルに含まれています。

PagerSupport - セッションへの検索条件の格納をサポート

通常、検索条件オブジェクトはHttpSessionのアトリビュートに格納します。 S2Pagerでは検索条件オブジェクトのHttpSessionへの格納などをサポートする ユーティリィ的なクラスとしてorg.seasar.dao.pager.PagerSupportクラスを用意しています。

PagerSupportクラスのコンストラクタで次の項目を指定します。

引数 意味 説明
第1引数 最大取得数 PagerConditionのlimitに使用されます。
第2引数 条件保持DTOのクラスオブジェクト セッション中に条件保持DTOが存在しかった場合、ここで指定したクラスの検索条件DTOが生成されます。
第3引数 属性名 セッションの属性名を指定します。ここで指定した名前で検索条件DTOがセッション中に格納されます。
        /** ページャサポートクラス */
        private PagerSupport pager = 
                new PagerSupport(10, CategoryPagerCondition.class, "categoryPagerCondition");

セッション中の検索条件DTOの取得開始位置(offset)の更新は、パラメータ名"offset"が取得開始位置を意味する場合次のコードで可能です。

        // パラメータoffsetを元にページャのoffset位置を更新
        pager.updateOffset(request);

また、任意のパラメータ名で更新をしたい場合、次のようなコードになります。

        // パラメータ"hoge"を元にページャのoffset位置を更新
        pager.updateOffset(request, "hoge");

セッション中の検索条件DTOを取得するには、次のようなコードになります。

        // ページャの条件保持オブジェクトをセッションから取得
        // 存在しない場合は、PagerSupportのコンストラクタで
        // 渡されたクラスが新規に作成されます。
        CategoryPagerCondition dto = 
            (CategoryPagerCondition) pager.getPagerCondition(request);

以上のPagerSupportの使い方をまとめると、次のようなコードになります。 Actionクラスのインスタンス変数としてPagerSupportを保持していますが、 PagerSupportはスレッドセーフです。複数ユーザで共有して安全に使用することができます。 ただし、最大取得数(limit)をユーザごとに切り替えたい場合は、PagerSupport自体をセッションに格納するなど工夫が必要でしょう。

public BookAction extends Action {
        /** ページャサポートクラス */
        private PagerSupport pager =
                new PagerSupport(10, CategoryPagerCondition.class, "categoryPagerCondition");
        
        private BookDao bookDao;
        
        public void setBookDao(BookDao bookDao) {
                this.bookDao = bookDao;
        }
        
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // パラメータoffsetを元にページャのoffset位置を更新
        pager.updateOffset(request);
        
        // ページャの条件保持オブジェクトをセッションから取得
        // 存在しない場合は、PagerSupportのコンストラクタで
        // 渡されたクラスが新規に作成されます。
        CategoryPagerCondition dto = 
            (CategoryPagerCondition) pager.getPagerCondition(request);
        
        // 条件保持オブジェクト中の独自の検索条件をセット
        // この場合、書籍カテゴリを表すcateogry
        String category = request.getParameter("category");
        if (category != null && category.length() != 0) {
            dto.setCategory(category);
        }
        
        // ページャ対応の検索を実行
        List books = bookDao.findByCategoryPagerCondition(dto);
        request.setAttribute("books", books);
        
        // 結果ページにフォワード
        request.getRequestDispatcher("/result.jsp").forward(request, response);
        
    }
}

PagerViewHelper - ビューの作成を助ける

S2Pagerではページリンクを表示するビューの部品は用意していません。 これは、ページリンクの表示形式は、プロジェクトによって異なるためです。 ただ、サンプルは用意しています。 サンプルではJSTL版とJSP2.0のタグファイルによるページリンクの2パターンがあります。 カスタムタグでのサンプルはありません。作成された方はソースの提供お待ちしております;-)

また、PagerConditionの情報を元にビューでリンクを生成するためのビューヘルパークラスとして、 org.seasar.dao.pager.PagerViewHelperクラスがあります。 PagerViewHelperクラスを使うとビューで(特にJSTLのELやVelocityで)ページリンクを作成するのが楽になります。

PagerViewHelperは以下のメソッドを持っています。
/**
 * ページャのビューヘルパークラスです。
 * @author Toshitaka Agata
 */
public class PagerViewHelper implements PagerCondition {
        // コンストラクタ
    public PagerViewHelper(PagerCondition condition) { ... }
    public PagerViewHelper(PagerCondition condition, int displayPageMax) { ... }

        // PagerConditionへの委譲メソッド
    public int getCount() { ... }
    public void setCount(int count) { ... }
    public int getLimit() { ... }
    public void setLimit(int limit) { ... }
    public int getOffset() { ... }
    public void setOffset(int offset) { ... }

        // ヘルパーメソッド

    /**
     * 前へのリンクが表示できるかどうかを判定します。
     * @param ture/false
     */
    public boolean isPrev() { ... }

    /**
     * 次へのリンクが表示できるかどうかを判定します。
     * @param ture/false
     */
    public boolean isNext() { ... }

    /**
     * 現在表示中の一覧の最後のoffsetを取得します。
     * @param 現在表示中の一覧の最後のoffset
     */
    public int getCurrentLastOffset() { ... }

    /**
     * 次へリンクのoffsetを返します。
     * @return 次へリンクのoffset
     */
    public int getNextOffset() { ... }

    /**
     * 前へリンクのoffsetを返します。
     * @return 前へリンクのoffset
     */
    public int getPrevOffset() { ... }

    /**
     * 現在ページのインデックスを返します。
     * @return 現在ページのインデックス
     */
    public int getPageIndex() { ... }

    /**
     * 現在ページのカウント(インデックス+1)を返します。
     * @return 現在ページのカウント(インデックス+1)
     */
    public int getPageCount() { ... }
    
    /**
     * 最終ページのインデックスを返します。
     * @return 最終ページのインデックス
     */
    public int getLastPageIndex() { ... }

    /**
     * ページリンクの表示上限を元に、ページ番号リンクの表示開始位置を返します。
     * @return ページ番号リンクの表示開始位置
     */
    public int getDisplayPageIndexBegin() { ... }

    /**
     * ページリンクの表示上限を元に、ページ番号リンクの表示終了位置を返します。
     * @return ページ番号リンクの表示終了位置
     */
    public int getDisplayPageIndexEnd() { ... }

以下はサンプルにあるJSTLによるページリンクの実装例です。
呼び出し側:result.jsp

<!--ページャー(件数表示あり)-->
<c:import url="tags/pager.jsp">
        <c:param name="condition" value="categoryPagerCondition"/>・・・検索条件DTOのセッション中の属性名を指定
</c:import>
ページャリンクの部品:pager.jsp
<%@ page contentType="text/html; charset=Windows-31J" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%
        // パラメータの取得
        String conditionName = request.getParameter("condition");
        String counter = request.getParameter("counter");
        String href = request.getParameter("href");
        
        // PagerViewHelperの作成
        org.seasar.dao.pager.PagerCondition condition =
                (org.seasar.dao.pager.PagerCondition)pageContext.findAttribute(conditionName);
        org.seasar.dao.pager.PagerViewHelper helperCondition = 
                new org.seasar.dao.pager.PagerViewHelper(condition);

        // 属性をセット
        pageContext.setAttribute("counter", counter);
        pageContext.setAttribute("href", href);
        pageContext.setAttribute("helperCondition", helperCondition);
%>


<%-- メイン --%>
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<c:if test="${counter != 'false'}">
        <td width="200" align="left" valign="center">
                該当件数:<c:out value="${helperCondition.count}"/>件
                (<c:out value="${helperCondition.offset + 1}"/>
                -<c:out value="${helperCondition.currentLastOffset + 1}"/>)             
        </td>
</c:if>

        <td align="right" valign="center">
<c:if test="${helperCondition.prev}">
                <a href="<c:out value="${href}"/>?offset=
                <c:out value="${helperCondition.prevOffset}"/>">< 前の
                <c:out value="${helperCondition.limit}"/>件</a>
</c:if>
                         
<c:forEach begin="0"
        end="${helperCondition.lastPageIndex}"
        step="1"
        varStatus="status">
        <c:if test="${status.index != helperCondition.pageIndex}">
                         <a href="<c:out value="${href}"/>?offset=
                        <c:out value="${status.index * helperCondition.limit}"/>">
                        <c:out value="${status.count}"/></a>
        </c:if>
        <c:if test="${status.index == helperCondition.pageIndex}">
                         [<c:out value="${status.count}"/>]
        </c:if>
</c:forEach>
                           
<c:if test="${helperCondition.next}">
                <a href="<c:out value="${href}"/>?offset=
                <c:out value="${helperCondition.nextOffset}"/>">次の
                <c:out value="${helperCondition.limit}"/>件 ></a></td>
</c:if>
<c:if test="${!helperCondition.next}">
                     
</c:if>

</tr>
</table>

PagerUtil - S2Daoを使わないList,Colllecitonに対するページング

S2Daoを使わないList,Colllecitonに対してページングを行いたいときはPagerUtil#filterを使用します。

List list = getItems(); // フィルタリング前のリスト
DefaultPagerCondition condition = new DefaultPagerCondition();
condition.setLimit(10);
condition.setOffset(10);
List result = PagerUtil.filter(list, condition);
System.out.println(result.size()); // 10
System.out.println(condition.getCount()); // 35

サンプル

S2PagerのサンプルはJSP2.0で動作します。以下の手順でTomcat5.0.xなどJSP2.0をサポートするServletコンテナ上で動作させる必要があります。

  1. s2-dao-pager-examples-x.x.x.zipを展開し、s2dao-pager-examples.warをTomcat5.0.xのwebappsディレクトリに配置します。
  2. Tomcat5.0.xを起動します。
  3. http://localhost:8080/s2dao-pager-examples/をブラウザで開きます。(Tomcatが8080ポートで動作している場合)

Tomcatプラグインを導入している環境であれば、EclipseのTomcatPlugin上でそのまま動作させることもできます。