Seasar DI Container with AOP

S2Pagerとは

S2Daoを使ってページャを実現する機能です。 S2Daoで検索した結果に対して、開始位置と最大取得件数を指定して結果の一部のみを取得することができます。 これにより、Googleの検索結果のように、大量の検索結果をページ単位で表示することが可能になります。

セットアップ

サンプルの実行

dao.diconの修正

S2Pagerを既存のプロジェクトで使用するには、S2Daoのdao.diconをS2Pager用に修正する必要があります。
S2Daoディレクトリ/src/s2dao.php5/dao-pager.diconを参考にして以下のようにdao.diconを修正します。 (dao-pager.diconをdao.diconにリネームする方法が簡単です。)

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components21.dtd">
<components namespace="dao">

    <include path="%PDO_DICON%" />

    <component class="S2Dao_BasicResultSetFactory" />
    <component class="S2Dao_BasicStatementFactory" />
    <component class="S2Dao_FieldAnnotationReaderFactory" />
    <component class="S2Dao_DaoMetaDataFactoryImpl" />
    <component name="interceptor" class="S2Dao_PagerS2DaoInterceptorWrapper">
      <property name="useLimitOffsetQuery">false</property>
    </component>

</components>

S2Pagerの使い方

S2Dao_PagerCondition - 検索条件を保持する

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

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

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

通常、検索条件オブジェクトはsession変数に格納します。 S2Pagerでは検索条件オブジェクトのsessionへの格納などをサポートする ユーティリティ的なクラスとしてS2Dao_PagerSupportクラスを用意しています。

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

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

セッション中の検索条件DTOの取得開始位置(offset)の更新は、次のコードで可能です。

        // ページャのoffset位置を更新
        $pager->updateOffset($_REQUEST['offset']);

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

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

以上のS2Dao_PagerSupportの使い方をまとめると、次のようなコードになります。

<?php

/** ページャサポートクラス */
$pager = new S2Dao_PagerSupport(10, 'CategoryPagerCondition', 'categoryPagerCondition');

// パラメータoffsetを元にページャのoffset位置を更新
$pager->updateOffset($_REQUEST['offset']);

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

// 条件保持オブジェクト中の独自の検索条件をセット
// この場合、書籍カテゴリを表すcateogry
$category = $_REQUEST["category"];
if (isset($category)) {
    $dto->setCategory($category);
}

// ページャ対応の検索を実行
$books = $dao->findByCategoryPagerCondition($dto);

?>

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

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

PagerViewHelperは以下のメソッドを持っています。

<?php
/**
 * ページャViewのヘルパークラス
 * @author yonekawa
 */
class S2Dao_PagerViewHelper implements S2Dao_PagerCondition
{
    /** 画面上でのページの最大表示件数のデフォルト  */
    const DEFAULT_DISPLAY_PAGE_MAX = 9;

    /** 検索条件オブジェクト */
    private $condition;

    /** 画面上でのページの最大表示件数 */
    private $displayPageMax;

    public function __construct($condition, $displayPageMax = null)
    {
        $this->condition = $condition;

        if (isset($displayPageMax)) {
            $this->displayPageMax = $displayPageMax;
        } else {
            $this->displayPageMax = self::DEFAULT_DISPLAY_PAGE_MAX;
        }
    }

    public function getCount(){ ... }
    public function setCount($count){ ... }

    public function getLimit()
    {
        return $this->condition->getLimit();
    }

    public function setLimit($limit)
    {
        $this->condition->setLimit($limit);
    }

    public function getOffset()
    {
        return $this->condition->getOffset();
    }

    public function setOffset($offset)
    {
        $this->condition->setOffset($offset);
    }

    /**
     * 前へのリンクが表示できるかどうかを判定します。
     */
    public function isPrev()
    {
        return 0 < $this->condition->getOffset();
    }

    /**
     * 次へのリンクが表示できるかどうかを判定します。
     */
    public function isNext()
    {
        $count = $this->condition->getCount();
        $nextOffset = $this->condition->getOffset() + $this->condition->getLimit();

        return 0 < $count && $nextOffset < $count;
    }

    /**
     * 現在表示中の一覧の最後のoffsetを取得します。
     */
    public function getCurrentLastOffset()
    {
        $count = $this->condition->getCount();
        $nextOffset = $this->getNextOffset($this->condition);
        if ($nextOffset <= 0 || $this->condition->getCount() <= 0) {
            return 0;
        } else {
            return $nextOffset < $count ? $nextOffset - 1 : $count - 1;
        }
    }

    /**
     * 次へリンクのoffsetを返します。
     */
    public function getNextOffset()
    {
        return $this->condition->getOffset() + $this->condition->getLimit();
    }

    /**
     * 前へリンクのoffsetを返します。
     */
    public function getPrevOffset()
    {
        $prevOffset = $this->condition->getOffset() - $this->condition->getLimit();
        return $prevOffset < 0 ? 0 : $prevOffset;
    }

    /**
     * 現在ページのインデックスを返します。
     */
    public function getPageIndex()
    {
        $limit = $this->condition->getLimit();
        $offset = $this->condition->getOffset();
        if ($limit == 0) {
            return 1;
        } else {
            return floor($offset / $limit);
        }
    }

    /**
     * 現在ページのカウント(インデックス+1)を返します。
     */
    public function getPageCount()
    {
        return $this->getPageIndex() + 1;
    }

    /**
     * 最終ページのインデックスを返します。
     */
    public function getLastPageIndex()
    {
        $limit = $this->condition->getLimit();
        $count = $this->condition->getCount();
        if ($limit == 0) {
            return 0;
        } else {
            return floor(($count - 1) / $limit);
        }
    }

    /**
     * ページリンクの表示上限を元に、ページ番号リンクの表示開始位置を返します。
     */
    public function getDisplayPageIndexBegin()
    {
        $lastPageIndex = $this->getLastPageIndex();
        if ( $lastPageIndex < $this->displayPageMax ) {
            return 0;
        } else {
            $currentPageIndex = $this->getPageIndex();
            $displayPageIndexBegin = $currentPageIndex - (floor($this->displayPageMax / 2));
            return $displayPageIndexBegin < 0 ? 0 : $displayPageIndexBegin;
        }
    }

    /**
     * ページリンクの表示上限を元に、ページ番号リンクの表示終了位置を返します。
     */
    public function getDisplayPageIndexEnd()
    {
        $lastPageIndex = $this->getLastPageIndex();
        $displayPageIndexBegin = $this->getDisplayPageIndexBegin();
        $displayPageRange = $lastPageIndex - $displayPageIndexBegin;
        if ($displayPageRange < $this->displayPageMax) {
            return $lastPageIndex;
        } else {
            return $displayPageIndexBegin + $this->displayPageMax - 1;
        }
    }

}

?>

以下はサンプルのページリンクの実装例です。
ページリンクの実装:pager.php

<?php

$container = S2ContainerFactory::create('resource/example.dicon.xml');
$dao = $container->getComponent('BooksDao');

$dto = new S2Dao_DefaultPagerCondition();

$dto->setOffset(3);
$dto->setLimit(5);

$books = $dao->getByPagerDtoList($dto);
$helper = new S2Dao_PagerViewHelper($dto);

?>
<html>
<body>

<?php

// 前の○件を表示
if ($helper->isPrev()) {
    print( '<a href="pager.php?offset=' . ( $helper->getOffset() + 1 ). '">' );
    print('前の' . $helper->getLimit() . '件');
    print( '>' );
}

// ページ番号リンクを表示
for ( $i = $helper->getDisplayPageIndexBegin(); $i <= $helper->getDisplayPageIndexEnd(); $i++ ) {
    if ( $i == $helper->getPageIndex() ) {
        print($i + 1);
    } else {
        print('<a href="pager.php?offset=' . $i * $helper->getLimit() . '">' . ( $i + 1 ) . '</a> ');
    }
}

// 次の○件を表示
if ( $helper->isNext() ) {
    print( '<a href="pager.php?offset=' . ( $helper->getOffset() - 1 ). '">' );
    print( ' 次の' . $helper->getLimit() . '件' );
    print( '>' );
}
?>

<table>
<tr>
<td>ID</td><td>TITLE</td><td>CONTENT</td>
</tr>

<?php
foreach ($books as $book) {
    print( '<tr>' );
    print( '<td>' . $book->getId() . '</td>' );
    print( '<td>' . $book->getTitle() . '</td>' );
    print( '<td>' . $book->getContent() . '</td>' );
    print( '</tr>' );
}
?>
</table>
</body>
</html>

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

PosgreSQLやMySQLのように「limit offset」が使用できるDBMSでは、大量データ検索時のパフォーマンスが大幅に向上します。
以下の設定によりS2Dao_PagerS2DaoInterceptorWrapperのuseLimitOffsetQueryプロパティをtrueにすることで 「limit offset」を使用した取得が可能になります。

limitとoffsetを使用した高速取得の設定(dao-pager.dicon)

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components21.dtd">
<components namespace="dao">

    <include path="%PDO_DICON%" />

    <component class="S2Dao_BasicResultSetFactory" />
    <component class="S2Dao_BasicStatementFactory" />
    <component class="S2Dao_FieldAnnotationReaderFactory" />
    <component class="S2Dao_DaoMetaDataFactoryImpl" />
    <component name="interceptor" class="S2Dao_PagerS2DaoInterceptorWrapper">
      <property name="useLimitOffsetQuery">true</property>
    </component>

</components>

全件数取得時のorder by句削除のON/OFF

S2Dao.PHP5-1.1.3から、S2Pager内部で発行する全件数を取得するSQLで、元のSQLからorder by句を削除することで、さらに高速にページングできるようになりました。これは、全件数を取得するのに並び順は関係がないため、order by句を削除することでSQLのコストを小さくしています。
もしorder by句の削除を行いたくない場合や問題がある場合は、S2Dao_PagerS2DaoInterceptorWrapperの「chopOrderBy」プロパティをfalseにすることで、以前と同様にorder by句を削除せずに全件数を取得するSQLを発行することもできます。(デフォルトはtrueでorder by句が削除されます)

<component name="interceptor" class="S2Dao_PagerS2DaoInterceptorWrapper">;
  <property name="useLimitOffsetQuery">true</property>
  <property name="isChopOrderBy">true</property>
</component>