こんにちわ。nekokakさんをはじめ#dbi-ja@irc.perl.orgのPerlハッカーのみなさまにムチャ振り声をかけていただき、突然hacker trackで書くことになりました。eisukeoishiともうします。はじめまして。
今回は、私が作成したDBIx::ObjectMapperというO/Rマッパーについて紹介させていただきます。
みんな大好きな「PofEAA」のData Mapperパターンを実装したものです。
Data Mapperパターンをごく簡単に説明すると、データベースやO/Rマッパーに依存せずにオブジェクトをデータベースと連携させるためのものです。
DBIx::ObjectMapperで使用するクラスはPOPO(Plain Old Perl Object)であり、O/Rマッパーだけで使用する必要もありません。
他言語ではPythonのSQLAlchemy, JavaのHibernateなどがData MapperパターンのO/Rマッパーですね。
DBIx::ObjectMapperは特にSQLAlchemyから諸々拝借しています。
今回はこのDBIx::ObjectMapperで簡単なinsert,select,update,deleteの操作をやってみます!
まずは、DBIx::ObjectMapperオブジェクトを作成します。
use DBIx::ObjectMapper; use DBIx::ObjectMapper::Engine::DBI; my $engine = DBIx::ObjectMapper::Engine::DBI->new({ dsn => 'DBI:SQLite:', username => undef, password => undef, on_connec_do => [ q{CREATE TABLE users( id INTEGER PRIMARY KEY, name TEXT)} ], }); my $mapper = DBIx::ObjectMapper->new( engine => $engine );
Engineを作成して、それをDBIx::ObjectMapperにわたしてあげます。
次にusersテーブルのデータを保持するMy::Userクラスを作成します。
package My::User; use Mouse; has 'id' => ( is => 'rw', isa => 'Int' ); has 'name' => ( is => 'rw' ); 1;
これは、なんの変哲もない普通のクラスですね。
Mouseを使用していますが、みなさん大好きなMooseでもなんでも、とにかくperlのクラスとして定義できていればOKです。
(※Class::Accessor::Liteでは動作しないというバグがあるため、現在対応中です)
さて次に、usersテーブルのschema情報をデータベースからロードしてメタデータとして取り込み、My::Userクラスとメタデータを連携させます(mapping)。
my $users_table = $mapper->metadata->table( 'users' => 'autoload' ); $mapper->maps( $users_table => 'My::User' ); # あるいはmetadata->tableメソッドにテーブル名だけを指定することでテーブルのメタデータを取得できます。 $mapper->maps( $mapper->metadata->table('users') => 'My::User' );
というクラスの場合、上記のようにオプションを指定することなくmappingできます。
上記のようなクラス以外でもmapsメソッドにさらにオプションを指定することで、様々な形態のクラスにmappingすることが可能です。
さて、ここまでで準備は完了です。
それでは早速データベースへinsertをしてみましょう。
my $user = My::User->new( name => 'eisukeoishi' ); my $session = $mapper->begin_session( autocommit => 0 ); $session->add($user); # BEGIN; $session->commit; # INSERT INTO users ( name ) VALUES ('eisukeoishi'); # COMMIT;
ごく普通にMy::Userオブジェクトを作成して、ナゾのsessionというもののaddメソッドにオブジェクトをわたしてあげるだけで、insertができました。
データベースにinsertするのではなく、オブジェトをaddしたというイメージになるでしょうか。
さて、ここでナゾのsessionというものについて説明します。
sessionはDBIx::ObjectMapperにとって重要な存在で、直接的な操作はすべてsessionから行います。
sessionの役割りとして、
などがあります。
特に最後の一連の操作単位を決める部分が重要になります。
begin_sessionメソッドのオプションを指定することでその動作が決定されます。
オプションは下記になります(カッコ内はデフォルト値です)
注意点としては、sessionの有効範囲をあまり広くしないようにすること、データベースへの破壊的な操作(update,delete,insert)が発生する場合は、autocommit=0とすることをお勧めします。
ごちゃごちゃといろいろ書いてしまいましたが、気をとりなおして、先程insertしたデータを取得してみましょう。
my $session = $mapper->begin_session( autocommit => 0 ); my $user = $session->get( 'My::User' => 1 ); # SELECT users.name, users.id FROM users WHERE ( users.id = 1 ) print $user->id; # 1 print $user->name; # eisukeoishi
sessionのgetメソッドにクラス名とプライマリになる値を渡すことでMy::Userオブジェクトが取得できます。DBIx::SkinnyのsingleメソッドやDBIx::Classのfindメソッドと同じようなものになります。
複数のカラムでユーニクになる場合はHashリファレンスを渡すことで取得ができます。
my $session = $mapper->begin_session( autocommit => 0 ); my $user = $session->get( 'My::User' => 1 ); # BEGIN; # SELECT users.name, users.id FROM users WHERE ( users.id = 1 ); $user->name('emanon'); $session->commit; # UPDATE users SET name = 'emanon' WHERE ( users.id = 1 ); # COMMIT;
取得したオブジェクトの属性をsessionのスコープ内でアクセッサから変更してあげるだけでupdateができます。
my $session = $mapper->begin_session( autocommit => 0 ); my $user = $session->get( 'My::User' => 1 ); # BEGIN; # SELECT users.name, users.id FROM users WHERE ( users.id = 1 ) $session->delete($user); $session->commit; # DELETE FROM users WHERE ( users.id = 1 ); # COMMIT;
addのときと同様、オブジェクトをdeleteメソッドに渡してあげるだけです。
my $session = $mapper->begin_session( autocommit => 0 ); $session->add_all( My::User->new( name => 'user1' ), My::User->new( name => 'user2' ), My::User->new( name => 'user3' ), My::User->new( name => 'user4' ), ); my $it = $session->search('My::User')->execute; while( my $user = $it->next ) { print $user->id . ':' . $user->name . $/; } $session->commit;
条件を指定する場合は、
my $session = $mapper->begin_session( autocommit => 0 ); my $attr = $mapper->attribute('My::User'); my $it = $session->search('My::User') ->filter( $attr->prop('id') > 3, $attr->prop('name') != undef, )->execute; while( my $user = $it->next ) { print $user->id . ':' . $user->name . $/; }
のように、mapperのattributeメソッドでクラスの属性を取得してfilterメソッドに条件を書いてあげます。
ここでも注意点としては、条件を指定するのは、テーブルのカラムではなく、オブジェクトの属性を指定している点です。オブジェクトを検索しているというイメージを持ってもらえば大丈夫かと思います。
また、propメソッドで取得したクラスの属性情報に演算子をつけて条件を指定するところが、一般的なO/Rマッパーとはちょっと違う点ですが、慣れると直感的に書けます。
サポートしている演算子は ==,!=,>=,>,<=,<,eq,ne,lt,gt,le,geです。
その他に
$attr->prop('id') == [ 1, 2, 3 ]
は、id IN (1, 2, 3) と INとして動作し、
like,betweenなどは
$attr->prop('name')->like('%str%'); $attr->prop('id')->between( 1, 3 );
のように書けます。
mappingするのにわざわざクラスを作成するとかメンドイという方には、自動でクラスを作成するオプションがあります。
冒頭のサンプルを改変してMy::Userクラスを作成せずに、下記のようにmapsメソッドを呼ぶことで、自動でクラスを作成できます。
$mapper->maps( $users_table => 'My::User', constructor => { auto => 1 }, accessors => { auto => 1 }, );
これで、個別に作成する必要があるクラスのみを用意すれば良いので、Data Mapperパターンでありがちなクラス地獄に陥いることが防げるのではないでしょうか。
今回はざっくりと簡単な紹介をしましたが、DBIx::ObjectMapperはその他にも様々な機能を搭載しています。
簡単な概要が知りたい方は、http://eisuke.github.com/yapcasia2010/をご覧ください。
DBIx::Skinnyを筆頭に、O/Rマッパーのシンプル化の波に逆行している感もあるこのモジュールですが、オブジェクトに重点を置いたアプローチは他にはない大きな特徴で、そういった選択肢がPerlでできるということが重要なのかなと私は思っております。
もちろんシンプルにできないと諦めているわけではなく、依存モジュールを極力減らしたり、コードの再整理などを行なって使いやすいモジュールになるようにもっと改善できたらなあと思っております。
現在はドキュメントなどがとても不足しており、みなさまに広く使っていただける状況まで至っていませんが、今後もblogなどでDBIx::ObjectMapperについての記事を書いていきたいなあと思っている次第です。
また、DBIx::ObjectMapperを使ってみて、下記のような体験があれば、すぐにご一報いただけると幸いです。
DBIx::ObjectMapperよかったら使ってみてください!