DBICのリレーションシップ

やっぱり、DBIx::Class::Relationshipを読み直すことにした。
こういうのをちゃんと理解しておかないと足をすくわれるし。
ちょっとづつ書いたら、何日もかかった。ションボリ。

まずは前提となるデータ

こんな感じで、Authorテーブルと、Bookテーブルがあるとする。
ID | Name | Age
 ------------------
   1 | Fred | 30
   2 | Joe | 32

  ID | Author | Name
 --------------------
   1 | 1 | Rulers of the universe
   2 | 1 | Rulers of the galaxy

リレーションシップなしの場合

もしもリレーションシップを使わないときに、Fredの書いた全ての本を取
り出すとしたら、以下のようになる。
my $fred = $schema->resultset('Author')->find({ Name => 'Fred' });
my $fredsbooks = $schema->resultset('Book')->search({ Author => $fred->ID });

2行書かなきゃいけなくて、理解はしやすいかも知れないけど面倒すぎ。
リレーションシップありの場合

リレーションシップを使えば1行ですむ。
今回の場合は「Author上でbooksという名前のhas_manyなリレーションシッ
プを宣言」しておけば、以下のようにできる。(細かいことは後)
my $fredsbooks = $schema->resultset('Author')->find({ Name => 'Fred' })->books;

確かに手間が半分以下になる。すごい。
リレーションシップって何??

各リレーションシップは、 テーブルのアイテムたちを構成する
DBIx::Class::Manual::Glossaryオブジェクト
の"Row"がもつ、アクセサメソッドをセットアップする。
RowオブジェクトはResultSetオブジェクトから返ってきた実際のデータ。
DBIx::Class::Manual::Glossaryオブジェクトの"ResultSet" より、
search_relatedを使って、リレーションシップたちは検索されることが可能になる。
ResultSetオブジェクトはテーブルオブジェクトのこと。データそのもの。
リレーションシップの利点

リレーションシップは
リストコンテキストでは、リレーションはRowオブジェクトたち
のリストを関連するクラスに返す。
スカラーコンテキストでは、JOINされたテーブルを表現する新し
いResultSetを返す。

だから、クエリ圧縮のためのリレーションの連鎖呼び出しができる。
従って、実際にデータベースにクエリが投げられるのは、
実際のアイテムのためのデータを検索する必要に迫られたときで、
それまでは一切無駄な時間を使わない。

例えば、以下のようにsearch_relatedメソッドを使うと、
以下のような1行のSQLを発効する。
 my $cheapfredbooks = $schema->resultset('Author')->find({
   Name => 'Fred',
 })->books->search_related('prices', {
   Price => { '<=' => '5.00' },
 });

 SELECT * FROM Author me
 LEFT JOIN Books books ON books.author = me.id
 LEFT JOIN Prices prices ON prices.book = books.id
 WHERE prices.Price <= 5.00

複数回のfetcheなしでいける。
つか、リレーションシップを記述する利点は豊富なヘルパーメソッド

このとき、どこかでpriceという名前で、Bookテーブルと値段の書いたテー
ブル(多分Priceテーブル)との間のリレーションを記述しておく必要がある。

リレーションシップを記述すると、search_relatedなどのヘルパーメソッドが使
えるようになる。
search_relatedメソッド以外にも、頻繁に使う基本的なヘルパーなメソッドが用意されている。

search_relatedメソッド以外の、頻繁に使う基本的なヘルパーメソッドは
DBIx::Class::Relationship::Baseにあるから、そっちを見る。
(add,regist,search,count,create,find,update,set,delete,add,remove・・・)
まー、何か大体あるなー。

で、実際のリレーションシップに使うメソッドを見てね。

ヘルパーメソッドどもの使い方について


すべてのヘルパーメソッドは以下のような引数を取得する。
_PACKAGE__>$method_name('relname', 'Foreign::Class', $cond, $attrs);

$condと$attrsはオプションだから、何も書かなくてもいい。
どうしても何かを書きたくて、かつ無記入と同じデフォルトの値を渡したいときは、
undefを渡せばいいよ。

ヘルパーメソッドはさておき、リレーションの記述方法


実際にリレーションを記述するのは、テーブルスキーマを記述するクラス。

例えば、DB用のスキーマと、テーブルのスキーマを用意する。
package My::DBIC::Schema;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class::Schema/;
__PACKAGE__->connection('dbi:mysql:testapp', 'id', 'pass');
__PACKAGE__->load_classes(qw/Books Authors ISBN/);
1;

MySQLにtestappという名前のDBをcreateした状態。
idとpassには接続に必要なものを書く。
つか、いい加減だからコピペしても動かないと思う。

さらに、テーブルのスキーマ。
package My::DBIC::Schema::Book;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('book');
__PACKAGE__->add_columns(qw/id author name/);
__PACKAGE__->set_primary_key('id');
1;
__END__

package My::DBIC::Schema::Author;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('author');
__PACKAGE__->add_columns(qw/id name age/);
__PACKAGE__->set_primary_key('id');
1;
__END__


まー、多分こんな感じ。
説明とスキーマ内のテーブルとかアイテムの名前が違うけど、そこはそれ。
何書いているのか良く分からない人は、WEB-DBマガジンVol.36のnaoyaさ
んの連載を見ると、へえ、と分かると思います。

これらのものが少なくともある状態で、以下につづく。
has_one

Arguments: $accessor_name, $related_class,$foreign_key_column|$cond?, $attr?
引数:アクセサ名、関連付け対象のクラス名が必須。必要ならその他の引数も。

どこかにBookテーブルのidカラム(PK)と対応付けられている、
ISBNテーブルがあって、確実にBookとISBNが1対1対応すると分かっている
ときの例は以下のような感じ。My::DBIC::Schema::ISBNは省略。
package My::DBIC::Schema::Book;
・・・
__PACKAGE__->has_one(isbn => 'My::DBIC::Schema::ISBN');

My::DBIC::Schema::Bookに追記するってことですよ。

こういうリレーションシップを書いておくと、
isbnを呼び出してResultSetを得たり、ヘルパーメソッドを使えたりする。
あるクラスと他のクラスの1対1のリレーションシップを作ったことになる。
my $schema = My::DBIC::Schema->connect;
my $obj = $schema->resultset('Book')->find(1);
my $isbn_obj = $obj->isbn; # to get the ISBN object

この場合、スカラーコンテキストなのでResultSet(データ)が返ってくる。
返ってきたResultSetは、BookテーブルとISBNテーブルがINNER JOINされ
たもの。

JOINする際に「対応するオブジェクトが必ずある」という暗黙の了解がな
い場合は、might_haveを使うと良い。
might_haveの場合には、LEFT JOINされたResultSetが返ってくる。
has_oneとmight_haveの違いは、それだけ。
INNER JOINの場合は、対応関係が取れないデータは無視するし、
LEFT JOINの場合には対応関係が取れない場合に、対応先として、
とりあえずnullを挿入してJOINしてくれる。

JOINするときに使われるキーについては、might_haveの説明中に書く。
ちなみに、上のリレーションシップの場合は、
My::DBIC::Schema::ISBNのisbnカラムか、My::DBIC::Schema::ISBNのPKが、
My::DBIC::Schema::BookのPKと対応していると考えられる。
might_have

あー、えーと、Pseudonymテーブルが出てきますけど記述は省略。

Arguments: $accessor_name, $related_class,$foreign_key_column|$cond?, $attr?
引数:アクセサ名、関連付け対象のクラス名が必須。必要ならその他の引数も。
package My::DBIC::Schema::Author;
・・・
__PACKAGE__->might_have(pseudonym => 'My::DBIC::Schema::Pseudonym');

このようなリレーションを記述することで、以下のようにできる。
my $schema = My::DBIC::Schema->connect;
my $obj = $schema->resultset('Author')->find(1);
my $pname = $obj->pseudonym; # to get the Pseudonym object

で、JOINするときのキーが一体何なのか。
これが知りたくて英語ドキュメントを読み直したんだけど、
ちゃんと書いてありましたよ。
JOINするときのキーは何なのか

My::DBIC::Schema::Author->might_have(pseudonym =>'My::DBIC::Schema::Pseudonym')


上の例のMy::DBIC::Schema::Authorへの記述は、
クラスに対する任意の1対1のリレーションシップを作る。
これはhas_oneとmight_haveで共通のこと。

「Authorはmight_haveだ。Pseudonymを。」という関係。
つまりAuthorがPseudonymを所有している。
PseudonymがAuthorを所有するかなー?と思うので、
この場合、AuthorはPseudonymのPKを知らないけれど、
PseudonymはAuthorのPKを知っている状態ということになる。

まー、もちろん、AuthorがPseudonymのPKを知っていて、
互いに所有しあっていても問題はない。
ポイントは所有される側が、所有する側のPKを知っているか、
ということだろう。
これって、何か哲学的だなー。

例えば主人と犬がいて、犬は主人のPKを知らなきゃいけないけれど、
主人は犬のPKを知っていても知らなくてもいいってことだよなー。
ふかいー。子供が泣くから親は子を育てるんですねー。

あ、脱線した。

リレーションシップは標準ではアクセサ名(例、pseudonym)を、
関連付けされたクラス(例、My::DBIC::Schema::Pseudonym)中の外部キー
として、JOINを解決するために使う。
リレーションの記述の3つめの引数は、$foreign_key_columnか$condだが
例外は、関連付けされたクラスに$foreign_key_columnに明記されたカラムが
ある場合や、$condがJOIN条件を示すハッシュへの参照を示す場合だ。

リレーションシップは
- 関連付けされたクラスに$foreign_key_column(外部キーのカラム)と明記されたカラムがあるか
- JOIN条件を示すハッシュ$condがあるか
を調べて、JOINの対応を解決しようとする。
そもそも$foreign_key_columnか$condがなければ、
- 関連付けされたクラスに、アクセサ名と同じ名前のカラムはあるか
を調べてくれる。もしカラムがあればJOIN時につかわれる。
それでも駄目なら、2つのテーブルはPKを共有していると見なすようだ。

なるほどねー。あとで試してみよう。やっと分かったよー。

つまり、上述のAuthorとPseudonymのリレーションシップでは、
AuthorにPseudonym関連の記述がないことは分かっているので、
PseudonymのpseudonymカラムかPKが、AuthorのPKと一致していないと、
期待通りの結果を得られないことがわかる。

PseudonymのauthorというカラムにAuthorのPKが入っているときには、
以下のように書いてあげれば、期待通りのJOINの解決がおこなわれる。
これが、$foreign_key_columnの設定。
この場合、PseudonymのauthorカラムをJOINに使う。

My::DBIC::Schema::Author->might_have( pseudonym =>
					 'My::DBIC::Schema::Pseudonym',
					 'author' );


さもなくば、こう書く。これがJOIN条件を示すハッシュ$cond。

  My::DBIC::Schema::Author->might_have( pseudonym =>
					 'My::DBIC::Schema::Pseudonym',
					 { 'foreign.author' => 'self.author' } );


この場合、foreign(My::DBIC::Schema::Pseudonym)のauthorカラムと
self(My::DBIC::Schema::Author)のauthorカラムをJOINの解決に使ってくれる。
JOINの解決に呼び出し側のPKを使わない場合はこれだね。

結局has_oneとmight_haveとbelongs_toは、それぞれ理解すると理解が深
まるので、上を見たり下をみたりするといいかも。
belongs_to

では、belongs_to。基本はhas_oneやmight_haveと同じ。

Arguments: $accessor_name, $related_class,$foreign_key_column|$cond?, $attr?
引数:アクセサ名、関連付け対象のクラス名が必須。必要ならその他の引数も。

  # in a Book class (where Author has many Books)
  My::DBIC::Schema::Book->belongs_to( author => 'My::DBIC::Schema::Author' );
  my $author_obj = $obj->author; # get author object
  $obj->author( $new_author_obj ); # set author object


「Bookは所有されている。Authorに。」という関係。
AuthorがBookの親であるという関係ですね。

AuthorがBookのPKをどのカラムにも持っていないときに使う。
つまり呼び出される側が呼び出す側のPKを持っていない場合が、
belongs_toとなる。

呼ぶ側のクラスが呼ぶ側のクラス自身のカラムの一つ(または複数)に、
呼ばれる側である外部クラスのプライマリキー(PK)を保存している、とい
うリレーションシップを作る。

上の例の場合、
呼ぶ側クラスから見た外部クラスは「My::DBIC::Schema::Author」で、
呼ぶクラス「My::DBIC::Schema::Book」は、コラムのどこかにAuthorのPK
をもっているということだ。

ふむふむ。

上述の例だと、$foreign_key_columnや$condが無いので、
Bookのauthorカラムを使おうとするわけだ。

ちなみにbelongs_toは1対1、1対多、多対多の関係のどれでも使われる。
has_oneやmight_haveは必ず1対1ね。

$foreign_key_columnや$condの使い方は同じ。

  My::DBIC::Schema::Book->belongs_to( author=> 'My::DBIC::Schema::Author',
				       { 'foreign.author' => 'self.author' } );

4つめの引数$attって何?

$attの説明をすっ飛ばしてきた。じゃあ、理解してみよう。

has_oneやmight_haveは、そのようなリレーションシップにおけるオブジェ
クト(JOINされたデータ)を作るためにあるんだけれど、
そのオブジェクトにdeleteしたりupdateしたりしたときに、
標準では関連付けされた基のオブジェクトもdeleteされたりupdateされ
たりする。これが嫌なときは$attを使う。

関連づけされた基のオブジェクトへの作用を切るには以下のようにする。
My::DBIC::Schema::Author->might_have( pseudonym =>
					 'My::DBIC::Schema::Pseudonym',
					 'author', {cascade_delete => 0} );

こうすることで、DBレベルのdaleteやupdateの制約を無効化できる。へえ。

belongs_toの$attではJOINの方法を指定できる。
どうもbelongs_toの標準はINNER_JOINのようだ。
そうすると、$obj->authorして、Bookのauthorカラムを見たときに、
Authorのauthorカラムと対応するものがないときに、
その対応するものがないauthorカラムの要素はJOIN時に無視される。
そうではなくて、Bookのauthorカラムに対応する要素がないなら、
対応要素の代わりにNULLをいれて欲しい場合もある。
そんな時は、以下のようにしてleft_joinだよ、と思えてあげればいい。
  # in a Book class (where Author has_many Books)
  __PACKAGE__->belongs_to(author => 'My::DBIC::Schema::Author',
			   'author', {join_type => 'left'});

また、belongs_toでは標準で「cascade_delete => 0」になっている。
なので、DBレベルのdaleteやupdateの制約を有効にしたければ、
「cascade_delete => 1」と指定してあげればいい。
has_many

はぁ、疲れてきた。has_manyか。

Arguments: $accessor_name, $related_class,$foreign_key_column|$cond?, $attr?
引数:アクセサ名、関連付け対象のクラス名が必須。必要ならその他の引数も。

has_oneじゃなく、1対多のhas_many。
「こいつはもってる。たくさんのあれを。」な関係。
今回は著者と本の関係。一人の著者が沢山の著書をもってる場合ですね。

記述はhas_oneとかの時と、そんなに変わっていない。
  # in an Author class (where Author has_many Books)
  My::DBIC::Schema::Author->has_many(books => 'My::DBIC::Schema::Book', 'author');

Authorから見て、複数のbookを持っているんだから、
直感的にbooksという名前のアクセサ名にしたい。
Author - has_many - booksね。

でも、そうするとbooksカラムなんて無いから、
ちゃんと$foreign_key_columnを指定する必要がある。
上述の場合は、BookのauthorカラムとAuthorのPKが対応してる。
もちろん、$foreign_key_columnや$condが無いときには、
アクセサ名を使って呼ばれたクラス(例だとBook)のカラムを探す。
なるほどね。

has_manyなリレーションシップを作ると、
3つのメソッドが作られて、それらを使えるようになる。
上述の例だと、以下の3つが作られる。

- books()
- books_rs()
- add_to_books()

要するに、

- $accessor_name()
- $accessor_name + _rs()
- add_to_ + $accessor_name()

の3つが作られる。

まず、例文を示す。

  # in an Author class (where Author has_many Books)
  My::DBIC::Schema::Author->has_many(books =>
  'My::DBIC::Schema::Book', 'author');

  my $booklist = $obj->books;
  my $booklist = $obj->books({
    name => { LIKE => '%macaroni%' },
    { prefetch => [qw/book/],
  });
  my @book_objs = $obj->books;
  my $books_rs = $obj->books;
  ( $books_rs ) = $obj->books_rs;

  $obj->add_to_books(\%col_data);


3つのメソッドを使っている。
リレーションシップで返ってくるオブジェクトは、
スカラーコンテキストの場合ResultSetだと分かっているので、
books()の使い方については割愛。
# だって、今はリレーションシップが分からないんだもん。

さらに、books_rs()は、単純にResultSetを返してくれる。
ただ、ポイントなのはスカラーコンテキストでも、
リストコンテキストでも、お構い無しにResultSetを返す。

add_to_books()については、詳しくはDBIx::Class::Relationship::Base
を参照してくれと書いてある。
新しいRowアイテムを追加するためにあるメソッドのようだ。
そのため、has_manyは標準ではDBレベルのdaleteやupdateの制約が
有効になっている。無効にしたければ$attに「cascade_delete => 0」
を入れておけばいい。

ちなみに、add_to_の例はこんな感じ。
  my $role = $schema->resultset('Role')->find(1);
  $actor->add_to_roles($role);
      # creates a My::DBIC::Schema::ActorRoles linking table row object
  $actor->add_to_roles({ name => 'lead' }, { salary => 15_000_000 });
      # creates a new My::DBIC::Schema::Role row object and the linking table
      # object with an extra column in the link

あー、なるほどねー。だ。
many_to_many

最後だ最後。many_to_manyいってみよう。

Arguments: $accessor_name, $link_rel_name, $foreign_rel_name, $attr?
引数:アクセサ名、リンクリレーションシップ名、外部リレーションシッ
プ名が必須。必要ならその他の引数も。

どんなときがmany_to_manyなのか。
それはマルチユーザなブックマークとかを考えるといいかも。
複数のユーザと複数のエントリがあって
あるユーザの行に複数のエントリの行が対応していて、
また、複数のユーザの行に、あるエントリの行が対応している。
そんなのをイメージすると、many_to_manyって必要だなと分かる。

実際のところ、このmany_to_manyな関係は例えばBookとAuthorのような、
2つのテーブルがあれば使えるわけではなく、
もう1つ中間テーブルのようなものが必要になる。

UserテーブルとEntryテーブルのmany_to_manyを表現するには、
Bookmarkテーブルも必要ってことですね。へえ。

丁度良いので、WEB-DBマガジンVol,36のBookmarkアプリの
スキーマを使って考えてみようかな。
create table user(
    id int unsigned auto_increment primary key,
    name varchar(32),
    key (name)
) TYPE = MyISAM DEFAULT CHARSET=utf8;

create table entry(
    id int unsigned auto_increment primary key,
    url varchar(255) binary unique,
    title varchar(255),
    created_on datetime,
    key (url)
) TYPE = MyISAM DEFAULT CHARSET=utf8;

create table bookmark(
    user_id int unsigned,
    entry_id int unsigned,
    comment text,
    created_on datetime,
    primary key(user_id, entry_id)
) TYPE = MyISAM DEFAULT CHARSET=utf8;

あー、ふーん。なるほど。

で、many_to_manyを使うまえにhas_manyな関係を記述する。

・userとbookmarkの関係
package Bookmark::Schema::User;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('user');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(
     bookmarks=>'Bookmark::Schema::Bookmark',{
	  'foreign.user_id'=>'self.id'
     }
);
1;

・entryとbookmarkの関係
package Bookmark::Schema::Entry;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('entry');
__PACKAGE__->add_columns(qw/id url title created_on/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(
     bookmarks=>'Bookmark::Schema::Bookmark',{
	  'foreign.entry_id'=>'self.id'
     }
);
1;

・bookmarkとentryの関係、と、bookmarkとuserの関係
package Bookmark::Schema::Bookmark;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('bookmark');
__PACKAGE__->add_columns(qw/user_id entry_id comment/);
__PACKAGE__->set_primary_key('user_id', 'entry_id');
__PACKAGE__->belongs_to(
     entry=>'Bookmark::Schema::Entry',{
	  'foreign.id'=>'self.entry_id'
     }
);
__PACKAGE__->belongs_to(
     user=>'Bookmark::Schema::User',{
	  'foreign.id'=>'self.user_id'
     }
);
1;

で、ここからmany_to_manyにしてみる。
あんまり意味がありませんが・・。

いじくるのは、Bookmark::Schema::EntryとBookmark::Schema::User。
・userとbookmarkの関係にmany_to_manyを追記
package Bookmark::Schema::User;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('user');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(
     bookmarks=>'Bookmark::Schema::Bookmark',{
	  'foreign.user_id'=>'self.id'
     }
);
__PACKAGE__->many_to_many(
	  entries=>'bookmarks', 'entry'
);
1;

・entryとbookmarkの関係にmany_to_manyを追記
package Bookmark::Schema::Entry;
use strict;
our $VERSION = '0.01';
use warnings;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('entry');
__PACKAGE__->add_columns(qw/id url title created_on/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(
     bookmarks=>'Bookmark::Schema::Bookmark',{
	  'foreign.entry_id'=>'self.id'
     }
);
__PACKAGE__->many_to_many(
	  'users'=>'bookmarks', 'user'
);
1;


Bookmark::Schema::Bookmarkが、Bookmark::Schema::Entryと
Bookmark::Schema::Userの間を取り持ってくれている。

Bookmark::Schema::Userではentriesというリレーションシップを作って、
Bookmark::Schema::Userのhas_manyで記述したbookmarks経由で、
Bookmark::Schema::Bookmarkのuserリレーションシップを呼ぶ。

Bookmark::Schema::Entryではusersというリレーションシップを作って、
Bookmark::Schema::Entryのhas_manyで記述したbookmarks経由で、
Bookmark::Schema::Bookmarkのentryリレーションシップを呼ぶ。

こうすることで、「B::S::Entryから、あるエントリに紐づく複数のユーザ」や
「B::S::Userから、あるユーザに紐づく複数のエントリ」を取得できる。
ああ、まー使えるかも。

many_to_manyでは、アクセサは2つのリレーションシップのようなものをつくり
ともかくアクセサはhas_manyのように、ResultSetやオブジェクトのリス
トを返す。

上の例のBookmarkクラスのように、many_to_manyな関係を作りたい
2つのテーブルの間を取り持つクラスをリンクテーブルクラスと言う。

Bookmark::Schema::Entryの例に着目して、many_to_manyの記述を見る。
package Bookmark::Schema::Entry;
・・・
__PACKAGE__->has_many(
     bookmarks=>'Bookmark::Schema::Bookmark',{
	  'foreign.entry_id'=>'self.id'
     }
);
__PACKAGE__->many_to_many(
	  'users'=>'bookmarks', 'user'
);

package Bookmark::Schema::Bookmark;
・・・・
__PACKAGE__->belongs_to(
     user=>'Bookmark::Schema::User',{
	  'foreign.id'=>'self.user_id'
     }
);

Bookmark::Schema::Entryを見る。Bookmark::Schema::Entryからの視点で
は、Bookmark::Schema::Bookmarkがリンクテーブルクラスと呼ばれ
Bookmark::Schema::Bookmark::Schema::Userは外部クラスと呼ばれる。

many_to_manyだけ、他のリレーションシップと引数が違って
引数 = ($accessor_name, $link_rel_name, $foreign_rel_name, $attr?)
だった。

アクセサ名は今までと同じ。

$link_rel_nameパラメタは、many_to_manyを記述しているクラスのテーブ
ル(例ではBookmark::Schema::Entry)からリンクテーブル(例では、
Bookmark::Schema::Bookmark)へのhas_manyなリレーションシップのアク
セサ名を記述する。

$foreign_rel_nameパラメタには、リンクテーブルから外部テーブルへの
belongs_toなリレーションシップのアクセサ名を記述する。

many_to_manyを使うためには、データを格納している外部テーブル(オリ
ジナルテーブルとも呼ぶ)からリンクテーブルへのリレーションシップと、
リンクテーブルからmany_to_manyを記述したテーブル(エンドテーブルと
も呼ぶ) へのリレーションシップが必要となる。
many_to_manyリレーションシップを呼ぶと合計3つのリレーションシップ
を使うことになる。

many_to_manyリレーションシップでも、
has_manyと同じような感じで3つのメソッドが作られる。

上の例では以下の3つのメソッドがつくられる。

- users();
- add_to_users();
- set_users();

つまり、

- $accessor_name();
- add_to_ + $accessor_name();
- set_ + $accessor_name();

が作られる。使い方は

・add_to_
Arguments: ($foreign_vals | $obj), $link_vals?
  $actor->add_to_roles({ name => 'lead' }, { salary => 15_000_000});
      # creates a new My::DBIC::Schema::Role row object and the linking table
      # object with an extra column in the link

・set_
Arguments: (\@hashrefs | \@objs)
  my $actor = $schema->resultset('Actor')->find(1);
  my @roles = $schema->resultset('Role')->search({ role => 
     { '-in' -> ['Fred', 'Barney'] } } );
  $actor->set_roles(\@roles);
     # Replaces all of $actor's previous roles with the two named


add_to_は追加したオブジェクトからリンクテーブルオブジェクトが作ら
れる。追加したリンクテーブルのカラムは$link_valsで指定できる。

set_は、既存のリンクテーブルオブジェクトをset_で与えたオブジェクト
を入れ替える。既存のリンク関係は全部破棄されて、新しいリンクが作ら
れる。

詳しくはDBIx::Class::Relationship::Base




はー、長い。一応自分が使ったお試しスクリプトも貼っておく。

#!/usr/bin/perl
use strict;
use warnings;
use lib qw(./lib);
use Bookmark::Schema;

my $schema = Bookmark::Schema->connect;
$schema->storage->debug(1);

# 完全に削除
$schema->resultset('User')->search(     {} )->delete;
$schema->resultset('Bookmark')->search( {} )->delete;
$schema->resultset('Entry')->search(    {} )->delete;

my $u_rs = $schema->resultset('User');
my $b_rs = $schema->resultset('Bookmark');
my $e_rs = $schema->resultset('Entry');

$u_rs->create( { name => 'bw4' } );
$u_rs->create( { name => 'satou' } );

$e_rs->create(
    {   url =>
	 'http://gigazine.net/index.php?/news/comments/20070106_firefox_revenue/',
	 title => 'firefox',
    }
);
$e_rs->create(
    {   url   => 'http://www.popxpop.com/archives/2007/01/5firefox.html',
	 title => 'firefox',
    }
);

my $users_rs = $u_rs->search( {} );    # get ResultSet
while ( my $user = $users_rs->next ) { # use iterator(search once)
    print $user->name, ": search\n";
}

my $bw_users_rs = $u_rs->search_like( { name => 'bw%' } );    #LIKE get ResultSet
while ( my $user = $bw_users_rs->next ) {    # use iterator(search once)
    print $user->name, ": search like bw\n";
}

my $tmp_user;

$tmp_user = $u_rs->find( { name => 'satou' } );    # get Bookmark::Schema::User
# $bookmarks will be Bookmark::Schema::Bookmark
my $bookmarks = $tmp_user->bookmarks();    # has_many
while ( my $bm = $bookmarks->next ) {      # use iterator(search once)
    print $bm->comment, ": entry->bookmarks()\n";
}

$bookmarks = $tmp_user->bookmarks_rs();
while ( my $bm = $bookmarks->next ) {      # use iterator(search once)
    print $bm->comment, ": entry->bookmarks_rs()\n";
}

# has_many
$bookmarks = $tmp_user->add_to_bookmarks(
    {   entry_id => 1,
	 comment  => 'hahaha',
    }
);

# has_many
$bookmarks = $tmp_user->add_to_bookmarks(
    {   entry_id => 2,
	 comment  => 'fufufu',
    }
);

# get Bookmark::Schema::Bookmark
my $bookmark = $tmp_user->search_related( 'bookmarks', { entry_id => 2 }, )->first;
my $entry = $bookmark->entry;    # get Bookmark::Schema::Entry

# get ResultSet
my $bookmark_rs = $entry->search_related( 'bookmarks', { entry_id => 2 }, );
while ( my $bm = $bookmark_rs->next ) {    # use iterator(search once)
    print $bm->comment, ": search related\n";
    }

my $entries_rs = $tmp_user->entries();
while ( my $ent = $entries_rs->next ) {    # use iterator(search once)
    print $ent->url, ": entries()\n";
}

$tmp_user->add_to_entries(
    {   url   => 'http://www.popxpop.com/archives/2007/01/5firefox.xml',
	 title => 'firefox!!!',
    }
);
$entries_rs = $tmp_user->entries();
while ( my $ent = $entries_rs->next ) {    # use iterator(search once)
    print $ent->url, ": entries()\n";
}

my @ent_arr = $e_rs->search_like( { title => '%!%' } );
$tmp_user->set_entries( \@ent_arr );
$tmp_user->add_to_entries(
    { url => 'http://overlasting.dyndns.org/', title => 'my blog', } );
$entries_rs = $tmp_user->entries();
while ( my $ent = $entries_rs->next ) {    # use iterator(search once)
    print $ent->url, ": entries()\n";
}


書き始めたときは、あとで読み返すかな?と思ったけど、
記事にしちゃうと理解がおわっていて、後で見返すか不安。ま、いいか。

あと、書き忘れたけど、has(have)とbelongsは表裏一体の関係なので、
片方向からだけでなく、双方向になるように両方とも記述しておくと、
あとで楽ですよっていうか、双方向にするような慣習があるみたい。

はっきりしないところはDBICよりもRuby on Railsの資料を読んだほうが
わかったりするかもしれませんね。


関連するエントリ

[2007-01-03-4] DBICのリレーションが良くわからないのでメモ - 1対多
[2007-01-03-3] DBICのリレーションが良くわからないのでメモ - 1対1
[-] 3
投稿者:としのり  日時:23:59:59 | コメント | トラックバック |
blog comments powered by Disqus