DBIC で後付けの relationship を定義してたらうまく動かなくなった

昔かいた DBIC を使うコードが,DBIC を新しくしたら動かなくなった。

うまくいく例

use strict;
use warnings;

# artist の定義
package My::Schema::Result::Artist;
use base qw( DBIx::Class );
__PACKAGE__->load_components(qw( Core ));
__PACKAGE__->table('artist');
__PACKAGE__->add_columns(qw( id name ));
__PACKAGE__->set_primary_key('id');

# album の定義
package My::Schema::Result::Album;
use base qw( DBIx::Class );
__PACKAGE__->load_components(qw( Core ));
__PACKAGE__->table('album');
__PACKAGE__->add_columns(qw( id title artist_id ));
__PACKAGE__->set_primary_key('id');

# relationship の定義
My::Schema::Result::Artist
    ->has_many('albums', 'My::Schema::Result::Album', 'artist_id');
My::Schema::Result::Album
    ->belongs_to('artist', 'My::Schema::Result::Artist', 'artist_id');

# スキーマの定義
package My::Schema;
use base qw( DBIx::Class::Schema );
__PACKAGE__->register_class('artist', 'My::Schema::Result::Artist');
__PACKAGE__->register_class('album',  'My::Schema::Result::Album');


# メイン
package main;

my $schema = My::Schema->connect('dbi:SQLite:test.db');

my @artists
    = $schema->resultset('artist')->search(
        {
            # conditions
        },
        {
            prefetch => 'albums',
        },
    )->all();

全体スキーマ定義に各テーブルスキーマを導入するために register_class() を使っているけれど,普通は load_namespaces()load_classes() を使うんだと思う。あるいは DBIx::Class::Schema::Loader を使ってクラス上でスキーマを定義するんじゃなくて DB からスキーマ定義を自動生成するほうがイマドキかもしれない。

が,ちょっと(後述するような)事情があって上記のように register_class() を使ってる。


実行すると,

$ DBIC_TRACE=1 perl test.pl

SELECT me.id, me.name, albums.id, albums.title, albums.artist_id
  FROM artist me
  LEFT JOIN album albums ON albums.artist_id = me.id
  ORDER BY albums.artist_id: 

うまくいってる。

うまくいかない例

んが,古いコードでは下記のようになっていた。

# artist の定義
package My::Schema::Result::Artist;
# ...... snip snip snip ...... #

# album の定義
package My::Schema::Result::Album;
# ...... snip snip snip ...... #

# スキーマの定義
package My::Schema;
use base qw( DBIx::Class::Schema );
__PACKAGE__->register_class('artist', 'My::Schema::Result::Artist');
__PACKAGE__->register_class('album',  'My::Schema::Result::Album');

# メイン
package main;

# relationship の定義
My::Schema::Result::Artist
    ->has_many('albums', 'My::Schema::Result::Album', 'artist_id');
My::Schema::Result::Album
    ->belongs_to('artist', 'My::Schema::Result::Artist', 'artist_id');

# ...... snip snip snip ...... #

さきほどの例との違いは,全体スキーマに register_class() したあとに各テーブルスキーマの relationship を定義しているところ。

ふつうの感覚ではこれらのコードの働きにさほど違いがないように思えるが,さいきんの DBIC で実行すると,

No such relationship albums at /usr/lib/perl5/site_perl/5.8.8/DBIx/Class/Schema.pm line 1027

のように怒られる(古い DBIC では大丈夫だった)。


なんでこのようなコードレイアウトになってたかというと,当該アプリケーションでは,スキーマ定義にまつわる部分(冒頭〜メインまで)を SQL-TranslatorSQL 文から自動生成していたから。

自動生成+カスタムの relationship(実際には別モジュールとしている)というしくみになっているので,上記の「うまくいく」例のようにコードの並び順を変えることは望ましくない。スキーマを変えるたびに手で変えることになるから(もちろんそこを含めて自動生成するようなスクリプトを書いてもいいんだけど)。

うごかすには

深追いする体力も能力もないので,アドホックに解決できないかといろいろ試行錯誤してたら,下記のようにすると一応動くようになった。

# artist の定義
package My::Schema::Result::Artist;
# album の定義
package My::Schema::Result::Album;

# スキーマの定義
package My::Schema;
use base qw( DBIx::Class::Schema );
__PACKAGE__->register_class('artist', 'My::Schema::Result::Artist');
__PACKAGE__->register_class('album',  'My::Schema::Result::Album');

# メイン
package main;

# relationship の定義
My::Schema::Result::Artist
    ->has_many('albums', 'My::Schema::Result::Album', 'artist_id');
My::Schema::Result::Album
    ->belongs_to('artist', 'My::Schema::Result::Artist', 'artist_id');

### もっかい register_class する
My::Schema->register_class('artist', 'My::Schema::Result::Artist');
My::Schema->register_class('album',  'My::Schema::Result::Album');

# ...... snip snip snip ...... #

なんのことはない,relationship の設定をおこなったあとに,再度全体スキーマに register_class() しなおしているだけ。

副作用があったらイヤだけど,とりあえずこれで動いてる。もっとよい案があったら教えてください。


いちいち register_class() を列挙するのが面倒なら

### もっかい register_class する
if (eval { My::Schema->can('class_mappings') }) {
    my $mappings = My::Schema->class_mappings;
    while (my ($class, $rs) = each %$mappings) {
        My::Schema->register_class($rs, $class);
    }
}

みたいに,My::Schema に登録されているスキーマ定義クラスリストをもとに自動的に register_class() を実行するように書くこともできる。でも非公開 API を使っているので今後うまく動かなくなる可能性もある。