whereメソッドとConditionedViewクラスの実装

友人の結婚式に出るために関西に帰ったり、Module::Starterを触ったり、PostgreSQLトランザクションの実装を調べたりしている間にずいぶん間が空いてしまいました。

さて、前回までにSELECT/INSERT/UPDATE/DELETE、つまりCRUDのためのAPIを一通り考えた*1ので実装に移りたいと思います。トランザクション*2やCREATE/DROPなどのDMLのためのAPIも検討しなければなりませんが、それらは別途考えます。

例で使用するテーブル

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

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');

whereメソッドの使い方

whereメソッドの使い方を再掲しておきます。

-- 成人のメンバーを取得
SELECT * FROM members WHERE age >= 20;

上記のようなSQL相当の検索を実現するために、PerlShellKoherent::DBでは次のようなコードを書くことを想定していました。

my $adults = $members->where(sub{
    my $row = shift;
    $row->{'age'} >= 20;
});

whereメソッドに渡しているのは条件サブルーチンです。条件サブルーチンは引数として行($row)をとり、その行が条件に適合するかを判別します。条件に適合する場合には真を、条件に適合しない場合には偽を返します。whereメソッドの戻り値は、渡された条件サブルーチンに適合する行だけを保持したViewオブジェクトです*3

$adultsからデータを得るには、イテレータを用いて次のように行います。詳細はSELECTのAPI(3) イテレータによるデータアクセスの通りです。

# $adultsからイテレータを取得
my $iterator = $adults->iterator;

# 1行ずつデータを読み出して表示
while(my $row = $iterator->next){
    print "Family name: $row->{'family_name'}, First name: $row->{'first_name'}\n";
}

whereメソッドの実装

前節を元に実装を考えてみましょう。

whereメソッドはViewクラスのインスタンスメソッドであり、Viewオブジェクトを返す必要があります。引数は前節で述べたように条件サブルーチンです。戻り値となるViewオブジェクトは、当然Viewクラスを継承したクラスのインスタンスである必要があります。

whereメソッドの戻り値となるViewオブジェクトのクラスをConditionedViewクラスとしましょう。そうすると、whereクラスが行うことは条件サブルーチンを用いてConditionedViewオブジェクトを生成し、それを返すことと言えます。ConditionedViewクラスのコンストラクタには何を渡せば良いでしょうか。当然、条件サブルーチンを渡さなければなりません。加えて、どのViewオブジェクトに対する条件なのかという情報がなければならないので、whereメソッドを呼び出したViewオブジェクト自身を渡す必要があります。

コードにすると次のようになります。whereメソッドに関係する部分以外は省略しています。

package Koherent::DB::View;

use Koherent::DB::ConditionedView;

# 省略

sub where{
    my $self = shift;

    # 条件サブルーチンの受け取り
    my $condition = shift;

    # 条件サブルーチンを用いて生成したConditionedViewオブジェクトを返す。
    # whereメソッドを呼び出したViewオブジェクト自身を一つ目の引数で渡している。
    Koherent::DB::ConditionedView->new($self, $condition);
}

# 省略

ConditionedViewクラスの実装

whereメソッドではConditionedViewクラスに処理を丸投げしただけなので、次にConditionedViewクラスの実装を考えます。ConditionedViewクラスで考えなければならないのは、コンストラクタであるnewメソッドと、データアクセスのためのイテレータを返すiteratorメソッドです。

コンストラクタでは特別な処理はありません。受け取った引数を必要なときに取り出せるように、フィールド(メンバ変数)として格納しておくだけです。

iteratorメソッドも特別な処理はしません。データアクセスはイテレータを介して行うため、ここでもイテレータに処理を丸投げするだけです。この、丸投げする先のイテレータを表すクラスをConditionedViewIteratorクラスとしましょう。条件判別のために必要な情報はすべてConditionedViewオブジェクトが保持しているため、ConditionedViewIteratorクラスのコンストラクタに渡さなければならないものはConditionedViewオブジェクトのみで良いでしょう。

コードにすると次のようになります。Viewクラスはnewとiterator以外にも様々なメソッドを持つ予定ですが、ややこしくなるのでここでは省略します。

package Koherent::DB::ConditionedView;

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

# 省略

sub new{
    my ($class, $view, $condition) = @_;

    # 受け取った$viewと$conditionをConditionedViewクラスのフィールドとして格納
    my $self = $class->SUPER::new;
    $self->{'view'} = $view;
    $self->{'condition'} = $condition;

    $self;
}

sub iterator{
    my $self = shift;

    # 実際に条件判別をしながらデータアクセスを行う
    # ConditionedViewIteratorオブジェクトを生成して返す。
    Koherent::DB::ConditionedViewIterator->new($self);
}

# 省略

ConditionedViewIteratorクラスの実装

ConditionedViewクラスでも処理を丸投げしただけだったので、条件判別のための処理はConditionedViewIteratorクラスに記述されることになります。

ConditionedViewIteratorクラスはViewIteratorクラスを継承しています。ViewIteratorクラスは、順次データにアクセスするためのnextメソッドのみを持ちます。nextメソッドは、対象とするテーブルやビューの行(のデータを格納したハッシュへのリファレンス)を返します。すべての行を取り出した後はundefを返します。

nextメソッドで行う処理は次の三つです。

  1. whereメソッドの呼び出し元Viewオブジェクトのイテレータを利用し、行を取り出す。
  2. 取り出した行を条件サブルーチンに渡し、対象であればそれを返す。
  3. 取り出した行が対象でなければ1に戻り、次の行を取り出す。

SELECTのAPI(3) イテレータによるデータアクセス 条件が評価されるタイミングで述べたように、イテレータが返すデータはiteratorメソッドが呼ばれた時点でのものです。このため、whereメソッドの呼び出し元Viewオブジェクトからイテレータを取得するのは、ConditionedViewオブジェクトのiteratorメソッドが呼ばれたタイミングとなります。つまり、ConditionedViewIteratorクラスのコンストラクタの内部で元のViewオブジェクトのiteratorメソッドを呼び、イテレータを取得する必要があります。

これらを考慮してConditionedViewIteratorクラスを実装すると、次のようになります。

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

# 省略

sub new{
    my ($class, $conditioned_view) = @_;

    my $self = {
        conditioned_view => $conditioned_view,
        # whereメソッドの呼び出し元Viewオブジェクトからイテレータを取得
        original_iterator => $conditioned_view->{'view'}->iterator,
    };

    bless $self, $class;
}

sub next{
    my $self = shift;

    my $original_iterator = $self->{'original_iterator'};
    my $row;

    # 1. whereメソッドの呼び出し元Viewオブジェクトのイテレータを利用し、行を取り出す。
    while($row = $original_iterator->next){
        # 2. 取り出した行を条件サブルーチンに渡し、対象であればそれを返す。
        my $condition = $self->{'conditioned_view'}->{'condition'};
        # 3. 取り出した行が対象でなければ1に戻り、次の行を取り出す。
        last if $condition->($row);
    }

    $row;
}

# 省略

アクセサメソッドを介さないConditionedViewオブジェクトのフィールドの参照

上記では、ConditionedViewIteratorオブジェクトからConditionedViewオブジェクトのデータを参照する際にアクセサメソッドを介さずにフィールドを直接参照しています。

ConditionedViewIteratorクラスとConditionedViewクラスの結び付きは通常の独立したクラス同士よりも強く、例えばJavaでこれを記述しようとするとConditionedViewIteratorクラスはConditionedViewクラスの内部クラスとして記述されるでしょう。内部クラスと考えれば、ConditionedViewIteratorオブジェクトからConditionedViewオブジェクトのフィールドを直接参照しても不自然ではありません。逆にConditionedViewIteratorオブジェクトからConditionedViewオブジェクトのデータを参照するためだけにアクセサメソッドを作れば、ConditionedViewクラスがアクセサメソッドだらけになり、ユーザ視点ではわかりづらくなるだけだと思います。このような理由から、ConditionedViewクラスにアクセサメソッドを作らずにConditionedViewIteratorクラスから直接フィールドを参照しています。

なお、これはConditionedViewクラスとConditionedViewIteratorクラスだけでなく、select、order_by、limitなどの各種メソッドにおけるViewクラスとViewIteratorクラスでも同じような実装になるでしょう。

まとめ

SQLのWHERE句のように条件に適合するデータの取得を実現するために、whereメソッド、ConditionedViewクラス、ConditionedViewIteratorクラスの三つを実装しました*4

whereメソッドに限らず、今後も

  • Viewクラスの検索用メソッド(select、order_by、limitなど)
  • そのメソッドの挙動を実現するためのViewクラス(を継承したクラス)
  • そのViewクラスにアクセスするためのViewIteratorクラス(を継承したクラス)

の三つを1セットとして実装することになると考えられます。

*1:SELECTのAPI(1) 概要SELECTのAPI(2) 様々なSELECT文への対応SELECTのAPI(3) イテレータによるデータアクセスSELECTのAPI(4) インデックスの利用UPDATE、DELETE、INSERTのAPIを参照。

*2:トランザクションはひとまず扱わない予定でしたが、最初から追記型MVCCで書いた方が楽な気がしてきたので心が揺れています。

*3:正確には行を保持しているわけではなく、イテレータがデータアクセス時に条件に適合する行だけを返すことになります。

*4:インデックスを利用した検索に関してはwhereメソッドでは実現できません。詳細はSELECTのAPI(4) インデックスの利用を参照。