• このエントリーをはてなブックマークに追加

はじめに

Amon2 アプリケーションでデータベース(mysql)を操作するのに Teng を使う際の「スキーマ定義」には, memememomo さんの次のエントリ:

のスクリプト make_schema.pl を基にいくらか修正したものを使っています.

これまでは特に inflate / deflate を使うことなく済んできたのですが,今になってちょっと必要になってきたため,Teng::Schema::Dumper のドキュメント を確認しながら同スクリプトを修正すればよいかな,と最初は考えたのですが,ちょっといただけません.

考えてみたこと

  1. Teng::Schema::Dumper に渡す inflate ルール自体も Perl なコード(な文字列)なので,それだけを inflate.pl みたいな感じで別ファイルで持たせ,スクリプトに読ませればいいんでないか.
  2. 複数のテーブルに対する定義の場合,スクリプトで読んだ後の処理がちょっとメンドウか?では,特定のディレクトリの下に,テーブルごとのファイルを作ろう.
  3. ファイル名は {テーブル名}.pl とかしておけば,見た目的にも処理的にもまぁなんとかなるのではないか.

できあがったスクリプト

ということで,inflate ルールなファイルを呼んでそれを Teng::Schema::Dumper に渡すための処理を追加するなどして,次のようなコードになりました:

=head1 NAME
 
make_schema - generates schema definition for Teng
 
=head1 SYNOPSIS
 
./bin/make_schema <options>
 
# or
 
perl scrip/make_schema.pl <options>
 
=head1 OPTIONS
 
=over4
 
=item --namespace=MyApp::DB
 
=item --inflate-dir=etc/schema/inflate [optional]
 
=back
 
=cut
use strict;
use warnings;
use Getopt::Long qw/:config posix_default no_ignore_case bundling/;
use Teng::Schema::Dumper;
use Text::Xslate qw/mark_raw/;
use File::Basename;
use Pod::Usage;
use Data::Section::Simple qw/get_data_section/;
use MyApp;
 
my $c = MyApp->bootstrap();
 
my %opts = (
    ns          => undef,
    inflate_dir => $c->base_dir() . '/etc/schema/inflate',
);
GetOptions (
    'ns|namespace=s' => \$opts{ns},
    'inflate-dir=s'  => \$opts{inflate_dir},
) or pod2usage(1);
$opts{ns} or pod2usage(1);
 
my $ns          = $opts{ns};
my $inflate_dir = $opts{inflate_dir};
 
my %inflate;
for my $f ( glob( "$inflate_dir/*.pl" ) ) {
    my $name = basename $f, '.pl';
    open my $fh, '<', $f or die $!;
    my $code = do { local $/; <$fh> };
    $code =~ s/^(.*\S)/    $1/mg;  # indent 4
    $inflate{$name} = $code;
}
 
my $schema = Teng::Schema::Dumper->dump(
    dbh       => $c->dbh(),
    namespace => $ns,
    inflate   => \%inflate,
);
 
my $out = Text::Xslate->new->render_string(
    get_data_section('template.tx'),
    {
        namespace => $ns,
        schema    => mark_raw( $schema ),
        plugins   => [],
    },
);
 
print $out;
__DATA__
 
@@ template.tx
package <: $namespace :>;
use parent 'Teng';
: for $plugins -> $plugin {
__PACKAGE__->load_plugin('<: $plugin :>');
: }
1;
 
<: $schema :>

このスクリプトをラップするためのシェルスクリプトもついでに:

#!/bin/bash
BASEDIR=$(cd $(dirname $0)/.. && pwd)
exec perl -I$BASEDIR/lib $BASEDIR/script/make_schema.pl $@

実行例

データベース構成

次の SQL で定義した 2つのテーブルを持つデータベースを対象とします:

CREATE TABLE shop (
  id INT AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  url VARCHAR(255) NOT NULL,
  PRIMARY KEY (id)
);
CREATE TABLE USER (
  id INT AUTO_INCREMENT,
  name VARCHAR(32),
  nickname VARCHAR(32) NOT NULL,
  PRIMARY KEY (id)
);

inflate ルールが存在しないとき

% ./bin/make_schema --namespace=MyApp::DB
package MyApp::DB;
use parent 'Teng';
1;
 
package MyApp::DB::Schema;
use Teng::Schema::Declare;
table {
    name 'shop';
    pk 'id';
    columns (
        {name => 'url', type => 12},
        {name => 'name', type => 12},
        {name => 'id', type => 4},
    );
};
 
table {
    name 'user';
    pk 'id';
    columns (
        {name => 'nickname', type => 12},
        {name => 'name', type => 12},
        {name => 'id', type => 4},
    );
};
 
1;

規定のディレクトリに inflate ルールが存在するとき

inflate ルールのディレクトリを {アプリケーションのルート}/etc/schema/inflate としているので,その直下に user.pl:

inflate 'nickname' => sub {
    my ($v) = @_;
    return uc($v);
};

が存在する場合,次のような出力となります:

% ./bin/make_schema --namespace=MyApp::DB
package MyApp::DB;
use parent 'Teng';
1;
 
package MyApp::DB::Schema;
use Teng::Schema::Declare;
table {
    name 'shop';
    pk 'id';
    columns (
        {name => 'url', type => 12},
        {name => 'name', type => 12},
        {name => 'id', type => 4},
    );
};
 
table {
    name 'user';
    pk 'id';
    columns (
        {name => 'nickname', type => 12},
        {name => 'name', type => 12},
        {name => 'id', type => 4},
    );
    inflate 'nickname' => sub {
        my ($v) = @_;
        return uc($v);
    };
};
 
1;

テーブル user の定義部分に,inflate ルール user.pl の内容が含まれていることがわかります.

inflate ルールのディレクトリが既定の場所でないとき

inflate ルール shop.pl:

use URI;
inflate 'url' => sub {
    my ($v) = @_;
    return URI->new($v);
};

inflate ルール user.pl:

use Mock::Inflate::Name;
inflate 'name' => sub {
    my ($v) = @_;
    return Mock::Inflate::Name->new( name => $v );
};

これら 2つの inflate ルールが規定でない別のディレクトリ,例えば etc/schema_inflate,に存在するとき,--inflate-dir オプションでそれを指定することもできます:

% ./bin/make_schema --namespace=MyApp::DB --inflate-dir=etc/schema_inflate
package MyApp::DB;
use parent 'Teng';
1;
 
package MyApp::DB::Schema;
use Teng::Schema::Declare;
table {
    name 'shop';
    pk 'id';
    columns (
        {name => 'url', type => 12},
        {name => 'name', type => 12},
        {name => 'id', type => 4},
    );
    use URI;
    inflate 'url' => sub {
        my ($v) = @_;
        return URI->new($v);
    };
};
 
table {
    name 'user';
    pk 'id';
    columns (
        {name => 'nickname', type => 12},
        {name => 'name', type => 12},
        {name => 'id', type => 4},
    );
    use Mock::Inflate::Name;
    inflate 'name' => sub {
        my ($v) = @_;
        return Mock::Inflate::Name->new( name => $v );
    };
};
 
1;

※ Mock::Inflate::Name は,Teng::Schema::Dumper のサンプルコードを参考に.

ファイル構成の差分

amon2-setup.pl --flavor=Basic MyApp 直後と,上記コードや動作確認などのためにファイルを追加した状態との差分です:

--- .tree.init	2013-03-30 14:50:59.000000000 +0900
+++ .tree.master	2013-03-30 14:51:12.000000000 +0900
@@ -1,6 +1,8 @@
 .
 ├── Build.PL
 ├── app.psgi
+├── bin
+│   └── make_schema
 ├── config
 │   ├── deployment.pl
 │   ├── development.pl
@@ -8,6 +10,13 @@
 ├── cpanfile
 ├── db
 │   └── development.db
+├── etc
+│   ├── schema
+│   │   └── inflate
+│   │       └── user.pl
+│   └── schema_inflate
+│       ├── shop.pl
+│       └── user.pl
 ├── lib
 │   ├── MyApp
 │   │   ├── Web
@@ -16,6 +25,8 @@
 │   │   │   └── ViewFunctions.pm
 │   │   └── Web.pm
 │   └── MyApp.pm
+├── script
+│   └── make_schema.pl
 ├── sql
 │   ├── mysql.sql
 │   └── sqlite.sql
@@ -66,4 +77,4 @@
     ├── 01_pod.t
     └── 02_perlcritic.t
 
-18 directories, 48 files
+24 directories, 53 files

おわりに

Teng のスキーマ定義における inflate ルールな Perl のコードを外部に置き,それを読み込んだ上でスキーマ定義コードを生成する,そんなスクリプトを書いてみたお話でした.

inflate ルールとか,最初に決めたらあとで更新することなさげなので,管理の面から見れば,ファイルが増えた分,コストがあがるのかもしれませんがw