日向夏特殊応援部隊

俺様向けメモ

DBIx::Class::Schema::Loaderの手動スキーマ生成、初心者向けチュートリアル

と言う訳で自分なりに色々調べてみた。

テスト用データベース定義

CREATE TABLE `User` (
  `user_id` bigint(20) NOT NULL auto_increment,
  `name` varchar(255) character set latin1 default NULL,
  `created_on` datetime default NULL,
  `updated_on` datetime default NULL,
  PRIMARY KEY  (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `Book` (
  `book_id` bigint(20) NOT NULL auto_increment,
  `name` varchar(255) character set latin1 default NULL,
  `created_on` datetime default NULL,
  `updated_on` datetime default NULL,
  PRIMARY KEY  (`book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `BookShelf` (
  `bookshelf_id` bigint(20) NOT NULL auto_increment,
  `user_id` bigint(20) default NULL,
  `book_id` bigint(20) default NULL,
  `created_on` datetime default NULL,
  `updated_on` datetime default NULL,
  PRIMARY KEY  (`bookshelf_id`),
  UNIQUE KEY `user_book_uidx` (`user_id`,`book_id`),
  KEY `book_id` (`book_id`),
  CONSTRAINT `bookshelf_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `bookshelf_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`book_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ER図にするとこんな感じ。


DBIx::Class::Schema::Loaderを使ってSchemaクラスを生成する

とりあえずはCatalystとかとの連携を考えないで、ネイティブなDBICの事だけで考えます。

適当なディレクトリに行って、

$ module-starter --module DBICTest::DBIC::Schema --distro DBICTest-DBIC

みたいな感じでモジュールのスケルトンを作ります。

$ cd DBICTest-DBIC
$ mkdir -p bin
$ touch bin/update_schema.pl
$ chmod +x bin/update_schema.pl

とかやっておいて、このupdate_schema.plの内容をほとんどtypesterさんの奴のパクりで、

#!/usr/bin/perl

use strict;
use warnings;

use FindBin;
use File::Spec;

# ここは敢えてコメントアウトしておく
# use lib File::Spec->catfile( $FindBin::Bin, qw/.. schema/ );

use DBIx::Class::Schema::Loader qw/make_schema_at/;

die unless @ARGV;

make_schema_at(
    'DBICTest::DBIC::Schema',
    {   components => [ 'ResultSetManager', 'UTF8Columns' ]
        ,    # デフォでloadするcomponents
        dump_directory => File::Spec->catfile( $FindBin::Bin, '..', 'lib' )
        ,    # 出力先のディレクトリ
        really_erase_my_files => 1
        ,    # 元にあったファイルを再生成時に消すかどうか
        debug => 1,
    },
    \@ARGV,
);

んでもって、これを実行する。

$ ./bin/update_schema.pl dbi:mysql:database=dbictest root

みたいな感じで。

こうするとlibディレクトリ以下に、

$ find ./lib -name "*.pm"
./lib/DBICTest/DBIC/Schema/Book.pm
./lib/DBICTest/DBIC/Schema/Bookshelf.pm
./lib/DBICTest/DBIC/Schema/User.pm
./lib/DBICTest/DBIC/Schema.pm

みたいなのが出来ますよって話ですね。ここまでは非常に簡単なおさらいでした。

自動生成 + 手動生成

やはり色々と自動生成だけだとかゆい所まで手が届かなかったりします。
例えばMyISAM使ってるならリレーション設定は自動でやってくれないし、DBIC::Schemaの派生クラスなどにメソッド生やしたいとかそういう需要がある場合には、
自動生成で毎回作ってると色々と困る訳です。

ここでソリューションが二通りあって、

  • 別のINCパスに生やしたい差分を定義したモジュールを置いて、update_schema.plを実行する時のみ@INCにパスを通して実行 (typester版)
  • 生成されたモジュールのmd5sum以下に差分を定義する

と言う2パターンがあります。

別のINCにテンプレ

例えばこんな感じです。

$ mkdir -p schema/DBICTest/DBIC/Schema
$ touch schema/DBICTest/DBIC/Schema/User.pm

とかして、User.pmにて

package DBICTest::DBIC::Schema::User;

sub hoge {}

1;

などと書いておきます。
さらに、update_schema.plのuse lib部分のコメントアウトを外します。で再度実行する。

すると ./lib/DBICTest/DBIC/Schema/User.pm のファイル末尾付近に、

# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WFbbTfTFDFr/kewSj3QwAw
# These lines were loaded from '/private/tmp/DBICTest-DBIC/schema/DBICTest/DBIC/Schema/User.pm' found in @INC.# They are now part of the custom portion of this file# for you to hand-edit.  If you do not either delete# this section or remove that file from @INC, this section# will be repeated redundantly when you re-create this# file again via Loader!
package DBICTest::DBIC::Schema::User;

sub hoge {}

1;
# End of lines loaded from '/private/tmp/DBICTest-DBIC/schema/DBICTest/DBIC/Schema/User.pm'

として、自前で定義した物が挿入されます。
なんでカスタムであれこれしたければこの辺りをゴニョゴニョすれば良い訳です。

差分を直に書く方法

実は「別のINCにテンプレ」方式だと一点問題があって、Schemaクラスの拡張定義を書けないと言う点。
と言うのもDBIx::Class::Schema::Loaderのmake_schema_atは最終的にモジュールをファイルに書き出す際に、DBIx::Class::Schema::Loader::Base内で、

  • _load_tables()
    • _load_external()
  • _dump_to_dir()
  • _write_classfile()

みたいな流れになっています。

この_load_externalこそ他のINCにあるテンプレを読み込む処理に他ならないのですが、_load_tables()の中で抽出したDBテーブルに対応したモジュールに限定しているのでSchemaに対するテンプレを記述しても追記されません。

そこで差分を直接書く方法を取ってみます。

update_schema.plで二点変更します。

  • use libを再びコメントアウト
  • make_schema_atのreally_erase_my_filesを0にする (つまり再生成時に消さない)

として、./lib/DBICTest/DBIC/Schema.pm のファイル末尾付近で、

# Created by DBIx::Class::Schema::Loader v0.04004 @ 2008-03-18 16:56:43
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WFbbTfTFDFr/kewSj3QwAw

sub fuga {}

# You can replace this text with custom content, and it will be preserved on regeneration

こんな風にしておきます。そして再生成すると、きちんとコメントで囲われた部分の領域は保持されています。
意地悪して、データベースで、

ALTER TABLE User ADD COLUMN nickname VARCHAR(255);

として新しいカラムをわざと増やします。再びupdate_schema.plを実行してみると、
きちんとnicknameカラムの定義が増えてて、さらに自前で定義したfugaメソッドも残っている事が分かります。

まとめ

と言う訳でちょこまかDB定義を弄りながらやる場合は今回の手法のいずれかでやるのが良さそう。
Schemaに自動生成以上の何かをしたい場合は後者の手法しかないです。

あるいはSchema::Loader自体に手を加えるかですかね。
ふー、疲れたぜ。