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メソッドで行う処理は次の三つです。
- whereメソッドの呼び出し元Viewオブジェクトのイテレータを利用し、行を取り出す。
- 取り出した行を条件サブルーチンに渡し、対象であればそれを返す。
- 取り出した行が対象でなければ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) インデックスの利用を参照。