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
Quick start
Install "cpan DBIx::MoCo" You may need force install as of now.
setup your own classes DBIx::MoCo DBIx::MoCo:: DataBase Bookmark:: MoCo Bookmark:: DataBase Bookmark:: User Bookmark:: Entry uses
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 DBIx::MoCo:: DataBase 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 DBIx::MoCo:: DataBase 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 DBIx::MoCo:: DataBase Bookmark:: MoCo Bookmark:: DataBase Bookmark:: User Bookmark:: Entry uses
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);
SQL operations
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;
List operations
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 ;
Relationships
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
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
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;
Testing
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 and Cons
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)‏
We're hiring! google: はてな 求人
nice office.
nice development environment.
ところで ... (By the way)
勤務地は京都です  (Our HQ is located at Kyoto.)
Thank you!
Any Questions?

Introduction To Moco

  • 1.
    Introduction to DBIx::MoCoNaoya Ito http://www.hatena.ne.jp/
  • 2.
    What is DBIx::MoCo?“ Light and Fast Model Component” O/R Mapper for MySQL and SQLite
  • 3.
    Features Easy SQLoperations like CDBI / ActiveRecord (Rails)‏ Ruby-like list operations Transparent caching with Cache::* (usually with Cache::Memcached)‏ Test fixture
  • 4.
  • 5.
    Install "cpan DBIx::MoCo"You may need force install as of now.
  • 6.
    setup your ownclasses DBIx::MoCo DBIx::MoCo:: DataBase Bookmark:: MoCo Bookmark:: DataBase Bookmark:: User Bookmark:: Entry uses
  • 7.
    setup: 1. createDataBase class package Bookmark::DataBase; use base qw/DBIx::MoCo::DataBase/; __PACKAGE__->dsn('dbi:mysql:dbname=bookmark'); __PACKAGE__->username(‘foo'); __PACKAGE__->password(‘bar'); 1;
  • 8.
    setup: 1. createDataBase class DBIx::MoCo DBIx::MoCo:: DataBase Bookmark:: DataBase
  • 9.
    setup: 2. createbase 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; }
  • 10.
    setup: 2. createbase class of your models DBIx::MoCo DBIx::MoCo:: DataBase Bookmark:: MoCo Bookmark:: DataBase uses
  • 11.
    setup: 3. createmodel 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;
  • 12.
    setup: 3. createmodel classes DBIx::MoCo DBIx::MoCo:: DataBase Bookmark:: MoCo Bookmark:: DataBase Bookmark:: User Bookmark:: Entry uses
  • 13.
    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);
  • 14.
  • 15.
    search()‏ my @entries= moco('Entry') ->search( where => "url like 'http://d.hatena.ne.jp/%'", order => 'entry_id desc', limit => 10, ); say $_->title for @entries;
  • 16.
    search() : placeholdersmy @entries = moco('Entry')->search( where => [ "url like ?", 'http://d.hatena.ne.jp/%' ], order => 'entry_id desc', limit => 10, ); say $_->title for @entries;
  • 17.
    search() : namedplaceholders 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;
  • 18.
    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;
  • 19.
    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;
  • 20.
  • 21.
    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;
  • 22.
    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
  • 23.
  • 24.
    Using your ownlist 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');
  • 25.
    Using your ownlist class ## $entries is a Blog::Entry::List my $entries = moco('Entry')->search(...); say $entries ->to_json ;
  • 26.
  • 27.
    An entry hasmany 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', }, );
  • 28.
    $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;
  • 29.
    bookmarks has anentry 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' } );
  • 30.
    $bookmark->entry my $bookmark= moco('Bookmark')->retrieve; say $bookmark ->entry->title; say $bookmark ->owner->name
  • 31.
    BTW: SQL isexecuted 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 ; }
  • 32.
    A entry hasmany 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 ; }
  • 33.
    Implicit prefetching packageBookmark::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/] }, );
  • 34.
  • 35.
    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;
  • 36.
    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;
  • 37.
    inflate_column() without builtinclasses package Bookmark::MoCo::Entry; ... ## plain string __PACKAGE__-> inflate_column ( title => { inflate => sub { My::String->new(shift) } deflate => sub { shift->as_string } } );
  • 38.
  • 39.
    Transparent caching ##Just do it my $cache = Cache::Memcached->new({...}); Bookmark::MoCo->cache_object( $cache );
  • 40.
    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;
  • 41.
    NOTE: "session" isneeded when you use caching feature or prefetching. Blog::MoCo->start_session; my $entry = moco('Entry')->retrieve(...); Blog::MoCo->end_session;
  • 42.
  • 43.
    Fixtures: building recordsfor 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/
  • 44.
    Writing tests withfixtures ## 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, "...";
  • 45.
  • 46.
    Pros Simple andeasy List operations are very sexy. Transparent caching is "DB に優しい " Test fixture
  • 47.
    Cons less documentsome difficulties (especially in session and cache)‏ low test coverage some bugs
  • 48.
    patches are welcome.jkondo at hatena ne jp (primary author)‏
  • 49.
    We're hiring! google:はてな 求人
  • 50.
  • 51.
  • 52.
  • 53.
    勤務地は京都です (OurHQ is located at Kyoto.)
  • 54.
  • 55.