TL;DR
こちらの記事をご覧下さい: 【Amon2のオレオレTips】Test::mysqldを使ってTengのSchemaクラスをラクラク生成
以下の内容は, こちらの記事を参考にして試みた作業の個人用メモのようなものです.
TengのSchemaとTeng::Schema::Dumper
例えばAmon2 + Tengといった環境でWebアプリを開発する場合, MyApp::DB::Schemaといった場所にTengのSchemaクラスを生成する必要があります. このSchemaクラスの生成については, 「Schemaファイルを手動で書く」という面倒な解決策以外に, Teng::Schema::Loaderを使って自動的に呼び出す方法と, Teng::Schema::Dumperを使って生成する方法があります.
Teng::Schema::Loaderは, Tengのインスタンス作成時にDBにアクセスして, DBからSchemaの情報を手に入れる為, 簡単に利用することができますが, 起動時の初動がやや遅くなります. もう一方のTeng::Schema::Dumperは, DBからSchemaの情報を手に入れるという点ではTeng::Schema::Loaderと同じですが, そのSchema情報を任意のファイルにダンプすることが出来ます. そのため, 通常時はダンプしたSchemaクラスを読み込んでおいて, DBの構成に変化があった時だけ, Teng::Schema::DumperでSchemaクラスを再ダンプする... といったことが実現出来ます.
Teng::Schema::Dumperについては, Teng Advent Calendarの「#14 schema dumper」の記事などが参考になります.
Test::mysqld
Teng::Schema::Dumperは, DBからSchemaの情報を入手する以上, データベースとしてMySQLを利用する場合には, どうしても事前にMySQLを立ち上げておく必要があります. また, 予めMySQL等々の(更新した)テーブルの定義を, 既存のテーブルに反映させるか, 或いは新しいダンプ用のデータベースを用意する必要があるので, その手間もかかります.
そこで登場するのがTest::mysqldです. Test::mysqldはデータベースを用いたテストをする際に, そのテスト時のみ使える一時的なデータベースを用意し, 更に起動や終了を自動的に行ってくれるモジュールです. 今回の場合, このモジュールが用意してくれたまっさらなデータベースに, テーブルの定義を適用させた上でTeng::Schema::DumperでSchemaクラスをダンプすればOK... という寸法です. 面倒なDBの起動や初期化, 終了などは, 全てTest::mysqldが自動的に処理してくれます.
この辺りの手法は, 冒頭で紹介した記事(【Amon2のオレオレTips】Test::mysqldを使ってTengのSchemaクラスをラクラク生成)と全く同じです. こちらの記事との違いは, Schemaクラスをダンプする為の仕組みを, Daiku用のDaikufileとして用意している, という点のみです.
Daiku
今回は, Test::mysqldとTeng::Schema::DumperでTengのSchemaクラスをダンプする「スクリプト」ではなく, Daiku用の「Daikufile」を紹介したいと思います. Daikuは, 元々は@tokuhiromさんが開発されていた, いわば「Perl版Rake」といった代物です. 最近は@songmuさんが開発者に加わって, 活発に開発が進められています.
Test::mysqldとTeng::Schema::DumperでTengのSchemaクラスをダンプするためのDaikufile
それでは, 早速ですがDaikufileを見て行きましょう.
#!perl
use strict;
use warnings;
use DBI;
use Path::Tiny;
use Teng::Schema::Dumper;
use Test::mysqld;
task 'dump_schema' => sub {
my $MYSQLD = Test::mysqld->new( my_cnf => { 'skip-networking' => '' })
or die $Test::mysqld::errstr;
my $dbh = DBI->connect($MYSQLD->dsn);
my $file_name = 'sql/mysql.sql'; # テーブル定義のファイル
my $source = path($file_name)->slurp_utf8;
for my $stmt (split /;/, $source) {
next unless $stmt =~ /\S/;
$dbh->do($stmt) or die $dbh->errstr;
}
my $schema_class = 'lib/MyApp/DB/Schema.pm'; # 出力先となるSchemaクラス
open my $fh, '>', $schema_class or die "$schema_class \: $!";
print $fh Teng::Schema::Dumper->dump(
dbh => $dbh,
namespace => 'MyApp::DB',
base_row_class => 'MyApp::DB::Row',
inflate => { },
);
close $fh;
};
Amon2の標準的なディレクトリ構成を想定しています.
プロジェクトのルートにこのコードを「Daikufile」という名称で保存し, daiku dump_schema
というコマンドで実行すると, sql/mysql.sql
という名称の定義ファイルから, lib/MyApp/DB//Schema.pm
へSchemaクラスを自動的に生成します.
例えば, 次のような定義ファイルがあった場合...
CREATE TABLE IF NOT EXISTS member (
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
created_at INTEGER,
updated_at INTEGER
);
生成されるSchemaクラスは, 次のようになります.
package MyApp::DB::Schema;
use strict;
use warnings;
use Teng::Schema::Declare;
base_row_class 'MyApp::DB::Row';
table {
name 'member';
pk 'id';
columns (
{name => 'id', type => 4},
{name => 'name', type => 12},
{name => 'created_at', type => 4},
{name => 'updated_at', type => 4},
);
};
1;
inflate/deflateを効かせたい場合, Teng::Schema::Dumperのdumpメソッドにおけるinflate
オプションで, どのようなコードを実行させるかを指定することができます.
my $inflate = <<'...';
inflate qr/.+_at/ => sub {
...
};
deflate qr/.+_at/ => sub {
...
};
...
print $fh Teng::Schema::Dumper->dump(
dbh => $dbh,
namespace => 'MyApp::DB',
base_row_class => 'MyApp::DB::Row',
inflate => {
member => $inflate,
},
);
Daikufileをこのように書いておくと...
package MyApp::DB::Schema;
use strict;
use warnings;
use Teng::Schema::Declare;
base_row_class 'MyApp::DB::Row';
table {
name 'member';
pk 'id';
columns (
{name => 'id', type => 4},
{name => 'name', type => 12},
{name => 'created_at', type => 4},
{name => 'updated_at', type => 4},
);
inflate qr/.+_at/ => sub {
...
};
deflate qr/.+_at/ => sub {
...
};
};
1;
出力されるSchemaクラスは, このようになります.
ここまで用意すれば, mysql.sqlを書き換えてから, おもむろにdaiku dump_schema
を叩けば, 自動的に最新のmysql.sqlに従ったTengのSchemaクラスが生成されるようになる, という寸法です.
まとめ
Teng::Schema::DumperとTest::mysqld, そしてDaikuを使ってTengのためのSchemaクラスを自動的に生成する方法を紹介しました. 次回は, Test::mysqldとDaikuにDBIx::FixtureLoaderを組み合わせて, 自動的にTest::mysqldのcopy_data_fromで使うデータを生成する方法を紹介したいと思います.
...まぁ, 内容的には, @songmuさんのブログの, DBIx::FixtureLoaderってのを書きましたや, Test::mysqldのcopy_data_fromでテストが更に捗る話といった記事の内容をDaikuを使ってやってみた, というだけなので, 例のごとく上記の2記事を見た方が幸せになるかと思います!