Seasar DI Container with AOP

S2Pagerとは

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

セットアップ

サンプルの実行

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上でそのまま動作させることもできます。

j2ee.dicon(jdbc.dicon)の修正

S2Pagerを既存のプロジェクトで使用するには、Seasar2のj2ee.dicon(Seasar2.4ではjdbc.dicon)をS2Pager用に修正する必要があります。 s2-dao-x.x.x.zipに含まれるj2ee.diconを参考にして以下のようにj2ee.dicon(jdbc.dicon)を修正します。

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR2.1//DTD S2Container//EN"
	"http://www.seasar.org/dtd/components21.dtd">
<components namespace="j2ee">
	:
	:
	<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 
		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>

	<!--
	<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>
	<component class="org.seasar.extension.jdbc.impl.OracleResultSetFactory"/>
	<component class="org.seasar.extension.jdbc.impl.BooleanToIntStatementFactory"/>
	-->
	:
	:

S2Pagerの使い方

PagerCondition - 検索条件を保持する

ページャ機能は次の手順で実現します。

  1. PagerConditionインターフェイスをimplementsする検索条件DTOを作成します。 デフォルトの実装として、org.seasar.dao.pager.DefaultPagerConditionクラスが用意されています。 検索条件DTOはS2Pager用のプロパティ(offset,limit,count)と、検索条件(下記の例ではcategory)を保持します。
    /**
     * ページャ条件オブジェクトのインターフェイス
     * @author Toshitaka Agata
     */
    public interface PagerCondition {
        
        public static final int NONE_LIMIT = -1;
        
        /**
         * 検索結果の総件数を取得します。
         * @return 総件数
         */
        public int getCount();
        
        /**
         * 検索結果の総件数をセットします。
         * @param count 総件数
         */
        public void setCount(int count);
        
        /**
         * 検索結果から一度に取得する最大件数を取得します。
         * @return 最大件数
         */
        public int getLimit();
        
        /**
         * 検索結果から一度に取得する最大件数をセットします。
         * @param limit 最大件数
         */
        public void setLimit(int limit);
        
        /**
         * 検索結果の取得開始位置ををセットします。
         * @param offset 取得開始位置
         */
        public void setOffset(int offset);
    
        /**
         * 検索結果の取得開始位置をを取得します。
         * @return 取得開始位置
         */
        public int getOffset();
    }
    
    /**
     * 検索条件DTO。
     * 独自の検索条件はこのクラスのようにPagerConditionインターフェイスを実装する
     * クラスで実現します。通常はDefaultPagerConditionを継承するとよいでしょう。
     * @author agata
     */
    public class CategoryPagerCondition extends DefaultPagerCondition {
        /** カテゴリー(検索条件) */
        private String category;
        public String getCategory() {
            return category;
        }
        public void setCategory(String category) {
            this.category = category;
        }
    }
    
  2. 検索条件DTOを引数に持つDaoの検索メソッドを作成します。
    public interface BookDao {
        public List findByCategoryPagerCondition(CategoryPagerCondition dto);
    }
    
  3. 検索条件DTOに開始位置(offset)と最大取得数(limit)をセットしてDaoのメソッドを呼び出します。
    
    // offsetとlimitをセット
    CategoryPagerCondition dto = new CategoryPagerCondition();
    dto.setLimit(10);
    String offsetStr = request.getParameter("offset");
    dto.setOffset(Integer.parseInt(offset));
    
    // 独自の検索条件
    String category = request.getParameter("category");
    if (category != null && category.length() != 0) {
        dto.setCategory(category);
    }
    
    // ページャ対応の検索を実行
    BookDao bookDao = (BookDao) container.getComponent(BookDao.class);
       List books = bookDao.findByCategoryPagerCondition(dto);
    request.setAttribute("books", books);
    
    // countには検索結果の総数がセットされている。
    System.out.println("count=" + dto.getCount());
    
    

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

limitとoffsetを使用した高速取得

PosgreSQLやMySQLのように「limit offset」が使用できるDBMSでは、大量データ検索時のパフォーマンスが大幅に向上します。 現在までに動作が確認できているのはPostgreSQL、MySQLの以下のJDBCドライバのバージョンです。

動作確認されたJDBC Driverのバージョン

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は動作しません。

以下の設定により「PagerResultSetFactoryLimitOffsetWrapper」のコメントアウトを解除することで、「limit offset」を使用した取得が可能になります。

limitとoffsetを使用した高速取得の設定(j2ee.dicon)
	:
	:

	<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 
		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>

	<!--
	<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>
	<component class="org.seasar.extension.jdbc.impl.OracleResultSetFactory"/>
	<component class="org.seasar.extension.jdbc.impl.BooleanToIntStatementFactory"/>
	-->
	:
	:

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

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

スクロール可能カーソルをOFFにする設定(j2ee.dicon)

	:
	:
	<component 
		name="resultSetFactory"
		class="org.seasar.dao.pager.PagerResultSetFactoryWrapper">
		<arg>
			<component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
		</arg>
		<!--
		<property name="useScrollCursor">true</property>
		-->
		<property name="useScrollCursor">false</property>
	</component>
	<!--
	<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>
			<!-- JDBCドライバなどの環境によってはPagerStatementFactoryを有効にする必要があるようです。 -->
			<!--
			<component class="org.seasar.dao.pager.PagerStatementFactory"/>
			-->
			<component class="org.seasar.extension.jdbc.impl.BasicResultSetFactory"/>
		</arg>
		<property name="fetchSize">100</property>
		<!--
		<property name="maxRows">100</property>
		-->
	</component>

	<!--
	<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>
	<component class="org.seasar.extension.jdbc.impl.OracleResultSetFactory"/>
	<component class="org.seasar.extension.jdbc.impl.BooleanToIntStatementFactory"/>
	-->
	:
	:
全件数取得時のorder by句削除のON/OFF
 S2Dao-1.0.43から、S2Pager内部で発行する全件数を取得するSQLで、元のSQLからorder by句を削除することで、さらに高速にページングできるようになりました。 これは、全件数を取得するのに並び順は関係がないため、order by句を削除することでSQLのコストを小さくしています。
 もしorder by句の削除を行いたくない場合や問題がある場合は、「org.seasar.dao.pager.PagerResultSetFactoryLimitOffsetWrapper」の「chopOrderBy」プロパティをfalseにすることで、 以前と同様にorder by句を削除せずに全件数を取得するSQLを発行することもできます。(デフォルトはtrueでorder by句が削除されます)
	:
	:
	<component 
		name="resultSetFactory"
		class="org.seasar.dao.pager.PagerResultSetFactoryWrapper">
		:
		:
		<property name="chopOrderBy">false</property>
	</component>
	:
	: