#5「GitDDLまじイノベーティブ」 tech.kayac.com Advent Calendar 2012 | tech.kayac.com - KAYAC engineers' blog
が便利そうだなーと思って。
でもGitと絡めなくても、Webアプリにおいて「現在の環境で使用するデータベース」と「有るべきスキーマの状態を示すDDLファイル」の差分を取って埋めることができればそれだけで十分使える気がする、と思って一つの運用方法を考えてみた。
もちろんGitDDL使っても良いのだけど、SQL::Translatorを使うだけでもある程度は、ということで。
Amon2プロジェクトの例で。
初期設定
$ amon2-setup.pl MyApp
とかで雛形プロジェクトを作ると、sqlディレクトリが作られて、そこにDDLを保存する雰囲気になる。そのままsql/mysql.sql
を使っていくことにする。
config/development.pl
で例えば
return +{ 'DBI' => [ 'dbi:mysql:dbname=example', 'root', '', +{ AutoCommit => 1, PrintError => 0, RaiseError => 1, ShowErrorStatement => 1, AutoInactiveDestroy => 1, mysql_enable_utf8 => 1, mysql_auto_reconnect => 0, }, ], };
のように設定し、アプリではこの設定を使ってMySQLのexampleデータベースに接続して動くようにする。
テーブル作成
exampleデータベースで例えば次のようにテーブルを作成。
CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` char(16) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `item` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned NOT NULL, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), KEY `user_id` (`user_id`), CONSTRAINT `item_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SQL::Translatorを使ってschema dump
上記で作ったDB定義をそのままmysqldump -d
してsql/mysql.sql
に突っ込んでもいいのだけど、敢えてPerlスクリプトからdumpしてみる。
script/dump_schema.pl
を以下のように作成。
#!/usr/bin/env perl use strict; use warnings; use File::Basename; use File::Spec; use lib File::Spec->catdir(dirname(__FILE__), '..', 'lib'); use MyApp; use DBI; use SQL::Translator; # DBIからschema生成 my $translator = do { my $config = MyApp->config; my $dbh = DBI->connect(@{ $config->{DBI} }); SQL::Translator->new( parser => 'DBI', parser_args => +{ dbh => $dbh }, ); }; my $schema = $translator->translate; # AUTO_INCREMENT情報を削る for my $table ($schema->get_tables) { my @options = $table->options; if (my ($idx) = grep { $options[$_]->{AUTO_INCREMENT} } 0..$#options) { splice $table->options, $idx, 1; } } # MySQL向けのSQLを生成 $translator->producer('MySQL'); my $sql = $translator->translate; print $sql; # sql/mysql.sql ファイルに書き込み my $file = File::Spec->catdir(MyApp->base_dir, 'sql', 'mysql.sql'); open my $fh, '>', $file or die $!; print $fh $sql; close $fh;
これを実行すると、mysqldumpしたのと同様に以下のようなSQLが吐かれる。
-- -- Created by SQL::Translator::Producer::MySQL -- Created on Fri Dec 14 15:40:27 2012 -- SET foreign_key_checks=0; -- -- Table: `item` -- CREATE TABLE `item` ( `id` integer(10) unsigned NOT NULL auto_increment, `user_id` integer(10) unsigned NOT NULL, `name` varchar(255) NULL DEFAULT NULL, INDEX `user_id` (`user_id`), PRIMARY KEY (`id`), CONSTRAINT `item_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8; -- -- Table: `user` -- CREATE TABLE `user` ( `id` integer(10) unsigned NOT NULL auto_increment, `name` char(16) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8; SET foreign_key_checks=1;
スキーマ情報にAUTO_INCREMENTオプション要らないので無理矢理削っている。。
SQL::Translator は、ファイルやDBIなどの入力を受け取ってschemaを保持し、producerを指定することで、そのschemaを様々な形で出力することが出来る。便利。
ここではDBIから読み取ったschemaをMySQLの形式で吐き出しただけ。
スキーマ変更
ここで、共同開発者がDB変更してdumpがpushされたとかでsql/mysql.sql
が以下のように変更されたとする。
CREATE TABLE `hoge` ( `id` integer(11) NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8; CREATE TABLE `item` ( `id` integer(10) unsigned NOT NULL auto_increment, `user_id` integer(10) unsigned NOT NULL, `name` varchar(255) NULL DEFAULT NULL, `type` tinyint(1) unsigned NOT NULL, INDEX `user_id` (`user_id`), PRIMARY KEY (`id`), CONSTRAINT `item_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8; CREATE TABLE `user` ( `id` integer(10) unsigned NOT NULL auto_increment, `name` char(16) NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8;
そうすると「DDLファイル」と「実際の手元のDB」でschemaが異なることになる。
SQL::Translator::Diffで差分抽出
GitDDLでも中で使っている、SQL::Translator::Diff。 これが2つのschemaの差異を算出してくれる。
script/diff_schema.pl
を以下のように作成。
#!/usr/bin/env perl use strict; use warnings; use File::Basename; use File::Spec; use lib File::Spec->catdir(dirname(__FILE__), '..', 'lib'); use MyApp; use DBI; use SQL::Translator; use SQL::Translator::Diff; my $config = MyApp->config; my $dbh = DBI->connect(@{ $config->{DBI} }); my $source = do { my $schema = SQL::Translator->new( parser => 'DBI', parser_args => +{ dbh => $dbh }, )->translate; }; my $target = do { SQL::Translator->new( parser => 'MySQL', filename => File::Spec->catdir(MyApp->base_dir, 'sql', 'mysql.sql'), )->translate; }; # AUTO_INCREMENT情報を削る for my $schema ($source, $target) { for my $table ($schema->get_tables) { my @options = $table->options; if (my ($idx) = grep { $options[$_]->{AUTO_INCREMENT} } 0..$#options) { splice $table->options, $idx, 1; } } } # diffの検出 my $std = SQL::Translator::Diff->new(+{ output_db => 'MySQL', source_schema => $source, target_schema => $target, producer_args => +{ quote_field_names => '`', }, }); my $diff = $std->compute_differences->produce_diff_sql; print $diff;
これで、「今の状態のDB」にどう変更をすれば「schemaファイル」と同じ状態になるか、が求められる。
$ perl script/diff_schema.pl -- Convert schema '' to '':; BEGIN; SET foreign_key_checks=0; CREATE TABLE `hoge` ( `id` integer(11) NOT NULL, `created_at` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8; SET foreign_key_checks=1; ALTER TABLE item ADD COLUMN `type` tinyint(1) unsigned NOT NULL; ALTER TABLE user ADD COLUMN `created_at` datetime NOT NULL; COMMIT;
ちゃんと差分だけを出してくれる。問題ないようであればこれをそのまま流してやれば良い。オプション付きで自動適用とかにしても良いかもしれない。
use Getopt::Long; GetOptions( 'upgrade' => \my $upgrade, ) or die; ... my $diff = $std->compute_differences->produce_diff_sql; print $diff; # DBへの適用 if ($upgrade) { for my $statement (split /;/, $diff) { next unless $statement =~ /\S+/; $dbh->do($statement); } }
これだけで、「現在の状態」と「有るべき状態」を比較して最小限の変更で「有るべき状態」への追従ができる。はず。
まとめ
やっていることはGitDDLとかとたぶん変わらないのだけど、schemaの差分を抽出して適用させる、という操作の部分だけ取り出すとこんな感じになるかと。
スキーマの差分は
https://github.com/onishi/mysqldiff
でも簡単に求められるけど、より細かいところまで差分だしたり、MySQL以外のDBで同じようにやりたいときはSQL::Translator::Diffが使えそうな気がする。