Hatena::ブログ(Diary)

脱サラ大学生のプログラム日記 RSSフィード

2010-03-11

Google devfest 2010にいってきました

| 22:30

最初に検索したら詳しく書いている人がいました。。。

d.hatena.zeg.la(http://d.hatena.ne.jp/zegenvs/20100311

私もA会場にずーっといました! ので、詳しいことは上のを見てもらって気になったことを。。。

ひがさんがいた!

そーいえばslim3つくっているもんね。ひがさんは招待なんだろうか? 一般?

正解者数

2113人エントリーして461人正解。会場に着ていたのが東京が300名強、京都が70人強でした。最終的にはもう少し増えたのかな?

案外エントリー数も少ない気がします。もともと会場に行けない人がエントリーしなかったからかな?

でも関東と関西のエンジニアの占める割合たぶん8割以上いますよね。。。元々日程的に参加出来ない人もいると思いますが、もう少し参加してもね

Preziかっこいい!

ぐりぐりまわります!

英語が。。。

3番目のGoogleMapのセッションは典型的なアメリカ人って感じのおじさんでした。もうね、普通に英語だからみんなほとんどわかっていなかったと思う。。。

そういえば事前に英語だって書いてあったよね。。。スマートフォン用のセッション説明にはなぜか抜けていたんだよね。。。

Waveの話でも質疑応答があったけれど。。。

やっぱり英語勉強しないとだめですね

ビール貰えた!

5時からLTでビール飲みながら! 5分のLTでしたが、時間きれると厳しめにゴングが鳴り続けます(笑)

ザウルスにAndroid載せた人が楽しかったな

2010-02-26

PHPを実行ファイルに(bamcompile)

| 01:40

古いツールなので今更感がありますが、便利でしたので!

bamcompileとは

PHPを実行ファイルにしてくれるツールです。ただし最終バージョンが2006年なのでPHP4で動いています。

処理的にはPHP本体が中に入っていて、PHPを読み込んで動いている感じです。

bamcompileのダウンロード

http://www.bambalam.se/bamcompile/

上記からダウンロード可能です。

PHPモジュールのダウンロード

標準だとmbstringが使えないのでダウンロードします。PHP4で動いているのでPHP4のWindows版のモジュールを利用します。

http://jp.php.net/releases/

上記から4.4.6のWindows binaryをダウンロードします。

プロジェクト構成

bamcompileの中にサンプルが入っているので、見てみます。

examples
├─gif2png
│  │  compile.bat
│  │  rambo.gif
│  │  
│  └─gif2png
│          gif2png.php
│          php_gd2.dll

compile.batはコンパイルを行うためのバッチファイルで、引数でコンパイルの実行をしています。

..\..\bamcompile -c -e:gif2png\php_gd2.dll gif2png.php gif2png

rambo.gifはサンプル画像です。ランボーの画像勝手に使っていいんだろうか。。。

gif2pngフォルダ以下が内部に取り込まれるファイル置き場になります。

gif2png.phpは実際のプログラムです。基本的に普通のPHPがそのまま動きます。

<?

print "\nGIF2PNG - Convert gif files to png\n\n";

if($argc==1)
{
	print "Usage: gif2png infile.gif [outfile.png]\n";
	exit;
}

$infile = "";
$outfile = "";

while(list($nr,$val)=each($argv))
{
	if($nr>0)
	{
		$val = strtolower($val);
		if(strpos($val,'.gif')>-1)$infile = $val;
		else if(strpos($val,'.png')>-1)$outfile = $val;
	}
}

if($infile == "")
{
	print "You must specify a gif file to convert!\n";
	exit;
}

if($outfile == "")$outfile = str_replace('.gif','.png',$infile);

$img = imagecreatefromgif($infile);
imagepng($img,$outfile);
print "$infile converted to $outfile!\n";

?>

中身はコマンドプロンプト用のツールみたいですね。

php_gd2.dllが読み込むPHPモジュールです。

プロジェクトファイルを利用する場合

─test
   │  bamcompile.exe
   │  compile.bat
   │  project.bcp
   │  
   └─dir
           php_mbstring.dll
           project.php
           project_sub.php
           icon.ico

こんな構成がおすすめです。

project.bcpはプロジェクトファイルになります。引数として与えることも可能ですが、わかりにくくなるのでプロジェクトファイルとして渡します。

mainfile project.php
outfile project.exe
embed dir
extension dir\php_mbstring.dll
DONTENCODE
ICON dir\icon.ico

重要なのがDONTENCODEで、PHPファイルをエンコーディングしないオプションです。日本語が入っている為かもしれませんが、エンコーディングするとforeachなどがうまく動かない場合がありました。

embedで指定したフォルダの中身はすべて実行ファイルに取り込まれます。

PHP内部からrequire_onceなどで読み込む場合などは、ここに入れておきましょう! 実行ファイルが大きいのが気になる人はCOMPRESSオプションで圧縮したり、DOS窓いらない人はWINDOWEDをつけましょう。

ちょっと気になる挙動

mainfileと同じPHPが出来上がった実行ファイルと同じ場所にあると、内部に取り込んだPHPではなくて、同じ場所にあるPHPを実行します!

DONTENCODEをつけないでPHPファイルが壊れた場合、同じ場所にPHPファイルがあると動くけれど、実行ファイルだけコピーすると動かなくなる挙動がでて焦りました。

総括

PHP4ではあるが、非常に便利なツールです。

ちょっとした画像の編集やツール作成などでは便利に使えるのではないでしょうか。おすすめはエクセルやPDF系のツールです。後日エクセルを読み込んでテキストに吐き出すツールについて記述をしてみたいと思います。

2010-01-05

お勧めのManaged Directx本続編

| 21:54

Managed DirectX Game Programming

Managed DirectX Game Programming

かなり前に書いた日記ですが続報です(前回の日記)

2006年7月22日に注文を行ったのですが、本日メールが届きました

English follows Japanese ( 文章の最後に英語で同様のご案内がございます)

このたびは、Amazon.co.jpをご利用いただき、ありがとうございます。

誠に申し訳ございませんが、お客様のご注文内容のうち以下の商品について、商品を
入手できないことがわかりました。

 Richard Hoskinson (著), Wes Greene (著) "Managed Directx Game Programming"

Amazon.co.jpでは、この商品につきまして、入荷の可否を継続して確認して参りましたが、
現時点では入荷の見込みがないと判断せざるを得ませんでした。

つきましては、Amazon.co.jp利用規約に基づき、お客様のご注文からキャンセルさせて
いただきましたので、お知らせいたします。
このたびは、当サイトをご利用いただいたにもかかわらず、このような結果となりました
ことをお詫び申し上げます。
Amazon.co.jp 利用規約について詳しくは下記のURLをご参照ください。
http://www.amazon.co.jp/gp/help/customer/display.html?nodeId=643006

3年半ぐらい経過して、やっとあきらめたようです(笑)

ただし、注文で期間限定のクーポンを1000円分使ったのですが、なくなった(涙)

今返ってこないかを問い合わせ中です。。。

(追記)1000円+300円のクーポンくれました

2009-09-18

99ドルの電源アダプタ型Linuxサーバ「SheevaPlug」

02:12

http://japanese.engadget.com/2009/02/25/linux-sheevaplug/

発表当初ちょっといいなと思っていましたが、最近また記事をみて欲しくなってきました(笑)

どうやら直販しかしていないようなので、直販サイト(http://www.globalscaletechnologies.com/p-22-sheevaplug-dev-kit-us.aspx)から注文しないといけないようです。

送料は購入者のブログによって結構値段が違いますね。。。よく調べたらStateを入れるか入れないかで値段が違うようです!

Stateありの場合がUPS使って、無い場合にはFedExのようです。Cityは影響しないようです。

f:id:akiraneko:20090919021055p:image

f:id:akiraneko:20090919021054p:image

UPS料金表

個数送料一個あたり送料合計一個あたり単価
1個$74.11$74.11$173.11$173.11
2個$97.32$48.66$295.32$147.66
3個$115.73$38.58$412.73$137.58
4個$129.28$32.32$525.28$131.32
5個$138.57$27.71$633.57$126.71
6個$150.07$25.01$744.07$124.01
7個$160.83$22.98$853.83$121.98
8個$172.38$21.55$964.38$120.55
9個$184.30$20.48$1075.30$119.48
10個$196.59$19.66$1186.59$118.66

FedEx料金表

個数送料一個あたり送料合計一個あたり単価
1個$34.49$34.49$133.49$133.49
2個$36.63$18.32$234.63$117.32
3個$65.33$21.78$362.33$120.78
4個$69.58$17.40$465.58$116.40
5個$75.81$15.16$570.81$114.16
6個$81.70$13.62$675.70$112.62
7個$91.65$13.09$784.65$112.09
8個$92.83$11.60$884.83$110.60
9個$100.67$11.19$991.67$110.19
10個$105.06$10.51$1095.06$109.51

※実際には消費税5%と手数料500円ぐらいが別途輸入消費税として必要みたいです

※都内でも住所で10ドルぐらい値段が違います!

んー、UPSだと個数で順調に単価下がりますがFedExの場合はサイズと重さの関係だと思いますがリニアには下がりません。金額を考えると5個かっても2個買ってもそれほど変わらないので2個かな〜

2個だったら1個誰かに売るか、予備で持っていてもいいかな。。。ただ値段とか考えるとFON2.0(http://www.fon.ne.jp/fonera2/)を買ってプラグイン開発って手もあるんだよね。ただApacheとかMySQLを普通に入れられるのはSheevaPlugのメリットかな?

PC用学習リモコンキットのPC-OP-RS1(http://buffalo.jp/products/catalog/item/p/pc-op-rs1/)をつないで、iPhoneからブラウザ経由でリモコン操作とか作りたいんですよね〜

ちなみに白箱は持っていますが、ファンの音がうるさすぎて通常電源オフのファイルバックアップ用サーバーとなっています。。。

本当に使うのかといわれるとちょっと即答できないですが、ガジェット的にはいいですよね?(苦笑)

2009-09-15

フロー図を描いてみる

22:36

仕様書などで簡単なフローを描く場合ってありますよね?

本当はJUDEとかのUMLエディタを使えばいいのですが、単純なフローとか特殊なソフトとかを入れなくても描けるといいなって思い実験しました。

概要

独自のフローを記述する言語を経由してGraphvizhttp://www.graphviz.org/)用の出力ファイルを作成。そのファイルを利用して画像を作成します。

参考

http://sourceforge.jp/projects/make-flowchart/

フローチャート生成ツール

フローチャートを生成します。テキストを入力とし、出力はgraphviz向け(DOT言語)です。

次を予定しています。1:独自言語→DOT言語変換2:プログラミング言語→独自言語変換3:独自言語→プログラミング言語(コメントによる雛形) ?:簡便に使用するためのラッパ等

上記がイメージとしては近いのですが、Perlで組んであったのと独自言語の仕様があまりまとまってなかったので、PHPで自分で作ってみました。

フロー記述言語

初期化処理
#IF 個数?
	#CASE 1個
		処理1
		処理2
		処理3
	#CASE それ以外
		処理1
		処理4
終了処理

こんな記述方法にしました。

特徴としては

  • 関数や判断は#で始まる
  • 同列に記述すると上から順に実行するフローとなる
  • タブで一段下げると子どもの処理となる
  • 関数はIFしかない

実際にはswtich文しかない感じですね。ループの処理は。。。人数分上記の処理を実行するなど文字でカバー(笑)

実行結果

f:id:akiraneko:20090915223200p:image

こんな感じのフローが出力されます。IFからの条件は本当は次のフローへの線に条件を書くべきだが条件が長い場合にあまりよいレイアウトにならないのと、1行ごとにブロックを置く設計になってしまったのでこう表示されています。

f:id:akiraneko:20090915223201p:image

理想はこうだよね。でも条件が長いと

f:id:akiraneko:20090915223202p:image

こんな感じになってしまいます。箱に入れても大きくなってしまうけれど、まだ見やすかったかな?

DOTへの出力

digraph G {
	graph [fontname="MS Gothic"];
	node [shape="box", fontname="MS Gothic", fixedsize="false", width=1.7, height=0.8];
	edge [fontname="MS Gothic"];
	start [label="S", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];
	end [label="E", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];

	start -> node1;
	node1 -> node2;
		node2 -> node3;
			node3 -> node4;
			node4 -> node5;
		node2 -> node6;
			node6 -> node7;
			node7 -> node8;
	node5 -> node9;
	node8 -> node9;
	node9 -> end;

	node1 [label="初期化処理"];
	node2 [label="個数?",shape="diamond"];
	node3 [label="1個",shape="doubleoctagon"];
	node4 [label="処理1"];
	node5 [label="処理2"];
	node6 [label="それ以外",shape="doubleoctagon"];
	node7 [label="処理1"];
	node8 [label="処理4"];
	node9 [label="終了処理"];
}

こんなDOT出力に変換しています。書式の詳細はgraphvizの仕様のままなので記述しませんが、DOTに変換後にPNGなどの画像に変換しています。

変換処理

#から始まる行が判断が必要で、タブで階層を判断します。処理の内容的にはC言語のポインターを利用したデータ構造で組んでしまったので、参照を利用しまくったPHPにはまったく見えないソースになってしまいました。。。

<?php

$tree = getFlowFile('test.flow');
$dot = outputDot( $tree );

echo "<pre>";
echo($dot);

function getFlowFile( $filename ){
	$tree = array();

	$fp = fopen( $filename, 'r' );
	if( $fp == null ){
		return $tree;
	}

	$nowLevel = 0;
	$boxIndex = 1;
	$nowTree =& $tree;
	$upTree = array();
	$upTree[] =& $tree;
	while( !feof( $fp ) ){
		$line = fgets($fp, 5120);
		$item = array();

		if( preg_match( "/(\t*)#(.*?) (.*)/", $line, $match ) ){
			// コマンド引数あり
			$level = strlen( $match[1] );
			$command = strtoupper( $match[2] );
			$text = trim( $match[3] );

			$item['level'] = $level;
			$item['command'] = $command;
			$item['text'] = $text;
		} else if( preg_match( "/(\t*)#(.*)/", $line, $match ) ){
			// コマンド引数なし
			$level = strlen( $match[1] );
			$command = strtoupper( trim($match[2]) );

			$item['level'] = $level;
			$item['command'] = $command;
		} else {
			// 文字列
			preg_match( "/(\t*)(.*)/", $line, $match );
			$level = strlen( $match[1] );
			$text = trim( $match[2] );

			if( strlen( $text ) === 0 ){
				continue;
			}

			$item = array();
			$item['level'] = $level;
			$item['text'] = $text;
		}

		$item['num'] = $boxIndex++;

		if( $nowLevel == $item['level'] ){
			$nowTree[] = $item;
		} else if( $nowLevel < $item['level'] ){
			$upTree[] =& $nowTree;
			$nowItem =& $nowTree[count($nowTree)-1];
			$nowItem['Items'] = array();
			$nowTree =& $nowItem['Items'];
			$nowTree[] = $item;
			$nowLevel = $item['level'];
		} else {
			while( $nowLevel != $item['level'] ){
				$nowTree =& $upTree[count($upTree)-1];
				unset($upTree[count($upTree)-1]);

				$nowLevel--;
			}

			if($nowTree === null){
				$nowTree =& $tree;
			}

			$nowLevel = $item['level'];
			$nowTree[] = $item;
		}
	}

	return $tree;
}

function outputDot( $tree ){
	$output = <<<__ECHO__
digraph G {
	graph [fontname="MS Gothic"];
	node [shape="box", fontname="MS Gothic", fixedsize="false", width=1.7, height=0.8];
	edge [fontname="MS Gothic"];
	start [label="S", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];
	end [label="E", shape="circle", style="filled", fillcolor="#CCCCCC", width=0.1, height=0.1];


__ECHO__;

	$nodeInfo = array();
	$nodeRoot = array();
	foreach( $tree as $item ){
		if( count( $nodeRoot ) === 0 ){
			// Start
			$nodeNum = $item['num'];
			$output .= "\tstart -> node" . $nodeNum . ";\n";
			$nodeRoot[] = $nodeNum;
		} else {
			$nodeNum = $item['num'];
			foreach( $nodeRoot as $rootNode ){
				$output .= "\tnode" . $rootNode . " -> node" . $nodeNum . ";\n";
			}
			$nodeRoot = array();
			if( isset( $item['Items'] ) ){
				//
			} else {
				$nodeRoot[] = $nodeNum;
			}
		}
		$nodeInfo[] = $item;

		if( isset( $item['Items'] ) ){
			$nodeRoot = array();
			$nodeRoot = dispItems(&$output, &$item, &$nodeInfo, &$nodeRoot, $nodeNum, 0 );
		}
	}

	foreach($nodeRoot as $node){
		$output .= "\tnode" . $node . " -> end;\n";
	}

	$output .= "\n";
	foreach($nodeInfo as $node){
		$type = "";
		if( $node['command'] === 'IF' ){
			$type = ",shape=\"diamond\"";
		}
		if( $node['command'] === 'CASE' ){
			$type = ",shape=\"doubleoctagon\"";
		}
		if( $node['command'] === 'END' ){
			$output .= "\tnode" . $node['num'] . " [label=\"E\", shape=\"circle\", style=\"filled\", fillcolor=\"#CCCCCC\", width=0.1, height=0.1];\n";
			continue;
		}

		$output .= "\tnode" . $node['num'] . " [label=\"" . $node['text'] . "\"" . $type . "];\n";
	}

	$output .= <<<__ECHO__
}
__ECHO__;

	return $output;
}

function dispItems($output, $nodeRoot, $nodeInfo, $nodeList, $rootNode, $mode){
	foreach( $nodeRoot['Items'] as $node ){
		$nodeNum = $node['num'];

		for( $i = 0 ; $i < $node['level'] ; $i++ ){
			$output .= "\t";
		}

		$output .= "\tnode" . $rootNode . " -> node" . $nodeNum . ";\n";
		if( $mode !== 0 ) {
			$rootNode = $nodeNum;
		}
		$nodeInfo[] = $node;
		if( isset( $node['Items'] ) ){
			if( $node['command'] === 'IF' ){
				dispItems(&$output, &$node, &$nodeInfo, &$nodeList, $nodeNum, 0 );
			} else {
				dispItems(&$output, &$node, &$nodeInfo, &$nodeList, $nodeNum, 1 );
			}
		}
	}

	if( $mode !== 0 && !isset( $node['Items'] ) && $node['command'] !== 'END' ) {
		$nodeList[] = $nodeNum;
	}

	return $nodeList;
}

自分で組んだながら汚いソースです。。。ネストしているデータ構造で最後に合流するってデータはちゃんとクラスを作って処理しないとだめっすね。

応用

Wikiとかでささっと大まかなフローを書くと、そのまま図になるって利用方法を想定して実験しています。Pukiwikiとかのプラグインで組んであげると、割と簡単に図が描けると思います。

他の例

初期化処理
#IF 個数?
	#CASE 1個
		処理1
		処理2
		処理3
	#CASE それ以外
		処理1
		処理4
		#IF 状態?
			#CASE 新規
				処理5
				処理8
				処理9
				処理10
				処理11
				処理12
			#CASE その他
				処理6
				処理7
				#END
終了処理

f:id:akiraneko:20090915223203p:image

すこし大き目のサンプルです。その場で完了する場合から#ENDを追加しています。

まとめ

この手の簡易フロー記述言語は拡張していくとそれこそプログラムになるので、あくまで簡易がいいと思います(笑)

利用想定としてはWikiを利用した仕様書作成ってのを研究していて表は書きやすいのですが、大まかな流れや条件別の計算式などを表す場合にはどうしてもフローの方がわかりやすいので検証してみました。

個人的にはこれぐらいの表現ができればあとは表でなんとかできるかなと考え中です。pukiwikiであれば子どものページを含めて1ページに表示する自作プラグインなどがあるので、コピーして張り付けるとWordに見出し付きで仕様書っぽい物ができてしまいます。

とはいえgraphvizは事前にセットアップしておく必要がありますので環境を作るのが結構面倒です。graphvizJava実装であるGrappaを利用してGAE/J上で構築した方が幸せになりそうな気がしました。。。

2009-08-27

理想の会社

| 00:02

私の考えるプログラマにとって理想の会社です。

新人研修がちゃんとある会社

思うに、ちゃんとした研修を受けていないプログラマが多いように思います。ちょっとしたことですが、プロとして仕事をする上で気をつけないといけないことってありますよね?

そーいうのをちょっとだけでも教えると、なんと工数がプラスになります!(笑) ←教育コストは除く。。。

まずはバグを組み込まないで、読みやすいプログラムを書くってことを教えないとだめっすね。手早くなんとなく動くけれど、バグがたくさんっていう学生っぽい組み方を最初に否定してあげないと、バグ入れられて工数がマイナスになります。。。

最初は遅くてもいいから、プロとしての品質を出す。それから速度を上げるのは慣れですからね。プログラムの概要がわかっている人であれば、一週間から二週間ぐらいでかなりの成果がでると思います!

人の入れ替わりがある会社

例が悪いですが、シューティングゲームの会社ってのは、高レベルプログラマのみで構成されています。そこに新人が入り込む余地はほとんどなく、熟練プログラマによってプログラムをされています。

ただこの構造ってのは次の世代への引継ぎができないので、組織としては成長していかないんですよね。もちろん研究所みたいなところは必要だとは思いますが、通常の会社としてはある程度人の入れ替わりが必要だと思います。

ただしい人の入れ代わりってのは、新人がある程度の定期的に入ってきて、先輩もある程度定期的に転職をしていく環境。普通といえば普通ですが、売り上げ重視とかになるとキャパシティー以上に人を取ったり、転職する人と喧嘩別れしたりしますよね。。。

システム的にはリクルートの3年働くと100万円もらえるって契約形態ってプログラマに向いているのかなって思います。昔は5年ぐらいでしたが、今は1年から2年ぐらいで転職する人って多いですよね?

ただ実際新人からだと3年ぐらいやらないと一人前にならないですし、会社側もなかなか費用の回収ができないと思います。全員が巣立っていくと困りますが、3年ぐらいのサイクルの場合予定が組みやすい人員配置ができると思うんですよね。

卒業生と交流がある会社

やっぱり転職するって上司に言うと社長室に呼び出されて、お前を育てるのにいくらかかったと思うと怒鳴られるような会社はだめだと思います。笑顔で送り出して、機会があったらまた一緒のプロジェクトをしようなっていうぐらいがいいと思います。

思うに、どうせ人はある程度の年数でやめていくんです!(笑)

新人がどんどん育っていく環境を作るのを優先しましょう。上が抜けていろんな会社に散っていくのはコネができたと思ってよしとしましょう!

程度な男女比(笑)

見える範囲に多少女性が必要です!

天然っぽいけれどまじめで、学生時代から彼氏がいるぐらいの子がいいと思います。変にルーズな子がいると逆に場が乱れます。。。(職場の三角関係とかまじ最悪です)

適切な採用

会社の規模によりますが、基本的には中途採用よりは新卒採用に比重を置いたほうがいいと思います。お勧めは情報処理系の専門学校生です。在学中から実はアルバイトって形で会社に呼べたりします。インターン制度で給料払っちゃだめで、テストには出してねってところから、学校が終わってからじゃないとだめってところ、フルタイムでテストも全部免除ってところまであります。

ゲーム系とかも最近ネットワークとかやっていて、C言語とWeb系できたりするので使いやすいかもしれません。ただしやっぱりちゃんとした学校経営をしているところだと、フルタイムのバイトはだめだったりします。まあ、学校にいくら行っても現場に出てコーディングしないと無駄だったり、ちゃんとやればやるほど就職大変だったりするんですけれどね。。。

少し前までは、人をとりあえず集めてちょっと研修をして即派遣って会社もあったようです。その手の会社の場合前職ウエイトレスの女性とかを短期間研修して、まったくプログラムを組めないような状況で派遣先に出しても返される、もしくは面接で落ちてずーっと待機になるなど、適当な会社もあったみたいです。

また、中途採用に関してもWeb系の場合まずは楽天だYAHOOだと有名な会社を受けますので、まず中堅の会社にはいい人が来ません(笑)

たまーにふらりと来ることがありますが、大手からの転職組みは協力会社のメンバーにすべて任していて自分では何もできない人や、指示待ちで自分だけだと動けないPGなど新人とあまり変わらないスペックだけれど、妙にあつかいにくくて給料の高いひとができあがったりします。。。

わかりやすい給料体系

いろいろ、わかりにくくしている会社が多いと思います(苦笑)

残業代払わないのであれば、ちゃんとみなし制度にして給与のときに個別の金額について説明をするべきだと思います。変に複雑な計算式にして、意図を隠そうとしている方が裏が見えて社員のモチベーションが下がると思います。

たぶん、そんなに給料の額ってモチベーションにかかわらないと思うんですよね。額以外の部分が悪くても、まずは額に対して言うと思いますが、本当に文句があるのは額じゃないことが多いと思います。。。

なるべく兵隊を作らない組織

Javaなんかは階層が分かれていて、一兵卒は軍曹ぐらいまでしかなれなくって、幹部候補生は別みたいなイメージがありますが、仕様書を渡されてそのとおりにコーディングするって末端プログラマって積極的に作る必要はないと思うんですよね。

まあJavaも、ものすごく大きなプロジェクト以外はそんなに階層がわかれていないと思いますが訓練されているプログラマは明らかに間違っている仕様書でも、間違ったまま実装します!

私の理想はレンジャーを育てること。自分である程度判断ができる人を育てたいですね。もちろん自分で判断できないことは上にエスカレーションすることは大切です。明らかに間違っている仕様書があったら、これ大丈夫ですかと確認して欲しいんですよね。

まあ、本当は仕様概要書を渡して仕様は自分で作って欲しい(笑)

もちろん、人のキャラクターもありますので、黙々と細かいところを作るのが得意な人もいますし、とにかく難しい実装が好きな人もいます。組み合わせですが、最初から兵隊を育てようと思うと組織全体が育っていきにくいと思うんですよね。。。

コンテンツ系の開発は階層ごとじゃなくって、機能ごとに一人が上から下まで担当する形が多いので一人で動ける人が便利ってのもありますが、新人が先輩に教わりながらPGやって、そのうち一人立ちして、慣れたらPGリーダーとして新人の面倒をみて、徐々にプロジェクトリーダーみたいな仕事をやっていく。。。そんな会社がいいですね。。。

スペシャリスト系の職種がある組織

よくプロジェクトリーダーはやりたくないって人がいますが、それはその組織がプロジェクトリーダーをやると損だって会社だと思うんですよね。プロジェクトリーダーまでは全員が経験するべき共通のステップだと思います。損だと思われていることを解消することがまずは必要だと思います。

その先はマネージメントとスペシャリスト系の2パターンあってもいいかなと思っています。マネージメントってできない人はできないので、プログラムのスキルとはまったく違う能力だと思います。

ただし、スペシャリスト系って普通の人ってほとんど該当しないんですよね。。。メンバー時代でもスペシャリスト系の人ってのは周りの人と明らかに違うので、自分が普通の範疇にあるなと思ったら、スペシャリスト系じゃなくって高レベルエンジニアを目指した方がいいと思います。

それこそYAHOOとか楽天とかGoogleとかはてなとかの普通のメンバーレベルが実は居心地いいのかなって思います!

中規模の会社の場合には、高レベルエンジニアっていると便利ですがプロジェクトリーダーとかやりたくないんだったら、メンバーでいると逆にもったいないというか、邪魔なんですよね(苦笑)

総括

途中ごちゃごちゃと話が広がりました。。。

人がある程度育っていく環境が居心地のいい環境だと思います!

育っていかない環境や、現場まかせって会社は組織力ってのは付かないと思います。

育てるキーマンってなかなかいないのですが、一度育つ組織をつくるとその人いなくてもどんどん育っていきますからね!

結論をいうと、職場にはある程度の女性が必要!(違)

いぶいぶ 2009/09/08 07:26 こんにちは。いぜんよりRSSをグーグルで購読していますが、ここ連続の記事(会社のこと)はとても参考になりました。とはいってもうちは全くITには関係のない工事の会社なのですが、エンジニアやスペシャリストが働く場所という意味では同じなのかもしれません。
会社に対してとてもよく考えられた記事だなと思いました。
いくつも頷きながら読ませていただきましたが、改めて会社の構造などにテコ入れをしなくてはいけないと思うのと同時に、ここは気をつけなくてはいけないんだろうなと気付かせていただいた部分も多々ありました。
またこういった記事も楽しみにしています。

2009-08-25

プログラマの理想郷について

| 23:41

ちらりとブログで話題になっているので、パソコン黎明期からバブルがはじけるぐらいまでのプログラマの理想郷について聞いた話を書いてみたいと思います。

当時はまだコンピューターが高価な時代であり、比較的裕福な家に住んでいる人か高収入を得ている人しかコンピューターを持つことが難しい時代がありました。パソコン一式購入して100万円とかの時代ですね。

その時代から活躍していたプログラマの人が中心として作った会社のお話です。

プログラマの理想郷ってなんだと思いますか?

私が思う理想郷って「働かなくてもいい会社」だと思います(笑)

だって、プログラマって将来楽するためにいまがんばる人種だと思いますので、働かなくてもいい環境が理想ですよね?

その会社は特殊な分野での第一人者がいたので、年1本ぐらいソフトを発売するだけで仕事をしないでも会社がまわっていたようです。そのため夕方ぐらいに出社してテレビを見て会社で時間をすごしてから帰るような社員もいたとか、いないとか。。。

経営には無頓着な人がやっている会社ですので、最終的には中間でソフトの売り上げをものすごい抜かれていたり契約面でぼろぼろだったのですが、それ以上の売り上げをあげていたのでバブルがはじけるまでは存続できたようです。

歴代の在籍者を聞いてみると、そうそうたるメンバーで、聞いたことあるようなソフトの開発者がいたようですが去っていった人が多かったみたいですね。

プログラマが理想郷にいるとどうなるか?

たぶん、伸びなくなります。スタープログラマの人は元々第一線でプログラムを組む能力があったので、伸びなくても超一級なのですがそれ以外の人はたぶんプログラマとしての能力は止まってしまいますね。

成長しなくてもいい環境ってのが理想郷なのかもしれません。

その証で、10年以上も会社を存続されていたのに新人から育ったプログラマがいなかったようです。難しいことはすべてスタープログラマの人が組むので、新人は雑用系のコーディングしかさせなかったみたいです。

この辺は浦島太郎みたいなものかな?

居心地がいいと時間が止まってしまうみたいです。

これって主夫になりたいって思うプログラマの心理と一緒でしょうか?(たぶん違う)

2009-08-24

Teedaでネストしたプロパティー表示の実験

| 21:20

概要

Teedaを利用していて、面倒だなって思うところでDtoなどの大量のデータを表示する際にページプロパティーに詰め替える作業があります。Teedaの次バージョンでは実装予定項目で入っていますが、まだまだ出そうにないので実験してみました。

目的

実用的に利用できる状態までの実験は行わず、実装方法の検証とどれぐらいの作業量がかかりそうかの検証を目的とします。

書式の規定

通常Teedaはテンプレートのidにプロパティー名を入れます。ネストした場合には「neko.itemName」とするのが妥当なのですが、"."(ピリオド)を利用するとJavaScriptなどでスタイルシートの"."(ピリオド)との区別が付かなくなります。JSFデフォルトの":"(コロン)と同じくあまり好ましくない書式になります。

idに使わない記号で、あまり違和感のない"_"(アンダーバー)を今回は利用したいと思います。Teedaの場合通常ローワー・キャメルケースで書くので"_"(アンダーバー)は使いませんよね?

ただし、JSFの実装理念的には"."(ピリオド)を使うのが正しいはずです!

<span id="nekoDto_nekoString">nekoStringDummy</span><br />
<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

こんな感じで"_"(アンダーバー)がきたら、そのクラスのプロパティーを表示するようにしたいと思います。

準備

ソースのダウンロード

Teeda本体をコンパイルするのにSeasar2本体も必要なので同時にダインロードしましょう。

JDKの設定

Seasarプロダクトは基本的にJDK4かJDK5でコンパイルを行います。Tigerと付いているパッケージは5でそれ以外が4の事が多いようです。今回はJDK5でコンパイルを行いたいと思います。

また、JDK6だとコンパイルエラーがでるのでコンパイルを行うことができません!

JAVA_HOME=C:\Program Files\Java\jdk1.5.0_16

JDK5の指定を行います。またコンパイル等はコマンドプロンプト上のmaven2を利用して行いました。

パッケージのテスト
mvn package

を実行してパッケージが成功するか確かめましょう。

該当場所のソース検索

表示にかかわる場所はすべて「teeda-extension」以下にありますが、そのものずばりのソースの場所がわからないので、まずは「label」をキーワードにしてソースを眺めてみました。

LabelProviderMap.java

    public Object get(final Object pageName) {
        return new LabelProviderMap() {
            public Object get(final Object key) {
                final String label = LabelUtil.getLabelValue((String) key,
                        (String) pageName);
                if (!StringUtil.isEmpty(label) || suppressDecolate) {
                    return label;
                }
                return "??" + key + ExtensionConstants.LABEL_ATTRIBUTE_SUFFIX +
                        "??";
            }
        };
    }

ラベルの指定をミスると??で囲われて出力されるので、実際にはここでリストから取ってきているようです。ただ今回はあまり関係なさそうですね。

AbstractElementProcessorFactory.java

    protected String getLabelExpression(final String attributeValue,
            PageDesc pageDesc) {
        final String pageName = pageDesc.getPageName();
        final String labelName = attributeValue.substring(0, attributeValue
                .length() -
                ExtensionConstants.LABEL_ATTRIBUTE_SUFFIX.length());
        return "#{labelProvider." + pageName + "." + labelName + "}";
    }

ここで上のプロバイダーを利用してラベルの出力を行っているようです。#{}の形式ですのでJSFで出力しているんですね。

OutputTextFactory.java

    public boolean isMatch(ElementNode elementNode, PageDesc pageDesc,
            ActionDesc actionDesc) {
        final String tagName = elementNode.getTagName();
        if (TeedaExtensionConfiguration.getInstance().outputTextSpanOnly &&
                !tagName.equals(JsfConstants.SPAN_ELEM)) {
            return false;
        } else if (!acceptableElements.contains(tagName.toLowerCase())) {
            return false;
        }
        if (pageDesc == null) {
            return false;
        }
        final String id = elementNode.getId();
        if (id == null) {
            return false;
        }
        if (isLabel(id, elementNode)) {
            return true;
        }
        return pageDesc.hasProperty(id);
    }

isLabelという名前を発見。なんかここが怪しいですね!

    protected boolean isLabel(final String id, final ElementNode elementNode) {
        final String key = toNormalizeId(id);
        if (!TeedaExtensionConfiguration.getInstance().outputTextLabelUnderAnchorOnly) {
            return key.endsWith("Label");
        }

        final ElementNode parent = elementNode.getParent();
        if (parent == null) {
            return false;
        }
        final String tagName = parent.getTagName();
        if (key.endsWith("Label") &&
                tagName.equalsIgnoreCase(JsfConstants.ANCHOR_ELEM)) {
            return true;
        } else {
            return false;
        }
    }

えーっと、処理内容的には最後にLabelが付いた場合にtrueになっていますね。isMatchという名前を見る限りテキスト出力時にidが処理対象かを判断する場所と思われます!

ここを拡張してネストしたプロパティーを実装したいと思います。

実装の追加

ネストチェックロジック追加
    protected boolean isNest(final String id, PageDesc pageDesc) {
        String[] items = id.split("_");

        if( items.length != 2 ){
            return false;
        }

        return pageDesc.hasProperty(items[0]);
    }

isNestという名前でチェック関数の追加をします。内容的には"_"(アンダーバー)が1つ含まれている場合で、そのクラスがページプロパティーにある場合にtrueとなります。本当はネストした先の存在チェックとかも必要ですし、複数ネストした状況とかもあるのですが実験はここまで。。。

idチェックロジック修正
    public boolean isMatch(ElementNode elementNode, PageDesc pageDesc,
            ActionDesc actionDesc) {
        final String tagName = elementNode.getTagName();
        if (TeedaExtensionConfiguration.getInstance().outputTextSpanOnly &&
                !tagName.equals(JsfConstants.SPAN_ELEM)) {
            return false;
        } else if (!acceptableElements.contains(tagName.toLowerCase())) {
            return false;
        }
        if (pageDesc == null) {
            return false;
        }
        final String id = elementNode.getId();
        if (id == null) {
            return false;
        }
        if (isLabel(id, elementNode)) {
            return true;
        }
        if (isNest(id, pageDesc)) {
            return true;
        }
        return pageDesc.hasProperty(id);
    }

isNestをチェック関数に追加します。これでネストしたid形式の場合OutputTextで利用されるようになりました。

テストプロジェクトの準備

ここで一度テスト用のプロジェクトを作成して、実験してみたいと思います。

プロジェクト作成

Doltengを利用してTeedaプロジェクトを作成します。

Dto作成

NekoDto.java

package neko.web;

public class NekoDto {
    public int nekoInt;
    public String nekoString;
}

シンプルなDtoクラスを作成します。privateでもかまいませんがその場合にはSetterとGetterを作成してあげてください。

ページクラス作成

TestPage.java

package neko.web;

public class TestPage {

    public NekoDto nekoDto;

    public Class<?> initialize() {
        nekoDto = new NekoDto();
        nekoDto.nekoInt = 12345;
        nekoDto.nekoString = "neko1234";

        return null;
    }

    public Class<?> prerender() {
        return null;
    }
}

こちらもシンプルにDtoの作成と値設定のみを行います。

テンプレート作成

test.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" lang="ja" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>nekoTest</title>
</head>
<body>

Teeda[nekoString]:<span id="nekoDto_nekoString">nekoStringDummy</span><br />
Teeda[nekoInt]:<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

</body></html>

ネストの指定が入っている以外は通常のXHTMLで問題ありません。

自作Teedaパッケージへ入れ替え

新規作成の状態では最新のTeedaのパッケージが利用されるので、自分でパッケージしたファイルを利用するように変更します。

copy /y teeda-ajax\target\teeda-ajax-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib
copy /y teeda-core\target\teeda-core-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib
copy /y teeda-extension\target\teeda-extension-1.1.0-alpha1-SNAPSHOT.jar ..\neko\src\main\webapp\WEB-INF\lib

なんのひねりもないですが、自分でパッケージしたファイルをテスト用のプロジェクトにコピーして、元々あるパッケージを削除してコピーしたパッケージを利用するようにクラスパスを書き換えます。

実行

Teeda[nekoString]:??nekoDto_nekoSLabel??
Teeda[nekoInt]:??nekoDto_neLabel??

当たり前ですが、出力がおかしいです。OutputTextで出力部分の実装を行っていないですからね。ただlabelの処理系に飛んでいるってことで、認識までは動く動作していることが確認できました。

出力部分実装

ここはかなりいろいろ試行錯誤して、どこにいれればいいのか悩んだのですが、、、

OutputTextFactory.java

    protected void customizeProperties(Map properties, ElementNode elementNode,
            PageDesc pageDesc, ActionDesc actionDesc) {
        super
                .customizeProperties(properties, elementNode, pageDesc,
                        actionDesc);
        properties.put("tagName", elementNode.getTagName());

        if (pageDesc == null) {
            return;
        }
        final String id = elementNode.getId();
        if (pageDesc.hasProperty(id)) {
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), id));
        } else {
            final String key = toNormalizeId(id);
            TextNode firstTextNode = elementNode.getFirstTextNode();
            properties.put(JsfConstants.VALUE_ATTR, getLabelExpression(key,
                    pageDesc));
        }
    }

灯台下暗し・・・isLabelのすぐ上になった関数で処理していました!

処理的には「pageDesc.hasProperty(id)」でページプロパティーの場合にはidを出力して、それ以外はlabelを出力するってなっていますね。

    protected void customizeProperties(Map properties, ElementNode elementNode,
            PageDesc pageDesc, ActionDesc actionDesc) {
        super
                .customizeProperties(properties, elementNode, pageDesc,
                        actionDesc);
        properties.put("tagName", elementNode.getTagName());

        if (pageDesc == null) {
            return;
        }
        final String id = elementNode.getId();
        if (pageDesc.hasProperty(id)) {
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), id));
        } else if( isNest(id, pageDesc) ){
            String[] items = id.split("_");
            properties.put(JsfConstants.VALUE_ATTR, getBindingExpression(
                    pageDesc.getPageName(), items[0]+"."+items[1]));
        } else {
            final String key = toNormalizeId(id);
            TextNode firstTextNode = elementNode.getFirstTextNode();
            properties.put(JsfConstants.VALUE_ATTR, getLabelExpression(key,
                    pageDesc));
        }
    }

改造して、isNestの場合に「nekoDto_nekoSLabel」を「nekoDto.nekoSLabel」に置換して出力しています。

実行

  • パッケージ作成(mvn package)
  • jarファイルコピー(copy -y...)
  • Tomcat再起動

の手順を踏んでからページを再表示させます。

Teeda[nekoString]:neko1234
Teeda[nekoInt]:12345

見事表示されました!

仕組み

元々JSFではネストしたプロパティーを実は表示することができます。そのため案外改造箇所が少なく動いています。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" xmlns:h="http://java.sun.com/jsf/html" lang="ja" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>nekoTest</title>
</head>
<body>

Teeda[nekoString]:<span id="nekoDto_nekoString">nekoStringDummy</span><br />
Teeda[nekoInt]:<span id="nekoDto_nekoInt">nekoIntDummy</span><br />

<br />

JSF[nekoString]:<h:outputText value="#{testPage.nekoDto.nekoString}"/><br />
JSF[nekoInt]:<h:outputText value="#{testPage.nekoDto.nekoInt}"/><br />

</body></html>

こんな感じで最初に「xmlns:h="http://java.sun.com/jsf/html"」を追加してJSFのタグを利用可能にして、h:outputTextを直接呼べば無改造のTeedaでもネストしたプロパティーへアクセスすることができます。

現状はネストしたidの場合ページプロパティーに存在していないと思い、labelの処理に飛んでいるので表示することができませんでした。

総括

簡単にできているように書いてありますが、予想以上に解析に時間かかっていたりします(笑) 結局6時間前後かかったよ。。。

outputTextは簡単にできているように見えますが、実運用するためにはもう少しきれいに組む必要があります。。。InputTextとか大量に対応しないといけない場所があるので、ちょっと面倒かも。ただ技術的難易度はそれほどない気がしますので、手間をかければ実装できそうです。

ただTeedaプロジェクトの人は現在T2に注力している感じなので、当分機能追加のバージョンアップはなさそうですね。個人的にはこのネストがあるとページクラスが非常にすっきりするので欲しい機能だったりします。

うーむ、自分で組むかが悩ましい。。。

2009-08-20

Teedaにおけるpom.xmlの設定

| 00:24

Teedaでプロジェクトを利用していると、通常Maven2でパッケージを作ると思いますが、その設定例を書き出してみます。基本的にはEclipse上でDoltengを利用して作成したものに手をいれています。あとちょっと昔のプロジェクトなのでバージョンとかは少し古いものです。

注意事項としては、小規模から中規模案件向けの設定になります。

build

ビルドに関してはpathなどはデフォルト設定のまま使っています。ここは妙に変えると面倒ですからね。会社規定のpath構造などをあって、デフォルトと違う場所に移動するとパッケージを作る際に特別な手順などが必要なのと、導入教育が面倒なのでデフォルトで使うのが良いと思います。(共通Path設定にするためのMavenだよね?)

コンパイラバージョンの指定
<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <source>1.5</source>
      <target>1.5</target>
      <encoding>UTF-8</encoding>
    </configuration>
</plugin>

通常1.6の新機能って使うことないと思いますので、1.5にしておきます。この辺は環境依存しますが確実に1.6の使える環境であれば1.6を使った方が動作が早いような気もしますが、まだまだサーバーが1.5で動いていることが多いので。。。

javadocの設定
<plugin>
  <artifactId>maven-javadoc-plugin</artifactId>
  <configuration>
    <source>1.5</source>
    <minmemory>128m</minmemory>
    <maxmemory>512m</maxmemory>
    <encoding>UTF-8</encoding>
    <docencoding>UTF-8</docencoding>
    <charset>UTF-8</charset>
    <locales>ja</locales>
  </configuration>
</plugin>

個人的には使いませんが、とりあえず入れておきます。パッケージを作るだけであれば利用されないですしね。なんとなくDoxygen派です。でも基本的にはソースは頭で覚えるか実物見るのであまりこの手のドキュメントは使いません。

profiles

環境別のビルドファイルを作る設定を追加します。

<profiles>
  <profile>
    <id>development</id>
    <activation>
      <activeByDefault>true</activeByDefault>
    </activation>
    <build>
      <finalName>neko-development</finalName>
      <resources>
        <resource>
          <directory>src/main/resources</directory>
        </resource>
        <resource>
          <directory>src/main/java</directory>
        </resource>
      </resources>
    </build>
  </profile>
  <profile>
    <id>ita</id>
    ... 略 ...
  </profile>
  <profile>
    <id>itb</id>
    ... 略 ...
  </profile>
  <profile>
    <id>product</id>
    <build>
      <finalName>neko-product</finalName>
      <resources>
        <resource>
          <directory>src/product/resources</directory>
        </resource>
        <resource>
          <directory>src/main/resources</directory>
        </resource>
        <resource>
          <directory>src/main/java</directory>
        </resource>
      </resources>
    </build>
  </profile>
</profiles>

src/main/resources にデフォルト環境+開発環境

src/ita/resources に社内テストサーバー用設定(差分のみ)

src/itb/resources に本番テストサーバー用設定(差分のみ)

src/product/resources に本番サーバー用設定(差分のみ)

って使い分けをしています。中身は

env.txt環境別にitやproductを指定
jdbc.diconDBのIPやパスワードの設定
log4j.propertiesテストサーバーはdebugなど出力レベル調整
siteSetting.diconそのサーバー特有の設定

みたいなものを入れています。デフォルトでないのがsiteSetting.diconです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
  "http://www.seasar.org/dtd/components24.dtd">
<components>
  <component name="SiteSettingService" class="neko.service.SiteSettingService">
    <property name="csvBasePath">
      "C:/neko-data/csv/"
    </property>
    <property name="siteBaseURL">
      "http://127.0.0.1:8080/neko/"
    </property>
  </component>
</components>

上記のようにサーバーによって変わってくる部分を記述します。メールとかに埋め込むサイトのURLとか、管理者のメールアドレスとかDBに設定すればいいのですが、ちょっとしたものはここで設定しています。propertiesでもいいのですが、なんとなく(笑)

package neko.service;

public class SiteSettingService {
  public String csvBasePath;
  public String siteBaseURL;
}

受側のjavaファイルです。サンプルでは何もしていませんが、実際にはURLの合成などのちょっとした処理が入っていたりします。利用するときにはこのサービスを宣言すると自動的にDIされて設定したdiconファイルの内容が初期値に入っています。

pluginRepositories

リポジトリの設定ですが利用していません。個人的にはプロジェクトで利用するファイルはすべてSVNにコミットする方針を採っているので、インターネットやイントラからのダウンロードはしない方針です。

dependencies

すべてプロジェクト内部にファイルを持ちます。これによってMaven系のプラグインのダウンロードは発生しますが、個別のライブラリのダウンロードは必要なくなります。

    <dependencies>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-jsp_2.0_spec</artifactId>
            <version>2.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-jsp_2.0_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-servlet_2.4_spec</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-servlet_2.4_spec-1.0.jar</systemPath>
        </dependency>
        ... 略 ...

パッケージの作り方

バッチファイルで以下のコマンドを実行しています。Eclipse上から実行するとEclipseの実行Pathからになるので、エクスプローラーかDOS窓上から実行しています。内容は本番テストサーバーと本番サーバーのパッケージになります。開発系も一緒にパッケージングしてもいいのですが、開発系はHudsonとかで自動デプロイされる環境がこのましいのと、別タイミングで作ることが多いのでこんな感じになっています。

mkdir package

call mvn clean package -P itb
copy /y target\*.war package

call mvn clean package -P product
copy /y target\*.war package

call mvn clean

全文

<?xml version="1.0" encoding="UTF-8"?><project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>neko</groupId>
    <artifactId>neko</artifactId>
    <packaging>war</packaging>
    <name>neko</name>
    <version>0.0.1</version>
    <build>
        <sourceDirectory>src/main/java</sourceDirectory>
        <outputDirectory>src/main/webapp/WEB-INF/classes</outputDirectory>
        <testOutputDirectory>target/test-classes</testOutputDirectory>
        <testSourceDirectory>src/test/java</testSourceDirectory>
        <testResources>
            <testResource>
                <directory>src/test/resources</directory>
            </testResource>
        </testResources>
        <defaultGoal>validate</defaultGoal>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>source-jar</id>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>add-source</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>add-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>src/main/webapp/view</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-javadoc-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <minmemory>128m</minmemory>
                    <maxmemory>512m</maxmemory>
                    <encoding>UTF-8</encoding>
                    <docencoding>UTF-8</docencoding>
                    <charset>UTF-8</charset>
                    <locales>ja</locales>
                </configuration>
            </plugin>
        </plugins>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-eclipse-plugin</artifactId>
                    <configuration>
                        <wtpversion>1.5</wtpversion>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
    <!--  [[profiles]]  how to use : mvn package -P product -->
    <profiles>
       <profile>
         <id>development</id>
         <activation>
           <activeByDefault>true</activeByDefault>
         </activation>
         <build>
           <finalName>neko-development</finalName>
           <resources>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
       <profile>
         <id>ita</id>
         <build>
           <finalName>neko-ita</finalName>
           <resources>
             <resource>
               <directory>src/ita/resources</directory>
             </resource>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
       <profile>
         <id>itb</id>
         <build>
           <finalName>neko-itb</finalName>
           <resources>
             <resource>
               <directory>src/itb/resources</directory>
             </resource>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
       <profile>
         <id>product</id>
         <build>
           <finalName>neko-product</finalName>
           <resources>
             <resource>
               <directory>src/product/resources</directory>
             </resource>
             <resource>
               <directory>src/main/resources</directory>
             </resource>
             <resource>
               <directory>src/main/java</directory>
             </resource>
           </resources>
         </build>
       </profile>
    </profiles>
<!--
    <pluginRepositories>
        <pluginRepository>
            <id>maven.seasar.org</id>
            <name>The Seasar Foundation Maven2 Repository</name>
            <url>http://maven.seasar.org/maven2</url>
        </pluginRepository>
    </pluginRepositories>
    <repositories>
        <repository>
            <id>maven</id>
            <name>Maven2 Repository</name>
            <url>http://repo1.maven.org/maven2/</url>
        </repository>
        <repository>
            <id>maven.seasar.org</id>
            <name>The Seasar Foundation Maven2 Repository</name>
            <url>http://maven.seasar.org/maven2</url>
        </repository>
    </repositories>
-->
    <dependencies>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-jsp_2.0_spec</artifactId>
            <version>2.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-jsp_2.0_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-servlet_2.4_spec</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/geronimo-servlet_2.4_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/junit-3.8.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>junit-addons</groupId>
            <artifactId>junit-addons</artifactId>
            <version>1.4</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/junit-addons-1.4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>rhino</groupId>
            <artifactId>rhino</artifactId>
            <version>1.6r2</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/rhino-1.6r2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-webunit</artifactId>
            <version>0.2.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/teeda-webunit-0.2.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-xmlunit</artifactId>
            <version>0.1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/teeda-xmlunit-0.1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>xmlunit</groupId>
            <artifactId>xmlunit</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/lib/xmlunit-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/aopalliance-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-collections-3.1.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-el</groupId>
            <artifactId>commons-el</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-el-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-fileupload-1.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-io-1.3.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-logging-1.1.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-jta_1.1_spec</artifactId>
            <version>1.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/geronimo-jta_1.1_spec-1.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.0.69</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/h2-1.0.69.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>jboss</groupId>
            <artifactId>javassist</artifactId>
            <version>3.4.ga</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/javassist-3.4.ga.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.1.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/jstl-1.1.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.13</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>ognl</groupId>
            <artifactId>ognl</artifactId>
            <version>2.6.9-patch-20070908</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/ognl-2.6.9-patch-20070908.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.0-FINAL</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/poi-3.0-FINAL.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.dao</groupId>
            <artifactId>s2-dao</artifactId>
            <version>1.0.49</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-dao-1.0.49.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.dao</groupId>
            <artifactId>s2-dao-tiger</artifactId>
            <version>1.0.49</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-dao-tiger-1.0.49.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.container</groupId>
            <artifactId>s2-extension</artifactId>
            <version>2.4.33</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-extension-2.4.33.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.container</groupId>
            <artifactId>s2-framework</artifactId>
            <version>2.4.33</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-framework-2.4.33.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.container</groupId>
            <artifactId>s2-tiger</artifactId>
            <version>2.4.33</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2-tiger-2.4.33.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-ajax</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-ajax-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-core</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-core-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-extension</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-extension-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.teeda</groupId>
            <artifactId>teeda-tiger</artifactId>
            <version>1.0.13-sp4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/teeda-tiger-1.0.13-sp4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>xerces</groupId>
            <artifactId>xercesImpl</artifactId>
            <version>2.6.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/xercesImpl-2.6.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>xerces</groupId>
            <artifactId>xmlParserAPIs</artifactId>
            <version>2.6.2</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/xmlParserAPIs-2.6.2.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>net.arnx.jsonic</groupId>
            <artifactId>jsonic</artifactId>
            <version>1.0.3</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/jsonic-1.0.3.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>com.lowagie.text</groupId>
            <artifactId>iText</artifactId>
            <version>2.1.4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/iText-2.1.4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.4</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/commons-lang-2.4.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.chronos</groupId>
            <artifactId>s2chronos-core</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2chronos-core-1.0.0.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.seasar.chronos</groupId>
            <artifactId>s2chronos-extension</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/src/main/webapp/WEB-INF/lib/s2chronos-extension-1.0.0.jar</systemPath>
        </dependency>
    </dependencies>
</project>

総括

小規模から中規模程度のTeedaプロジェクトであればリポジトリよりはプロジェクトの内部にライブラリもった方が楽だと思います。SVNにはライブラリがコミットされているけれど、パッケージの際にさらに同じものがダウンロードとかされたりしてないでしょうか? JDBCとか公開リポジトリに置いてない物がありますしね。

2009-08-19

Teeda向けS2JDBC-GenでのDB構成管理

| 20:12

S2JDBC-Genの使い方としては、想定外の使い方をしています。他にも構成管理をする方法はありますので、よく検討してから参照してください。

基本概要

Teedaに限ったことではありませんが、S2JDBCを利用していないプロジェクト向けに、無理やりS2JDBC-Genを利用してDB構成管理をしてみる方法について記述しています。

そのため、S2JDBCを開発で利用している場合の想定手順と違う方法や、手法などを利用しています。

ERDツールとの差別化

構成管理だけであれば、ERDツールなりエクセルなりで管理が可能だと思います。ただしS2JDBC-Genではデータの管理も行うことができます。通常開発用の最低限のマスターデータが入ったもの。負荷テスト用に最大限のデータが入ったものなどを準備し、気軽に準備することができます。

もちろん開発用の共有で使えるデータベースを準備して、接続先を切り替えて使う方式でも対応できますが、S2JDBC-Genを使うとローカルのデータベースをコマンド一発で初期状態に戻したりできますし、確実に最新環境に入れ替えたりが可能ですのでより失敗することが少なく開発をすることが可能です。

準備

データベースのバックアップ

おそらく実験途中で何度かデータベースを壊します。復帰できるようにあらかじめデータベースのバックアップを行いましょう。

プロジェクト作成

Doltengを利用してSAStrutsS2JDBCの構成でプロジェクトを作成します。作成する名前はメインプロジェクトがnekoであれば、neko-dbなどわかりやすい名前が良いと思います。

ポイントとしては、なるべく最新のDoltengを利用して作成してください。Seasar本体のバージョンはなるべく新しいほうが安定していると思います。

DBの設定

resouecesの中にある「jdbc.dicon」と「s2jdbc.dicon」を編集して利用しているデータベースに接続できるようにします。必要があればJDBCなどをプロジェクトの中に追加する必要があります。

Teedaで利用されているS2Daoと違うところは、「s2jdbc.dicon」にて利用するデータエースの種類を選択することになります。ファイルの中身を見ればわかると思いますがdialectをデフォルトのH2から利用しているものに変更しましょう。

データベースへの設計反映思想

S2JDBC推奨Entity -> DDL作成S2JDBCで開発をするのであれば自然な流れ
個人的推奨DDL -> Entity作成新しく覚えることが最小

S2JDBCはEntityを作成して、それをデータベースに反映するサイクルを推奨しています。S2JDBCをベースに開発をしているのであれば上記の流れでもよいのですが、以下の場合にはこの流れを変えたほうがいいと思います。

  • ERDツールなどでDBを設計している
  • エクセルでDBを設計している
  • CREATE TABLE文を書かないと落ちつかない
  • S2JDBCのEntityの書き方を覚えるのが面倒

何をマスターとするかにかかわってきますが、S2JDBCはEntityをマスターとする考えになります。逆にERDツールを利用していると、ERDツールがマスターになりますので、推奨パターンでは面倒になります。あとは何気にエクセルで管理しているところもまだ多いですよね?

この流れの変化を嫌ってS2JDBC-Genを使っていない人って結構いるんじゃないでしょうか?

DDLからEntityへの私の推奨反映方法

新規作成したテーブルgen-entity、gen-ddlを実行
軽微なテーブル変更Entityを編集後、gen-ddlを実行
大規模なテーブル変更Entityを削除後gen-entity、gen-ddlを実行

もちろん、S2JDBC推奨のEntityを新規作成してもかまいません。軽微な変更でも毎回Entityを削除してからでもかまわないと思います。最終的にはgen-ddlが作成したDDLが、元のDDLと同一であるかが重要だと思います。

マッピングの不一致

実際のところgen-entity、gen-ddlをして作成されたDDLは当初のDDLと完全に一致するとは限りません。これは使っているデータ種などによるのですが、Javaの型とデータベースの型が1対1でマッピングできないので仕方がないと思います。

なので、出来上がったDDLを確認してからEntityの修正を行う必要があります。この作業があるためすでにある程度の数のテーブルがある場合にはかなりの手間になると思います。

プロジェクトの初期からの導入以外で、すでに稼動しているプロジェクトなどに適用するのはテスト工数などを考えると非常に大変になると思ってください。基本的に最初に導入を決めないと、途中からは危ないのでお勧めできません。

S2JDBC-Genの仕組み

プロジェクト直下にあるs2jdbc-gen-build.xmlS2JDBC-Genの設定ファイルになります。内容はAntタスクであり、実行するタスクが記述されています。

S2JDBC-Genを拡張する場合には、準備されているタスクの設定を変更するか、新規のAntタスクを作成することになります。比較的簡単に拡張が可能ですので、いろいろと作ってみると楽しいと思います。

DBに日本語のコメントを追加する

以下、実際のS2JDBC-Genの設定になります。

当初なくって残念でしたが、追加された機能です。JavadocAPIを利用してコメントに埋め込む形で対応を行っています。昔は自作タスクを作って対応していましたが現在は標準機能を利用したほうがスマートだと思います。

    <gen-entity
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      javafiledestdir="${javafiledestdir}"
      javafileencoding="${javafileencoding}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      applyDbCommentToJava="true"
    />

上記のように最後にapplyDbCommentToJavaを追加します。これでデータベースに設定されているコメントをEntity作成時に取り込むようになります。

    <gen-ddl
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      applyJavaCommentToDdl="true"
    />

EntityからDDL作成時にもコメントを利用するように設定します。gen-entityとgen-ddlで設定の名前が違うので注意してください。上記の設定ができたら、gen-entityを実行し作成されたEntityにコメントが振られていることを確認してください。

package neko.entity;

import java.io.Serializable;
import java.math.BigInteger;
import java.sql.Date;
import javax.annotation.Generated;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

/**
 * イベント履歴
 * 
 */
@Entity
@Generated(value = {"S2JDBC-Gen 2.4.39", "org.seasar.extension.jdbc.gen.internal.model.EntityModelFactoryImpl"}, date = "2009/08/18 23:58:19")
public class EventList implements Serializable {

    private static final long serialVersionUID = 1L;

    /** イベントの連番 */
    @Id
    @Column(precision = 22, nullable = false, unique = true)
    public BigInteger eventId;

    /** イベントの発生日時 */
    @Column(nullable = false, unique = false)
    public Date eventDate;

    /** イベントコメント */
    @Column(length = 4000, nullable = true, unique = false)
    public String eventComment;
}

たとえば上記のような形になります。

このファイルができたところで、gen-ddlを実行します。

com.sun.tools.javadoc.Docletが使用できません。JDKのtools.jarがクラスパスに通されていることを確認してください。

上記のエラーがgen-ddl時に出た場合にはJDKのtools.jarをクラスパスに追加しましょう。Eclipse上でJREなどを利用して開発を行っている場合にはtools.jarが入っていないのでエラーになります。

この場合には、お行儀が悪いですがどこかのJDKからtools.jarをコピーしてきて、プロジェクト直下のlibフォルダなどにいれてクラスパスに追加すると良いと思います。

そうするとdbフォルダ上にddlなどが作成されます。

create table EVENT_LIST (
    EVENT_ID number(22,0) not null,
    EVENT_DATE date not null,
    EVENT_COMMENT varchar2(4000),
    constraint FORM_EVENT_LIST_PK primary key(EVENT_ID)
);

comment on table EVENT_LIST is 'イベント履歴';
comment on column EVENT_LIST.EVENT_ID is 'イベントの連番';
comment on column EVENT_LIST.EVENT_DATE is 'イベントの発生日時';
comment on column EVENT_LIST.EVENT_COMMENT is 'イベントコメント';

Oracle上の場合ですが、上記のようなDDLが作成されました。この場合にはオリジナルのDDLと同一の構成なのでこのままで大丈夫ですが、違っていた場合には編集する必要があります。

たとえばOracleの場合CHARとVARCHAR2は両方Entity上ではStringになりますが、StringからはVARCHAR2で作成されるので非対称の変換になります。この場合には

    @Column(length = 8, nullable = false, unique = false)
    public String textData;

上記のようなカラムの場合には

    @Column(length = 8, nullable = false, unique = false, columnDefinition = "char(8)")
    public String textData;

このようにDBの宣言をそのものずばり書いてあげれば大丈夫です。(http://s2container.seasar.org/2.4/ja/s2jdbc_gen/entity_definition.html

シーケンスでのINSERT

S2JDBCはシーケンスの機能を備えていますが、データベースごとの機能の差を吸収するためだと思いますが、独自に実装しています。そのためS2JDBCを利用した開発の場合には問題がないのですが、S2Daoやツール上からInsertした場合などにシーケンスが自動的に更新されません。

たとえばOracleの場合にはシーケンスを作成して、Insertのトリガーとして処理を追加する必要があります。このようなデータベースに依存する実装方法はS2JDBC-Genではサポートしていません。

そこで自分でトリガーやシーケンスを作成する必要があります。Oracleの場合ですのでMySQLなどであれば違った手順になります。

    /** eventIdプロパティ */
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EVENT_ID")
    @SequenceGenerator(name = "EVENT_ID",allocationSize=1)
    @Column(name = "EVENT_ID", precision = 22, nullable = false, unique = true)
    public BigInteger formEventId;

上記のようにSequenceGeneratorでEVENT_IDという名前で、増分1のシーケンスを作成し、GeneratedValueで設定します。この状態でgen-ddlを実行すると030-sequenceというフォルダが作成され、中にシーケンスを作成するSQLが入っています。

このままですとシーケンスができただけで、トリガーができていませんのでトリガーは自分で追加してあげる必要があります。任意の名前でフォルダを作成して、中にSQLファイルを入れることで自動的に実行してくれますので999-USERとします。

db
 +- 0000 初期構成
 +- 0001
   +- create
   +  +- 010-table
   +  +- 020-uniquekey
   +  +- 030-sequence
   +  +- 040-dump
   +  +- 050-foreignkey
   +  +- 999-user
   +- drop

上記のようなフォルダ構成にします。数値の場所はDBのリビジョン番号ですのでどんどん増えていきます。システムが自動作成したフォルダ以外は次のリビジョンを作成したときに自動的にコピーしてくれますので、最新のリビジョンに入れることで次のリビジョンから常に入った状態になります。

CREATE OR REPLACE TRIGGER "BI_EVENT_LIST" 
  before insert on "EVENT_LIST" 
  for each row 
begin 
    select "EVENT_LIST_EVENT_ID".nextval into :NEW.EVENT_ID from dual; 
end; 
/

上記のようなSQLをevent_list.sqlなどとして999_userに入れておきます。

その他の注意点

カラムの順番が変わる

gen-entityはプライマリーキーを一番上に移動させますので、飛んでいるカラムにプライマリーキーを設定している場合には注意してください。

プライマリーキーの数など

念のためプライマリーキー関連は確かめた方がよいと思います。

外部制約など

外部制約なども確かめた方がよいと思います。

viewについて

viewはS2JDBC-Genでは管理されません。

トリガーと同じように適当なフォルダを作成して、中にSQLを入れておきます。

DBのリビジョン管理について

標準的に利用しているとdbフォルダ上にgen-ddlを実行するたびにリビジョンが増えていきます。どうもファイルが増えていくのが個人的には好きになれないので、標準のリビジョン管理を回避しています。

まず最初にdbフォルダを削除して、gen-ddlを実行します。

[db]
+- [migreate]
    +- [0000]
    +- [0001]
    +- ddl-info.txt

すると上記のような構成になります。

このddl-info.txtが内部で管理してあるデータベースのリビジョン番号を記述したファイルです。このファイルをコピーしてddl-info_base.txtとします。

[db]
+- [migreate]
    +- [0000]
    +- [0001]
    +- ddl-info.txt
    +- ddl-info_base.txt

こうなります。

次にs2jdbc-gen-build.xmlのgen-ddlのタスクを書き換えます。

  <target name="gen-ddl">

    (略)

    <!-- セキュリティの設定によっては以下の refresh タスクに時間がかかる場合があります.
      その場合は refresh タスクを削除してください.-->
    <refresh projectName="neko-db"/>

    <delete dir="db/migrate/0000"/>
    <copy todir="db/migrate/0001">
      <fileset dir="db/migrate/0002"/>
    </copy>
    <delete dir="db/migrate/0002"/>
    <copy file="db/ddl-info_base.txt" tofile="db/ddl-info.txt" overwrite="true" />

gen-ddlの最後にdelete以下の文を追加します。

0000の削除DROPは別タスクで行うので削除します
0002を0001にコピー新規でできた0002の内容を0001に上書きします。※
0002の削除追加分を削除します
ddl-infoの書き戻し常にリビジョン1固定にします

こんな処理をして、常に0001しかないようにしています。ただしこのままだとテーブルを削除した場合にも0001上にファイルが残り続けることになるので、そこは手で消します!

データベースの初期化

リビジョン管理をしなくなった場合、いつのデータベースに対してmigrateをかけるのかがわからないので、存在しないテーブルを削除しようとしてエラーになったり(現在はエラーでも処理が続きます)、管理外のテーブルが消えていなかったりします。

特に実装上必要だけれど、管理化に入っていないテーブルなどが残っていると大変ですので、migrateをしてまっさらな状態から管理しているテーブルを作る手順を取っています。

ただし危険です!

Antタスク開発用にjar追加

ant-1.7.0.jarなどant開発用のjarをプロジェクトに追加します。

削除用タスク作成

Oracleの例ですが、以下のファイルを作成します。

/neko-db/src/main/java/org/seasar/extension/jdbc/gen/internal/command/CleanDatabaseCommand.java
/neko-db/src/main/java/org/seasar/extension/jdbc/gen/task/CleanDatabaseTask.java

CleanDatabaseという、すべてのテーブルなどを削除するタスクを作成します。

/neko-db/src/main/java/org/seasar/extension/jdbc/gen/task/CleanDatabaseTask.java

package org.seasar.extension.jdbc.gen.task;

import org.seasar.extension.jdbc.gen.command.Command;
import org.seasar.extension.jdbc.gen.internal.command.CleanDatabaseCommand;

/**
 * データベースからテーブルなどを削除する{@link Task}です。
 *
 * @see CleanDatabaseCommand
 */
public class CleanDatabaseTask extends GenerateEntityTask {
	/** コマンド */
	protected CleanDatabaseCommand command = new CleanDatabaseCommand();

	@Override
	protected Command getCommand() {
		return command;
	}
}

このファイルは登録だけなので、GenerateEntityTaskを継承してCleanDatabaseCommandを作成して終わりです。

/neko-db/src/main/java/org/seasar/extension/jdbc/gen/internal/command/CleanDatabaseCommand.java

package org.seasar.extension.jdbc.gen.internal.command;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.seasar.extension.jdbc.util.ConnectionUtil;
import org.seasar.extension.jdbc.util.DataSourceUtil;
import org.seasar.framework.exception.SQLRuntimeException;
import org.seasar.framework.util.PreparedStatementUtil;
import org.seasar.framework.util.ResultSetUtil;
import org.seasar.framework.util.StatementUtil;

/**
 * データベースのテーブルなどをすべて削除するコマンドです。
 */
public class CleanDatabaseCommand extends GenerateEntityCommand {
    @Override
    protected void doExecute() {
        dropTables();
        dropSequence();
        dropView();
        dropTrigger();
    }

    private void dropTables() {
        String execSql = " select" +
                            "   tbl.TABLE_NAME NAME" +
                            " from" +
                            "   USER_TABLES tbl";
        Connection conn = DataSourceUtil.getConnection(jdbcManager
                .getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP TABLE \"" + name + "\" CASCADE CONSTRAINT";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }

    private void dropSequence() {
        String execSql = " select" +
                            "   tbl.SEQUENCE_NAME NAME" +
                            " from" +
                            "   USER_SEQUENCES tbl";
        Connection conn = DataSourceUtil.getConnection(jdbcManager
                .getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP SEQUENCE \"" + name + "\"";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }

    /**
     * VIEW削除
     */
    private void dropView() {
        String execSql =  "select tbl.VIEW_NAME NAME" +
                            " from USER_VIEWS tbl";
        Connection conn = DataSourceUtil.getConnection(jdbcManager.getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP VIEW \"" + name + "\"";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }

    /**
     * TRIGGER 削除
     */
    private void dropTrigger() {
        String execSql =  "select  trigger_name  NAME from USER_TRIGGERS";

        Connection conn = DataSourceUtil.getConnection(jdbcManager.getDataSource());
        try {
            logger.debug(execSql);
            PreparedStatement ps = ConnectionUtil.prepareStatement(conn,
                    execSql);
            try {
                ResultSet rs = PreparedStatementUtil.executeQuery(ps);
                try {
                    while (rs.next()) {
                        String name = rs.getString("NAME");

                        String execSql2 = "DROP TRIGGER \"" + name + "\"";

                        try {
                            logger.debug(execSql2);
                            PreparedStatement ps2 = ConnectionUtil.prepareStatement(conn, execSql2);
                            try {
                                PreparedStatementUtil.executeQuery(ps2);
                            } finally {
                                StatementUtil.close(ps2);
                            }
                        } finally {
                        }
                    }
                } catch (SQLException e) {
                    throw new SQLRuntimeException(e);
                } finally {
                    ResultSetUtil.close(rs);
                }
            } finally {
                StatementUtil.close(ps);
            }
        } finally {
            ConnectionUtil.close(conn);
        }
    }
}

ここではGenerateEntityCommandを参考にしてSQLを実行しています。詳細はS2JDBC-Genのソースを見て調べることになります。

内容的にはテーブルであれば、テーブルの一覧を取得してすべてのテーブルを削除しています。

タスクの登録

s2jdbc-gen-build.xmlにタスクを登録します。

  <taskdef resource="s2jdbc-gen-task.properties" classpathref="classpath"/>

  <taskdef name="clean-database" classname="org.seasar.extension.jdbc.gen.task.CleanDatabaseTask" classpathref="classpath"/>

s2jdbc-gen-task.propertiesの下ぐらいに追加します。

<target name="migrate">
    <clean-database
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      javafiledestdir="${javafiledestdir}"
      javafileencoding="${javafileencoding}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
    />

migrateタスクの一番先頭でこのタスクを呼び出します。これで最初にデータベースの内容をすべて消してから0001のSQLを順に実行していきます。

migrateの実行

既存のテーブルなどをすべてDROPしてからCREATE TABLEしていくはずです。完了した後にテーブルのデータなどが正しく入っていれば成功です。この際に当初の構造と同じかを確認して、違っていたらEntityの編集などを行い修正を行います。

Eclipse上のログなどを確認し、エラーが出ていないかも確認することが重要だと思います。

SCHEMA_INFOの削除

データベースのリビジョン管理用のテーブルが作成されますが、邪魔であれば999-userなどにDROPするSQLを作成して入れておきましょう。

/neko-db/db/migrate/0001/create/999-user/drop_chema_info.sql

DROP TABLE SCHEMA_INFO;

環境別データ作成

  <target name="dump-2">
    <dump-data
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      applyenvtoversion="${applyenvtoversion}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      applyEnvToVersion="true"
    />
    <!-- セキュリティの設定によっては以下の refresh タスクに時間がかかる場合があります.
      その場合は refresh タスクを削除してください.-->
    <refresh projectName="${projectname}"/>
  </target>

上記のようにdumpタスクを編集して、applyEnvToVersionを有効にすると

[db]
+-[migreate]
    +-[0000]
    +-[0001]
    +-[0001#ut]
       +-[create]
          +-[040-dump]

とデータのみ別のディレクトリに保存されます。同じ用にloadタスクを作ると通常と、#utで2つのデータを使い分けることができます。通常は無印で作業を行い、パフォーマンスチェック用にデータ件数が多い物を#utに置いておくことなどができます。この名前はenvで制御していますので、もっとたくさんの種類のデータを準備することもできます。

この場合にはまずはmigrateを実行して、テーブルなどを最新の状態にしてからload-2で別環境用のデータを利用する手順になります。

総括

データベースの構成管理は結構導入が大変です。ただし一度できてしまえば比較的横展開はしやすいと思います。また、メリットもたくさんあるので、ぜひ使って見ましょう。また、導入途中でS2JDBC-Genのプロジェクトがおかしくなったり、データベースが破壊されたりしますのでおかしいなと思ったらプロジェクトの作り直しや、データベースのリストアなどをして何度か壊す覚悟で作業を進めてください。

またDBFluteを利用している場合にはS2JDBC-Genではなく、DBFluteにも構成管理がありますのでそちらの利用をお勧めします。