limitメソッドとLimitedViewクラスの実装

limitメソッドは、SQLのLIMITやOFFSET*1相当の機能を実現するためのメソッドです。今回は、limitメソッドとそれを実現するLimitedViewクラスの実装について書きます。

例で使用するテーブル

これまで同様、次のようなテーブルがあるものとして話を進めます。

members: メンバーを格納したテーブル
member_id family_name first_name age sex group_id
1011 中田 祐輔 22 0 1
1234 山下 沙希 17 1 2
1999 宮川 18 0 2
2112 石田 菜々 21 1 1
2345 山崎 千佳 19 1 1
PerlShellKoherent::DBでのテーブルの取得

次のようにして、$membersがTableオブジェクトを事前に保持しているものとします。

my $members = $database->table('members');

limitメソッドの実装

SELECTのAPI(2) 様々なSELECT文への対応に書いたように、検索結果のリミット(件数制限)やオフセットを行うには、limitメソッドを用いて次のようにします。

SELECT * FROM members LIMIT 3;
SELECT * FROM members LIMIT 2, 3; -- LIMIT 3 OFFSET 2
my $result;
$result = $members->limit(3);
$result = $members->limit(2, 3); # LIMIT 3 OFFSET 2

また、LIMITやOFFSETはORDER BYと共に用いられることが多いですが、これは次のようになります。

SELECT * FROM members ORDER BY first_name LIMIT 1, 2;
$result = $members->order_by('first_name')->limit(1, 2);

whereメソッドやorder_byメソッドと違いコード・リファレンスを渡す必要もないため、limitメソッドの実装は単純です。気をつけなければならないのは、与えられた引数が一つかそれより多いかで第1引数がリミットを表すかオフセットを表すかを決定しなければならない点のみです(第1引数がオフセット、第2引数がリミットという順はMySQLのLIMITに倣いました)。limitメソッドは、それを実現するViewクラス(のサブクラス)であるLimitedViewクラスのインスタンスを返します。

コードを書くと次のようになります。例によって関係ない部分は省略しています。

package Koherent::DB::View;

# 省略

use Koherent::DB::LimitedView;

# 省略

sub limit{
    my $self = shift;

    my $limit;
    my $offset;

    croak 'No arguments.' if @_ < 1;

    if(@_ == 1){
        # 第1引数のみ与えられた場合はLIMITのみ
        $limit = shift;
        $offset = 0;
    }else{
        # 引数が2個以上の場合は第1引数はOFFSET、第2引数がLIMIT(第3引数以降は無視)
        ($offset, $limit) = @_;
        croak "OFFSET is undefined." unless defined($offset);
    }
    croak "LIMIT if undefined." unless defined($limit);

    Koherent::DB::LimitedView->new($self, $offset, $limit);
}

# 省略

limitメソッドではオフセットのみを設定することができないのでoffsetメソッドも実装します。なお、LimitedViewクラスのコンストラクタに第2引数を渡さなかった場合はオフセットのみを行うLimitedViewオブジェクトが生成されるものとします。

# 省略

sub offset{
    my $self = shift;
    my $offset = shift;

    croak "OFFSET is undefined." unless defined($offset);;

    Koherent::DB::LimitedView->new($self, $offset);
}

# 省略

ただし、limitメソッドとoffsetメソッドを組み合わせて使うと効率が悪いので、リミットとオフセットの両方を設定する際はlimitメソッド一つで済ませるべきですす。

# 悪い例
$result = $members->limit(2)->offset(3);
# 良い例
$result = $members->limit(3, 2);

LimitedViewクラスの実装

LimitedViewクラスは、基本的にLimitedViewIteratorクラスに処理を丸投げするだけです。ConditionedViewクラスやOrderedViewクラス同様LimitedViewクラスもViewクラスを継承しているため、limitの戻り値であるLimitedViewオブジェクトに対してwhere、order_by、limit、offsetなどのメソッドを呼び出すことができます。

LimitedViewクラスのコードは次のようになります。

package Koherent::DB::LimitedView;

# 省略

use base qw(Koherent::DB::View);
use Koherent::DB::LimitedViewIterator;

sub new{
    my $class = shift;
    my $view = shift;
    my $offset = shift;
    my $limit = shift;

    my $self = $class->SUPER::new;
    $self->{'view'} = $view;
    $self->{'offset'} = $offset;
    $self->{'limit'} = $limit;

    $self;
}

sub iterator{
    my $self = shift;

    Koherent::DB::LimitedViewIterator->new($self);
}

# 省略

LimitedViewIteratorクラスの実装

リミットやオフセットの処理はLimitedViewIteratorクラスで実現します。

リミットとオフセットの処理は単純です。limitメソッドの呼び出し元のViewオブジェクトからイテレータを取得し、初めにオフセットで設定された行数分だけ取得結果を捨てます。その後リミットで設定された行数分のみ結果を返します。具体的には、nextメソッドが呼び出された回数を変数に記録しておき、それがリミットで設定された回数以上になると値を返さないようにします。

LimitedViewIteratorクラスの実装上気をつけなければならないことは、リミットで指定した行数分だけのデータを呼び出した時点で、元のイテレータにも終了を通知しなければならないということです。例えば、元のイテレータがファイルからデータを読み出している場合、limitメソッドでデータ取得が打ち切られると元のイテレータはファイルを開きっぱなしになってしまいます。

このようなケースに適切に対処するために、ViewIteratorクラスにはendメソッドを用意しています(ViewIteratorクラスのデストラクタでもendメソッドが呼ばれる仕組みになっています)。endメソッドの処理はクラスによって異なりますが、LimitedViewIteratorクラスでは元イテレータのendメソッドを呼び出し、データ読み出しの終了を通知します。

LimitedViewIteratorクラスのコードは次のようになります。

package Koherent::DB::LimitedViewIterator;

# 省略

use base qw(Koherent::DB::ViewIterator);

sub new{
    my $class = shift;
    my $view = shift;

    my $original_iterator = $view->{'view'}->iterator;
    my $offset = $view->{'offset'};

    # 最初の$offset行分をスキップ
    if(defined($offset)){
        for(1..$offset){
            last unless $original_iterator->next;
        }
    }

    my $self = {
        view => $view,
        original_iterator => $original_iterator,
        # 何行読み出したかのカウント
        count => 0,
    };

    bless $self, $class;
}

sub next{
    my $self = shift;

    # 読み出し終了済の場合は値を返さない。
    return if $self->{'ended'};

    my $original_iterator = $self->{'original_iterator'};
    my $count = $self->{'count'};
    my $limit = $self->{'view'}->{'limit'};

    $self->{'count'} = ++$count;

    if(!defined($limit) || $count <= $limit){
        # $limitが設定されていないか
        # まだ$limit行分データを読み出していない場合のみ
        # 次の行を読み出して返す。
        $original_iterator->next;
    }else{
        # 読み出しを終了
        $self->end;
        return;
    }
}

sub end{
    my $self = shift;

    return if $self->{'ended'};

    # 読み出し終了済のフラグを設定
    $self->{'ended'} = 1;

    # limitメソッド呼び出し元Viewオブジェクトのイテレータに
    # 終了を通知する。
    my $original_iterator = $self->{'original_iterator'};
    $original_iterator->end if $original_iterator;
}

# 省略

まとめ

SQLのLIMITおよびOFFSETに相当するlimitメソッドおよびoffsetメソッドを実装しました。

whereメソッド同様、limitメソッド(とoffsetメソッド)、LimitedViewクラス、LimitedViewIteratorクラスをセットで実装しました。limitメソッドをorder_byメソッドとセットで使用することによって、任意の順序でソートされたデータの、任意の位置から任意の位置までを取得することが可能となりました。

*1:正確には、MySQLPostgreSQLのLIMITやOFFSETです。標準SQLでは最近になってLIMIT、OFFSET相当の構文が定義されたようですが、現時点で一般的ではありません。