Cotton Bolls: July 2004 Archives
« June 2004 | トップページ | September 2004 »
2004.07.18
データベース定義XML
受託開発をしていると,Excelを使ってデータベース定義や固定長ファイルレイアウトを提供している開発がほとんどのようだ.だが,ポケットCVSリポジトリで書いたように,こういう情報こそテキストファイルにしてCVS上で管理したほうがよいと思う.
うちの開発では,たとえ外向きがExcelファイルでも内部ではXMLファイルを使って管理している.こういった情報は,プログラムのソースコード以上に重要で,スキーマ変更による影響がシステム全体に波及しかねない.また,スキーマ定義は開発中ころころ変わるのが当たり前だ.CVSでバージョン管理せずには対応できない.
例えば,社員マスタを次のように定義する(shain.xml):
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE tables SYSTEM "tables.dtd"> <tables> <table name="shain" jname="社員マスタ"> <field name="shain_cd" jname="社員コード" type="char" length="6" key="yes"/> <field name="shiten_cd" jname="支店コード" type="char" length="2"/> <field name="name" jname="名前" type="varchar" length="20"> <desc>全角10文字</desc> </field> <field name="saishuu_koushin_ymd" jname="最終更新日" type="date"/> </table> </tables>
他にも支店マスタ(shiten.xml)など,テーブルごとにXMLファイルを書いてバージョン管理する.
これらのXMLファイルが開発のハブとして働くことになる.つまり,これらを中心にいろいろなものを自動生成するわけだ.ざっと挙げれば次のようなものがある:
- データベース定義仕様書
- create-tables.sql(テーブル作成のsqlスクリプト)
- insert, update, delete文のスケルトン作成
- DAOクラスのスケルトン作成
ここで,最後のDAOクラスにはGeneration Gapパターンを使うこともある.
さて,上で挙げた対象ごとにrubyスクリプトを作り,Makefileにターゲットを書き込む.例えば,create-tables.sqlのターゲットは次のようになる:
create-tables.sql:*.xml ruby -Ks create-tables.rb create-tables.rsql *.xml > $@
ここで,create-tables.rbがcreate-table.sqlを作成するrubyスクリプト本体で,create-tables.rsqlがeRubyファイルだ.eRubyはjspに似た形式を持つ埋め込みrubyスクリプトで,rubyのライブラリの中では一番お世話になっている(ここを参照.作者の咳さんは来週XP祭りでしゃべるらしいです).
eRubyファイル:create-tables.rsqlの中身は次のようになる:
% for table in tables CREATE TABLE <%= table.name %> ( % for field in table.fields <%="%-21s %-14s %s" % [field.name, field.data_type, field.not_null] %>, % end PRIMARY KEY (<%= table.primary_keys.join(",") %>) ); % end
sprintf演算子(%)を使って整形しているところはわかりにくいかもしれないが,eRubyをよく知らない人にもおおよそ理解できると思う.作るのは簡単だ.
他にinsert文の例も挙げておこう.Makefileは次の通り:
%.insert: ruby -Ks xml2insert.rb xml2insert.rsql $*.xml
このターゲットルールにより,社員マスタのinsert文が簡単に作れる:
$ make shain.insert INSERT INTO shain (shain_cd, shiten_cd, name, saishuu_koushin_ymd) VALUES ('aaaaaa', 'bb', 'cccccccccccccccccccc', '2004-01-01');
eRubyファイル:xml2insert.rsqlは次の通り:
INSERT INTO <%= table.name %> (<%= table.fields.collect { |f| f.name }.join(", ") %>) VALUES (<%= table.fields.collect { |f| f.value }.join(", ") %>);
このように,eRubyのおかげでrubyスクリプト本体はモデル(jspでいうところのJavaBean)を作るだけで済む.XMLファイルを読み込んでモデルを構築するだけだ.
XMLの読み込みにはREXMLを使っているが,Unicodeで処理されるためShift JISで書かれたXMLファイルを読むには少し工夫が必要だ.Excelファイルのこともあり,やはりShift JISで扱いたい.
このため,rexmlのメソッドをuconvでフックする.僕の場合,REXMLを直接使うのではなくrexml-sjis.rbという独自ライブラリを使っている:
require 'uconv' require 'rexml/document' module REXML class Attribute alias original_value value private :original_value def value Uconv.u8tosjis(original_value) end end class Element alias original_text text private :original_text def text Uconv.u8tosjis(original_text) end end class Text alias original_to_s to_s private :original_to_s def to_s Uconv.u8tosjis(original_to_s) end end class CData alias original_to_s to_s private :original_to_s def to_s Uconv.u8tosjis(original_to_s) end end end
とりあえずこれでうまくいってるようだ.あ,それからcygwinのsetup.exeからインストールしたuconvモジュールは時々コアダンプしてしまうことがある.もしコアダンプするならよしだむさんのところから最新版uconvを再インストールすることも忘れないようにしよう.
ところで,スキーマ変更で一番大きなダメージを受けるのはテストコードだと思う.残念ながら,これについては十分な対策は見つかっていない.それでも,ほんの少しだけ修正が楽になる方法を紹介しておこう.
テーブルを扱ったテストケースでは,最初にinsert文でテーブルのレコードをセットアップすると思う.このとき,なるべくinsert文一発で行うのではなく,insert文とupdate文を組み合わせて行うほうがよい.つまり,次のようにレコード生成する:
INSERT INTO shain (shain_cd, shiten_cd, name, saishuu_koushin_ymd) VALUES ('1', 'bb', 'cccccccccccccccccccc', '2004-01-01'); INSERT INTO shain (shain_cd, shiten_cd, name, saishuu_koushin_ymd) VALUES ('2', 'bb', 'cccccccccccccccccccc', '2004-01-01'); UPDATE shain SET shain_cd = '111111', name = '社員1' WHERE shain_cd = '1' UPDATE shain SET shain_cd = '22222', name = '社員2' WHERE shain_cd = '2'
最初のinsert文は自動生成されたinsert文で,主キーのみ一意にするよう変更する.その後テストに関係のある項目だけupdate文で更新しておく.
こうすると,たとえスキーマ変更でフィールドの追加や変更,削除があってもinsert文の修正だけで済むことが多い.気休めかもしれないが,大量にテストコードがあるとき変更が楽になるので,簡単なテーブル以外はこう書くよう心がけておいたほうがいいだろう.
01:24 PM | 固定リンク | コメント (0) | トラックバック
2004.07.11
PerlUnitと動的スコープ
今回の仕事はPerlを使ったWebアプリの開発…って,なんとPerlだ.正直この歳(36)でPerlなんて夢にも思わなかった.
そもそも,最近はプロジェクトが変わるごとに開発言語も変わっている.curl,Java,C#と来て今度はPerlか….去年の11月から開発言語が4つも変わっている(泣).言語が変わる度に文法の勉強,開発環境の整備,パッケージの調査….いくらなんでもしんどいです.こんなんじゃプロジェクト工数かかるのも無理ないわな.
でも,この歳でまだプログラミング開発の仕事をやらせてもらってるのはありがたいと思う.昔から上流の仕事も管理職もまったく興味なし.そういう意味では今の会社に感謝している(…と一応持ち上げておく).
ところで,Perlは良くも悪くもバッドノウハウのかたまり言語だと思う.少し間違えただけですぐハマる.use strictや-wでもまだ足りない.他の言語では明らかにエラーになるところを余裕で素通りする.例えば,Error.pmのtry...catch.最後のセミコロンを忘れたら大変だ.動作がおかしくなるだけで文法エラーも何もでない:
try { $package->assert_str_equals($expected, $actual); } catch Test::Unit::Exception with { my $e = shift; $e->throw_new(-package => $package, -file => $filename, -line => $line, -text => $e->{-text}); }; # <= このセミコロンが重要!
他にハマッたのは親クラス宣言.親クラスは
use base qw(SuperClass);
と書くのが正解だが,編集中にうっかりして
use base SuperClass;
とやってしまった.こうするとUnknown Errorというエラーメッセージが出るだけだ.エラー箇所やヒントは一切ない(cygwin perl).一体なんなんだろう,この言語は….
その一方で,Perlはプリミティブであるがゆえに工夫次第でかなりのことができてしまう.これでPerlにのめりこむ人も多いのではないかな.
例えば,先ほどのtry...catch.Perlには例外処理なんてないのに,Error.pmというモジュールで実現できる.Exporterを使えばMixinも可能.なんとAspect.pmなんてモジュールもあるらしい(参考:Object Oriented Programming in Perl).
こういう機能は作る分にはすごく面白いと思う.けれども,利用する側はかなり不便だ.try..catchすら標準モジュールにないため,あるモジュールを利用するときは依存している他のモジュールを何個何個もインストールするはめになる.とにかく,いろんなモジュールが絡むため,再配布のことを考えるとかなり面倒.もし依存しているモジュールのバージョンが変わっていたら悲惨だろう.この辺Perlユーザはどう考えているのかな.
というわけで,今回もPerlのテスト環境や開発環境を整えることから始めることになった.特にこういう言語では,ユニットテストがしっかり書けないと泥沼になるのは目に見えているからだ.
まずはMutliTestRunner.pl.これは,複数のTestCaseを個別に実行したり,一度に実行したりできるようにするためのPerlスクリプトだ.これは,昔Javaで作ったMultiTestRunner(JUnit 実践講座 - GNU Make を使った開発環境の構築)のPerl版だ.時間がなかったのでテストメソッド単位の実行は省略した:
#!/usr/bin/perl -w use strict; use Test::Unit::TestSuite; use Test::Unit::Debug qw(debug_pkgs); use Test::Unit::TestRunner; my $suite = Test::Unit::TestSuite->empty_new("A Test Suite"); foreach my $file (@ARGV) { my $package = Test::Unit::Loader::compile($file); my $test_case = Test::Unit::Loader::load_test_case($package); $suite->add_test($test_case) if $test_case; } my $testrunner = Test::Unit::TestRunner->new(); $testrunner->do_run($suite);
次にPerlUnit Converter.移植するのはかなり苦労したが,おおむね次のような形にまとめることができた:
package Test::Unit::ConvTest; use strict; use base qw(Test::Unit::TestCase); use Test::Unit::Conv; { package Test::Unit::ConvTest::TestBean; sub new { my $class = shift; my $self = { field1 => 'field1_val', field2 => 'field2_val', }; return bless $self, $class; } sub get_field3 { return 'field3_val'; } } sub TestBean { return __PACKAGE__ . "::TestBean"; } sub test_bean_value_converter { my $self = shift; my $converter = DefaultConverter->new; $converter = BeanValueConverter->new($converter, TestBean, "field1", "field2", "field3"); my $expected = [ "field1_val", "field2_val", "field3_val", ]; $converter->assert_equals($expected, TestBean->new); }
こんな感じでPerlの勉強をしながらテストケースを書いている.
さて,Perlには他の言語にはあまり見られない動的スコープ(Dynamic Scope)というものがある.これを使うとHello Worldのテストコードを簡単に書くことができる.すなわち,標準出力(STDOUT)を動的スコープでフックするわけだ:
{ local *STDOUT; open(STDOUT, ">temp.out") || die "Can't redirect stdout"; print "Hello World\n"; }
こうするとHello Worldの文字列はtemp.outファイルにリダイレクトされることになる.このことを利用して作ったのがStdoutCaptureクラスだ:
package Test::Unit::IO::StdoutCapture; use strict; use File::Temp qw(tempfile); sub do(&) { my $block = shift; my ($f, $filename) = &tempfile(); { local *STDOUT; open(STDOUT, ">$filename") || die "Can't redirect stdout"; $block->(); } open(TEMPFILE, "<$filename"); my @out; while(<TEMPFILE>) { push @out, $_; } close(TEMPFILE); unlink($filename); return @out; } 1;
使用例は以下の通り:
package Test::Unit::IO::StdoutCaptureTest; use strict; use base qw(Test::Unit::TestCase); use Test::Unit::Conv; use Test::Unit::IO::StdoutCapture; sub test_basic { my $self = shift; my @out = Test::Unit::IO::StdoutCapture::do { print "line1\n"; print STDOUT "line2\n"; print "line3"; }; my $converter = DefaultConverter->new; my $expected = [ "line1\n", "line2\n", "line3" ]; $converter->assert_equals($expected, \@out); } 1;
このように,動的スコープでテストコードが楽に書けることがあるようだ.
ところで,Lispにも動的スコープがある.今回は,Emacs Lispでperldocコマンドを作っているときに役立った.
もともとperldocはPerl標準添付のシェルコマンドで,モジュールやサブルーチンの使い方を出力する.unixのmanコマンドのようなものだ.なかなかperlの関数が覚えられず,シェルで何度もperldocを動かしていたのでEmacsから呼べるperldocコマンドを作ることにした.
perldocコマンドはEmacsのmanコマンドみたいなもので十分だ.そこで,man.elのソースを見てみる.すると,manコマンドの本体はMan-getpage-in-background関数で,manual-program変数にシェルから起動するmanコマンド名を入れていることがすぐわかった.そこで,このmanual-program変数を動的スコープでフックする:
(defun perldoc (word) (interactive "sperldoc: ") (let ((manual-program)) (setq manual-program "perldoc") (Man-getpage-in-background word)))
なんと,たったこれだけでperldocコマンドが動くようになった.普通ならもっとlispのコードを書かないといけないのにこれだけで済むとは驚いた.このように,動的スコープはグローバル変数みたいなものだからあまりお勧めできないが,ちょっとしたものなら役に立つ.PerlUnitでのうまい応用例も見つかればいいと思っている.