DBIx::Class::QueryLog

CatalystでDBICのプロファイリングがしたかったのでDBIx::Class::QueryLogを使ってみた。

$c->model('Model')->storage->debugobj($ql);
$c->model('Model')->storage->debug(1);
$c->stash->{querylog} = $ql;

とコントローラーに書いておく。ほんとはモデルのほうに書くべきなのか?クックブックよくわからん。

あとはViewを用意すれば、結果(この場合は遅い順に5つ)が画面に吐き出される。

log

Catalyst::Plugin::DBIC::Proflierもあとで。

DBICで集約関数を使ってTTに送る場合に

タグのテーブルなんかでタグをカウントしてtagname(count)とか出力したいときにDBICだとどうやんのかな?と。

とasを指定しておけば、TTのほうで

t.get_column('posts_count')

でカウントが表示できる。

タグの名前とカウント数を表示させたい場合にはこのままTTでいいと思うが、タグクラウドにしたい場合は、コントローラーでHTML::TagCloud::ExtendedとかHTML::TagCloud を使ったほうがいいのかもと。

あとYAPCのチケットとった。DBICネタが密かに楽しみだったり。

DBICのSingleメソッド

というか、findとの違いがいまいち。searchは分かるのでいいとして、

DBIx::Class::ResultSet - Responsible for fetching and creating resultset. - search.cpan.org

Inflates the first result without creating a cursor if the resultset has any records in it; if not returns nothing. Used by "find" as an optimisation.

カーソルを作らずに最初の結果をインフレートする、ただし結果がない場合はなにも返さない。

じゃぁ、findって何?ってことになるわけだが。

findって主キーから探すときにしか使ってなかったけど、ユニーク制約かけてあるとこから探してもいいのね。知らんかった。

DBIx::Class::ResultSet - Responsible for fetching and creating resultset. - search.cpan.org

Finds a row based on its primary key or unique constraint.

singleだと

Return Value: $row_object?

と?がついているのに、findは

Return Value: $row_object

となっているということは、singleはオブジェクトを返すかもしれないし返さないかもしれないのに対し、findの場合は必ずオブジェクトが返ってくるということか。

じゃぁ、ユニーク制約はどこでどうすんの?ってことだけど、 add_unique_constraintにあるようにDBICに制約属性ふっておけばいいらしいので、DBICが制約が張られているのをチェックしてよきに計らってくれるのだろう。(確認してないけど)

というわけで、多対多とか一対多の場合に制約属性ふっておくと検索するときに楽になるのね。

DBICでprefetchとjoin

ありがたい。色々勉強になりました。

Hatena::Diary::Neko::kak 500 Internal Server Error - CatはしらんけどSledgeなら

基本コントローラーでRSを作りまくってstashにぶち込みまくって Viewの方で実際にクエリを実行する感じですね。

というわけでcookbookみながら悩んでみた。いじってるのは、こんな感じのテーブルなので、

多対多対多

user_bookmarksテーブルに対してイテレータをまわすことにした。 あとcatlystだとautoっていう最初(のほうに)に呼ばれるメソッドがあるので、そこにユーザーが存在するかどうかのバリデーション書いた。

sub auto : Private {
    my ($self, $c, $action, $username) = @_;

    $c->stash->{user} = 
      $c->model('PblDB::User')->single({name => $username});

    unless (defined($c->stash->{user}) || $c->req->path =~ m!^bookmarks/?$!) {
    $c->response->redirect($c->uri_for('/'));
    return 0;
    }

    return 1;
}

sub default : Private {
    my ( $self, $c ) = @_;

    $c->stash->{user_bookmark_it} = 
    $c->model('PblDB::UserBookmark')->search(
          {user_id => $c->stash->{user}->id},
          {       
             prefetch => [qw/user bookmark/],
             join => [qw/user bookmark/],
             order_by => 'me.id DESC'
           }
           );
}

あと、ユーザーバリデーションしてる件に関しては、

livedoor クリップ - Hatena::Diary::Neko::kak 500 Internal Server Error - CatはしらんけどSledgeなら

個人的には、バリデーションでユーザがいるかどうかはチェックしないなー。 そこでユーザの存在がわかっちゃうとあれかなと。まぁ、場合によるね。

とか

ってやった場合に、ユーザがいない場合にそんなメソッドがないよってエラーになっちゃうので、バリデーションしたというのが経緯なのだが、join使えば、

sub default : Private {
    my ( $self, $c, $action, $username ) = @_;

    $c->model('PblDB::UserBookmark')->search(
          {'user.name' => $username},
          {
             prefetch => [qw/user bookmark/],
             join => [qw/user bookmark/],
             order_by => 'me.id DESC'
          }
          );
}

と書けばよいのでこれでもいいかもしれんと思った。

あと、実際のサービスでそういった場合の処理はどうしてんのかなと気になったので僕が使っているSBSで調べてみた。

  • はてなブックマーク
  • トップページに飛ばされる
  • livedoor clip
  • unregistration.html
  • del.icio.us
  • エラーページ

ふむー。

DBICで(多対多)対多

相変わらずCatalyst,DBICと戯れてる。で、テーブルも複雑になってくると。

今回こんな感じのテーブルを扱ってみたが、なかなかうまく書けん。

m2m2m

User,Bookmarkからサーチする場合

うまい書き方が分からないので、リレーション張らんでもええような、ベタベタな構文になってしまう。

勝手に二重のイテレータみたいになってくれればいいのに。

Tagからサーチする場合

my $ub_it = $c->model('PblDB::Tag')->find($tag->id)->user_bookmarks;
while(my $user_bookmark =  $ub_it->next){
    $item->[$i]->{'title'} = $user_bookmark->bookmark->title;
    $item->[$i]->{'abstract'} = $user_bookmark->bookmark->abstract;

my $tag_it = $c->model('PblDB::UserBookmark')
    ->find($user_bookmark->id)->tags;

みたいにuser_bookmarksテーブルのイテレータをまわさないといけないのだろうか?

ここらへんのも参考にする。

graphvizでテーブルスキーマ

地道にそれっぽいのを用意しようとするならば、こんな感じ

digraph sample {
   graph [size = "6, 8"];   
   graph [nodesep = 0.7];
   node [shape = record, height = 0.01,   
         fontname = "Helvetica", fontsize = 9];

   bookmark [label = "{bookmark|id\nname\nuri\ndescription\ntime_created}"];
   tagmap [label = "{tagmap|id\nbookmark_id\ntag_id}"];
   tag [label = "{tag|id\nname}"];

   tagmap -> tag [label = "belongs_to"];
   tagmap -> bookmark [label = "belongs_to"];
   tag -> tagmap [label = "has_many"];
   tag -> tagmap [label = "many_to_many"];
   bookmark -> tagmap [label = "has_many"];
   bookmark -> tagmap [label = "many_to_many"];
}

dotコマンドで実行

$ dot -Tpng db.dot -o db_sample.png

many to many

もう少し、スキーマ図っぽくしたいなぁと色々探したらSQL::Translatorというものをみつけた。

後で試す。

DBICで多対多

多対多のサンプルとしてTags: Database schemasの“Toxi” solutionをDBICで書いてみる。

関連を表すテーブルに対しては、1対多の関係を設定しておいて、更にmany_to_manyを追加する

図にしてみたらわかりやすくなった。

manytomany

ちなみに、1対多はこちらを、多対多はこちらを参考にしました。

Kzfm::Schema

use 5.008008;
use strict;
use warnings;
use base 'DBIx::Class::Schema';
our $VERSION = '0.01';

__PACKAGE__->load_classes(qw/Tag Bookmark TagMap/);

1;

Kzfm::Schema::Tag

use strict;
use warnings;
use base 'Kzfm::Schema';

__PACKAGE__->load_components(qw/PK::Auto::SQLite Core/);
__PACKAGE__->table('tag');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(tagmap => 'Kzfm::Schema::TagMap','tag_id');
__PACKAGE__->many_to_many('bookmarks' => 'tagmap','bookmark_id');

1;

Kzfm::Schema::Bookmark

use strict;
use warnings;
use base 'Kzfm::Schema';

__PACKAGE__->load_components(qw/PK::Auto::SQLite Core/);
__PACKAGE__->table('bookmark');
__PACKAGE__->add_columns(qw/id name uri description time_created/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many(tagmap => 'Kzfm::Schema::TagMap','bookmark_id');
__PACKAGE__->many_to_many('tags' => 'tagmap','tag_id');

1;

Kzfm::Schema::TagMap

use strict;
use warnings;
use base 'Kzfm::Schema';

__PACKAGE__->load_components(qw/PK::Auto::SQLite Core/);
__PACKAGE__->table('tagmap');
__PACKAGE__->add_columns(qw/id bookmark_id tag_id/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->belongs_to('bookmark_id' => 'Kzfm::Schema::Bookmark');
__PACKAGE__->belongs_to('tag_id' => 'Kzfm::Schema::Tag');

1;

制約の部分だけ、考えてみる。

__PACKAGE__->belongs_to('カラム名(アクセサ?)' => 'スキーマ(pm)');
__PACKAGE__->has_many(テーブル名 => 'スキーマ(pm)','カラム名(アクセサ?)');
__PACKAGE__->many_to_many('任意のキー名' => 'テーブル名','多対多関係のカラム');
  • belongs_toはまぁわかりやすい。
  • many_to_manyは任意にキー名決められるのにhas_manyはキー値にテーブル名指定しないとエラー吐くのが謎。
  • many_to_manyでテーブル名のとこをKzfm::Schema::TagMapってやるのが駄目な理由がよくわからない。

あと、さらにユーザーを追加する場合に、ユーザーとブックマークも多対多の関係になるけど、こっちも正規化したほうがいいのか、むしろやりすぎないほうがむしろいいのか悩む。

追記 06.12.04

ボケボケだったことに気付いた。

DBICで多対多の設定を

かなりボケボケだった。

こういうことらしい。

DBIx::Class::Relationship - Inter-table relationships - search.cpan.org

PACKAGE>$method_name('relname', 'Foreign::Class', $cond, $attrs);

というわけで、こんな感じの理解で。

__PACKAGE__->belongs_to(アクセサ名(任意) => 
    '従うクラス','従うクラスのIDを持つ属性名');
__PACKAGE__->has_many(アクセサ名(任意) => 
    'クラス','自分のクラスのIDをもつ属性名');
__PACKAGE__->many_to_many(アクセサ名(任意) => 
    'has_manyでのアクセサ名','belongs_toでのアクセサ名');

データベースを指しているのかクラスを属性を指してるのか良く分かってなかった。というかO/RマッパーでDBのカラム名っておかしいよって何で気づかなかったんだろう。

  • many_to_manyは任意にキー名決められるのにhas_manyはキー値にテーブル名指定しないとエラー吐くのが謎。
  • has_many(アクセサ名(任意) => 'クラス','カラム名')でOK
  • many_to_manyでテーブル名のとこをKzfm::Schema::TagMapってやるのが駄目な理由がよくわからない。
  • has_manyで指定したアクセサ名が必要なのでダメ。

なんとなくこんな感じか。

belongs_toとhas_oneとmight_haveの関係

DBICのMLより

[Dbix-class] Difference between belongs_to and has_one

Belongs to is a child -> parent relationship Has one is a sibling <-> sibling relationship?

has_oneはちょっと違うんじゃないかと。

101号室より愛をこめてに詳しい解説があった。

家族の関係、核家族とか最近の4人家族のイメージだと

  • has_one,has_many
  • 親からみた子
  • might_have
  • 兄弟姉妹みたいな関係(いるかもしれんしいないかもしれん)
  • belongs_to
  • 子供からみた親の関係

ぐらいに捉えておけばいいのか。

タギングの実装方法に関して

ここ半年以上、pubmedに特化したdel.icio.usクローンをcatalystで作って運用している。個人的には重宝してるが、Catalyst覚えたての頃作ったので、コードが汚かったり、タギングのあたりに色々不満があったので最近また書き直してる。

書き直すモチベーション自体が、DBICを覚えたいという動機だったりするので、ほとんどスクラッチからっていう状態になってるが、pathinfoの再考できたりするのでこれはこれでいい感じ。

で、ナイスなタイミングでTags and search and DBIx::Classというエントリがでてたので、ちょっと参考にしようかと。

コメントで、Mysqlでの例が紹介されてたけど、 多分Tags: Database schemas のことでしょう。でも、これって単に正規化のレベルの話なんじゃ?

いま動いてるのはとりあえずブックマークサービスを用意したいという要求から始まっていたので、MySQLiciousみたいなタグをスペース区切りで一つのフィールドに押し込むようにしてたんだけど、やっぱ多対多っぽくちゃんと正規化(“Toxi” solutionみたいに)すべきかナァと。

でもこの場合、DBICって面倒じゃないんだろうか?deleteするときは一気に消せばいいだけだけど(cascadeってどうなるんだろうか?)、タグを消したり増やしたりする場合にはどういう感じに処理するんだろう?

MySQLicious みたいなテーブル構成だと何も考えずにアップデートすればいいだけだったのであんま考えなくて楽だったんだよね。まぁ、逆にタグの集計とか面倒なんだけど。

というわけで、そのうちちゃんと書いてみようと思った。