550 likes | 705 Vues
Introduction to DBIx::MoCo. Naoya Ito http://www.hatena.ne.jp/. What is DBIx::MoCo?. “ Light and Fast Model Component” O/R Mapper for MySQL and SQLite. Features. Easy SQL operations like CDBI / ActiveRecord (Rails) Ruby-like list operations
E N D
Introduction to DBIx::MoCo Naoya Ito http://www.hatena.ne.jp/
What is DBIx::MoCo? • “Light and Fast Model Component” • O/R Mapper for MySQL and SQLite
Features • Easy SQL operations like CDBI / ActiveRecord (Rails) • Ruby-like list operations • Transparent caching with Cache::* (usually with Cache::Memcached) • Test fixture
Install • "cpan DBIx::MoCo" • You may need force install as of now.
setup your own classes DBIx::MoCo:: DataBase DBIx::MoCo Bookmark:: MoCo Bookmark:: DataBase uses Bookmark:: User Bookmark:: Entry
setup: 1. create DataBase class package Bookmark::DataBase; use base qw/DBIx::MoCo::DataBase/; __PACKAGE__->dsn('dbi:mysql:dbname=bookmark'); __PACKAGE__->username(‘foo'); __PACKAGE__->password(‘bar'); 1;
setup: 1. create DataBase class DBIx::MoCo:: DataBase DBIx::MoCo Bookmark:: DataBase
setup: 2. create base class of your models package Bookmark::MoCo; use base qw/DBIx::MoCo/; use UNIVERSAL::require; use Expoter::Lite; our @EXPORT = qw/moco/; __PACKAGE__->db_object('Bookmark::DataBase'); ## moco('User') returns "Bookmark::MoCo::User" sub moco (@) { my $model = shift; return __PACKAGE__ unless $model; $model = join '::', 'Bookmark::MoCo', $model; $model->require or die $@; $model; }
setup: 2. create base class of your models DBIx::MoCo:: DataBase DBIx::MoCo Bookmark:: MoCo Bookmark:: DataBase uses
setup: 3. create model classes package Bookmark::MoCo::Entry; use base qw/Bookmark::MoCo/; __PACKAGE__->table('entry'); __PACKAGE__->primary_keys(qw/entry_id/); __PACKAGE__->unique_keys(qw/url/); __PACKAGE__->utf8_columns(qw/title/); 1;
setup: 3. create model classes DBIx::MoCo:: DataBase DBIx::MoCo Bookmark:: MoCo Bookmark:: DataBase uses Bookmark:: User Bookmark:: Entry
retrieve() my $entry = moco('Entry')>retrieve(url => $url); say $entry->entry_id; say $entry->title; say $entry->url; ## retrieve_by_foo(...) equals retrieve(foo => ...) $entry = moco('Entry')->retrieve_by_url($url); $entry = moco('Entry')->retrieve_by_entry_id($id);
search() my @entries = moco('Entry')->search( where => "url like 'http://d.hatena.ne.jp/%'", order => 'entry_id desc', limit => 10, ); say $_->title for @entries;
search() : placeholders my @entries = moco('Entry')->search( where => [ "url like ?", 'http://d.hatena.ne.jp/%' ], order => 'entry_id desc', limit => 10, ); say $_->title for @entries;
search() : named placeholders my @entries = moco('Entry')->search( where => [ "url like :url and is_public = :is_public", url => 'http://d.hatena.ne.jp/%', is_public => 1 ], order => 'entry_id desc', limit => 10, ); say $_->title for @entries;
search() : where ... in (...) ## SELECT * from entry where entry_id in (1, 2, 5, 6, 10) my @entries = moco('Entry')->search( where => [ "entry_id in (:entry_id)", entry_id => [1, 2, 5, 6, 10], ], order => 'entry_id desc', ); say $_->title for @entries;
Create, Update, Delete ## create new record my $entry = moco('Entry')->create( url => 'http://www.yahoo.co.jp/', title => 'Yahoo!'; ); ## update a column $entry->title('Yahoo! Japan'); ## save (in session) ## If it is not in session, updates are automatically saved. $entry->save; ## delete the record $entry->delete;
Ruby-like list operations ## Scalar context my $entries = moco('Entry')->search(...); say$entries->size; say$entries ->collect(sub { $_->title }) ->join("\n"); say$entries ->grep(sub { $_->is_public }) ->collect(sub { $_->num_of_bookmarks }} ->sum;
Ruby-like methods • push, pop, shift, unshift, add, append, prepend • size • first, last • slice, zip • map, collect, each • grep • compact • flatten • delete, delete_if, delete_at • inject • find • join • reduce • sum • uniq • dup • dump
List::RubyLike • google:github list-rubylike
Using your own list class ## create your own list class of Blog::Entry package Blog::Entry::List; use base qw/DBIx::MoCo::List/; sub to_json { ... } ## setup list_class() package Blog::Entry; ... __PACKAGE__->list_class('Blog::Entry::List');
Using your own list class ## $entries is a Blog::Entry::List my $entries = moco('Entry')->search(...); say $entries->to_json;
An entry has many bookmarks package Bookmark::MoCo::Entry; use Bookmark::MoCo; use base qw/Bookmark::MoCo/; __PACKAGE__->table('entry'); ... __PACKAGE__->has_many( bookmarks => moco('Bookmark'), { key => 'entry_id', order => 'timestamp desc', }, );
$entry->bookmarks my $entry = moco('Entry')->retrieve_by_url(...); ## bookmarks() returns bookmarks of the entry my @bookmarks =$entry->bookmarks; ## offset and limit (offset 10, limit 50) @bookmarks = $entry->bookmarks(10, 50); ## Ruby-like list operations in scalar context print$entry->bookmarks(10, 50)->grep(...)->size;
bookmarks has an entry and an owner package Bookmark::MoCo::Bookmark; use Bookmark::MoCo; use base qw/Bookmark::MoCo/; __PACKAGE__->table('bookmark'); ... __PACKAGE__->has_a( entry => moco('Entry'), { key => 'entry_id' } ); __PACKAGE__->has_a( owner => moco('User'), { key => 'user_id' } );
$bookmark->entry my $bookmark = moco('Bookmark')->retrieve; say $bookmark->entry->title; say $bookmark->owner->name
BTW: SQL is executed too many times ... my $entry = moco('Entry')->retrieve(...); say $entry->bookmarks->size; ## 1,000 ## oops, SQL is executed in 1,000 times. for ($entry->bookmarks) { say $_->owner->name; }
A entry has many bookmarks with owner (prefetching) my $entry = moco('Entry')->retrieve(...); say $entry->bookmarks->size; ## 1,000 ## bookmarks and owners will be retrieved at the same time. ## (SQL stetements are executed only 2 times.) for ($entry->bookmarks(0, 0, {with => [qw/owner/]})) { say $_->owner->name; }
Implicit prefetching package Bookmark::MoCo::Entry; use Bookmark::MoCo; use base qw/Bookmark::MoCo/; ... __PACKAGE__->has_many( bookmarks => moco('Bookmark'), { key => 'entry_id', order => 'timestamp desc', with => [qw/owner/] }, );
inflate / deflate (explicitly) my $entry = moco('Entry')->retrieve(1); ## plain string say $entry->timestamp; ## timestamp column as DateTime object say $entry->timestamp_as_DateTime->hms; ## url column as URI object say $entry->url_as_URI->host;
inflate / deflate (implicitly) package Bookmark::MoCo::Entry; ... ## plain string __PACKAGE__->inflate_column( url => 'URI', timestamp => 'DateTime, ); package main; say moco('Entry')->retrieve(1)->url->host;
inflate_column() without builtin classes package Bookmark::MoCo::Entry; ... ## plain string __PACKAGE__->inflate_column( title => { inflate => sub { My::String->new(shift) } deflate => sub { shift->as_string } } );
Transparent caching ## Just do it my $cache = Cache::Memcached->new({...}); Bookmark::MoCo->cache_object( $cache );
Transparent caching ## The entry object will be cached my $entry = moco('Entry')->retrieve(1); ## Cached object will be retrieved from memcached $entry = moco('Entry')->retrieve(1); ## both cache and database record will be updated $entry->title('foobar'); $entry->save;
NOTE: "session" is needed when you use caching feature or prefetching. Blog::MoCo->start_session; my $entry = moco('Entry')->retrieve(...); Blog::MoCo->end_session;
Fixtures: building records for testing from YAML ## fixtures/entries.yml model: Bookmark::Entry records: first: id: 1 title: Hatena Bookmark url: http://b.hatena.ne.jp/ second: id: 2 title: Yahoo! Japan url: http://www.yahoo.co.jp/
Writing tests with fixtures ## t/entry.t use DBIx::MoCo::Fixture; use Bookmark::Entry; use Test::More tests => 2; ## loading records from entries.yml, ## then returns them as objects. my $f = fixtures(qw/entries/); my $entry = $f->{entry}->{first}; is $entry->title, "..."; is $entry->url, "...";
Pros • Simple and easy • List operations are very sexy. • Transparent caching is "DBに優しい" • Test fixture
Cons • less document • some difficulties (especially in session and cache) • low test coverage • some bugs
patches are welcome.jkondo at hatena ne jp (primary author)