Oracle + JUnit4 で DBTestCaseを継承せずに dbUnit を使う

Oracle + JUnit4 で DBTestCaseを継承せずに dbUnit 使おうと思ったら、ちょっと苦労したのでメモ。

http://www.dbunit.org/howto.html#noextend によると、DBTestCase を継承しないで DBUnit のテストケースを書くには、dbUnit 2.2以降はIDatabaseTester が使えるようである。そこで、サンプルをもとに、以下の様に書いてみた。

public class HogeTest {

        private static IDatabaseTester dbTester;
       
        @BeforeClass
        public static void setUpBeforeClass() throws Exception {
              
               dbTester = new JdbcDatabaseTester("oracle.jdbc.driver.OracleDriver" ,"jdbc:oracle:thin:@xxxx:1521:xxx" ,"user" ,"pass");
               dbTester.setSetUpOperation(DatabaseOperation.INSERT);
               dbTester.setTearDownOperation(DatabaseOperation.DELETE);
              IDataSet dataset = new FlatXmlDataSetBuilder().build(new FileInputStream("testdata/hogeDataset.xml" ));
               dbTester.setDataSet(dataset);
       }

        @AfterClass
        public static void tearDownAfterClass() throws Exception {
       }

        @Before
        public void setUp() throws Exception {
               dbTester.onSetup();
       }

        @After
        public void tearDown() throws Exception {
               dbTester.onTearDown();
       }

        @Test
        public void testMethod() {
               // test
       }

}

これでテストを実行すると、以下の様なエラーが発生する。

AmbiguousTableNameException

これは、http://www.dbunit.org/faq.html#AmbiguousTableNameException に記載があった。
スキーマ名を指定せよとのこと。とくに、Oracle の場合は、スキーマ名を大文字でしてする必要があるらしい。
以下の様に書き換える。

dbTester = new JdbcDatabaseTester("oracle.jdbc.driver.OracleDriver" ,"jdbc:oracle:thin:@xxxx:1521:xxx" ,"user" ,"pass","SCHEMA");

これで、AmbiguousTableNameException は発生しなくなった。
しかし、今度は、dbTester.onSetup() 、テストデータの登録時に以下の様な例外が発生するようになった。

org.dbunit.dataset.NoSuchColumnException:

dataset の xml に誤りは無く、Column名が間違っていたわけではない。
ぐぐってみると、dataTypeFactory とやらを Oracle 用にしてみれば良いらしい。そこで、以下の記述を追加する。

dbTester.getConnection().getConfig().setProperty(DatabaseConfig. PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory());

しかし、これでは例外が解決しなかった。
考えたあぐねて、デバッグモードでdbTester.getConnection().getConfig() のプロパティを見てみたら、DefaultDataTypeFactory のままだった。

なぜ設定が反映されないのか。さっぱりわからなくなって、 JdbcDatabaseTester#getConnection のソース読んだら以下の様になっていた。

public IDatabaseConnection getConnection() throws Exception {
        logger.debug("getConnection() - start");
        assertNotNullNorEmpty( "connectionUrl", connectionUrl );
        Connection conn = null;
        if( username == null && password == null ){
            conn = DriverManager.getConnection( connectionUrl );
        }else{
            conn = DriverManager.getConnection( connectionUrl, username, password );
        }
        return new DatabaseConnection( conn, getSchema() );
    }

最後でDatabaseConnectionをnewしてやがる…
つまり、getConnectionでは常に新しいIDatabaseConnectionが返るので、せっかくプロパティをセットしても保持してくれないのだ。

ちなみに、他のIDatabaseTesterの実装も同様の実装だった。

しょうが無いので、JdbcDatabaseTesterを継承し、getConnectionをオーバーライドしてその都度プロパティをセットするようにした。

最終的には、以下の様なコードになった。

public class HogeTest {

        private static IDatabaseTester dbTester;
       
        @BeforeClass
        public static void setUpBeforeClass() throws Exception {
              
               dbTester = new JdbcDatabaseTester("oracle.jdbc.driver.OracleDriver" ,"jdbc:oracle:thin:@xxxx:1521:xxx" ,"user" ,"pass","SCHEMA") {
                      @Override
                      public IDatabaseConnection getConnection() throws Exception {
                           IDatabaseConnection conn = super.getConnection();
                            conn.getConfig().setProperty(DatabaseConfig. PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory());
                            return conn;
                     }
               };
               dbTester.setSetUpOperation(DatabaseOperation.INSERT);
               dbTester.setTearDownOperation(DatabaseOperation.DELETE);
              IDataSet dataset = new FlatXmlDataSetBuilder().build(new FileInputStream("testdata/hogeDataset.xml" ));
               dbTester.setDataSet(dataset);
       }

        @AfterClass
        public static void tearDownAfterClass() throws Exception {
       }

        @Before
        public void setUp() throws Exception {
               dbTester.onSetup();
       }

        @After
        public void tearDown() throws Exception {
               dbTester.onTearDown();
       }

        @Test
        public void testMethod() {
               // test
       }

}

これでようやく動作した。

Python3 で os.walk() を使ってファイル一覧を生成しようとしてyeildを使うまで

Python3 でツールを書いていて、あるフォルダ以下のファイル一覧を再帰的に取得したくなった。

MS-DOS の 「DIR /S /B」みたいなイメージで。

Python の osモジュールには、os.walkというこういうときに便利な関数があるので、これを使ってファイル一覧を作ることにした。最初に書いたのが以下。

import os
result = []
for path, dirs, files in os.walk(rootDir):
    result += map(lambda x: os.path.join(path, x), files)

まあ、これで十分シンプルでいいんだけど、for文使って足し込むのがイヤだったり、結果がiteratorでなくてリストそのもので作られてしまうので、リスト内包表記を使ってみることにした。

import os
files = [map(lambda x: os.path.join(path, x), files) for path, dirs, files in os.walk(rootDir)]

でもこれじゃ望んだ結果がこない。リストのリスト(のiterator)が返された。失敗失敗。

そこで、reduceを使って、リスト内のリストを結合することにしよう。…と思ったら、Python3からはreduce関数が標準関数でなくなって、functoolsモジュールに移動したらしい。使えないからビックリした。

functoolsモジュールからreduceをimportして、以下の様に書いた。

import os
from functools import reduce
files = reduce(lambda a, b: a+b, [map(lambda x: os.path.join(path, x), files) for path, dirs, files in os.walk(rootDir)])

これは動かなかった。なぜなら、リスト内包表記の方が返すリストが、mapオブジェクトのリストだから。
'+'演算子はmapオブジェクトに対しては使えない。そこで、mapの結果をリストに変換して以下の様に書く

import os
from functools import reduce
files = reduce(lambda a, b: list(a)+list(b), [map(lambda x: os.path.join(path, x), files) for path, dirs, files in os.walk(rootDir)])

これで、filesにrootDir以下のファイルをリストで格納できた。なんか冗長になっちゃった。

しかし、ここに至って、結果がiteratorでなくてリストになってしまっていることに気付いた。大量のファイルを扱う場合であれば、メモリ効率を考えて、できればiteratorで受け取りたいところ…

そこでPythonのドキュメントを探していたら、どうも、yield文というのを使って実現するらしい。
そこで、1行で書くのはあきらめて、yield を使ってみることにした

yeildを使ってファイルの一覧を返すiterator

def filesIterator(rootDir):
    for path, dirs, files in os.walk(rootDir):
        for filePath in map(lambda x: os.path.join(path, x), files):
            yield filePath

これでファイルの一覧をIteratorで取得できるようになったので、あとはfor文なりなんなりで自由に処理することができる。
でもこれ、for文がネストしているのをなんとか出来ないかなあ…

Snow Leopard に git をインストールする(インストーラ使用)

SCM Bootcamp に参加したので、MBA (まだSnowLepardです…)にGitをインストールしたのでメモ。

まず、ググる先生に聞いて、以下のサイトを参照。
最速で Git を Mac にインストールして基本的なコマンドを使う方法

このサイトではMacPortsを使えば簡単!最速!と紹介されているんだけど、残念ながら自分のMBAにはMacPortsは未インストール。
そこで、「インストーラを使用する」方法を選択。

以下のサイトからインストーラを取得。せっかくだから俺はこの64bit版を選ぶぜ!
http://code.google.com/p/git-osx-installer/downloads/list?can=3

ゲットできたdmgファイルを開いて、その中にあるpkgファイルをダブルクリック。

インストール完了。はやっ!

$which git
$man git

の動作を確認。とりあえず、大丈夫かな?

MacBook Air 11インチ欲しい!

MacBook Air 11インチ欲しい!

MacBook Air 11インチ欲しい!」ってはてなダイアリーに書くと抽選で一名様にもらえるらしいんで2ヶ月ぶりに書いてます。

しかもこんな内容でごめんなさい。

MacBook Air 11インチ欲しい!

現在MacBook Air(late 2009) モデルを使っていて、とくに不満は無いんだけど、鞄に入れて持ち歩くと歳のせいか腰にくるようになってしまって。
軽いMacBook Airが欲しいんですよ!

MacBook Air 11インチ欲しい!

WiMAXも導入したのでモバイルの準備は万端!あとは軽いMacBookだけあればいいんです!

MacBook Air 11インチ欲しい!

あ、商品のMacBook Air ってキーボード選べたりするのかしら。USキーボードの方がいいいなあ

MacBook Air 11インチ欲しい!

恥ずかしながら何度でも書きましょう

MacBook Air 11インチ欲しい!

Oracle JDBCドライバでLONG列にアクセスする際の注意点

LONG列なんかはもう非推奨で普通はBLOB使うからほとんど問題にはならない内容なのですが。

Oracleデータディクショナリテーブルの一つALL_TAB_COLUMNS(USER_TAB_COLUMNS)DATA_DEFAULT列はなんとLONG型で、これにアクセスするときにこの問題にはまって2時間ほど損失したのでメモしておく。

Oracle JDBCドライバでLONG列を含むテーブルにアクセスする場合は、ResultSetからデータを取得するときLONG列を一番最初に取得しなければならない」

な ん だ そ りゃ

具体的には以下のような現象。

final String SQL_COLUMNINFO =
  "SELECT " +
    "col.column_name COLUMN_NAME, " +
    "col.data_default DATA_DEFAULT, " + //こいつがLONG列
    "co.comments COMMENTS " +
  "FROM " +
    "user_tab_columns col " +
  "WHERE " +
    "col.table_name = 'HOGE' " +
  "ORDER BY " +
    "col.table_name, " +
    "col.column_id ";
Statement stmt = null;
ResultSet rs = null;
try {
  stmt = conn.createStatement();
  rs = stmt.executeQuery(SQL_COLUMNINFO);
  while(rs.next()) {
    String columnName = rs.getString("COLUMN_NAME");
    String comments = rs.getString("COMMENTS");
    String dataDefault = rs.getString("DATA_DEFAULT"); //ここで例外発生
    System.out.println(ColumnName + ':' + comments + ':' + dataDefault);
  }
} finally {
  try { if(rs!=null) rs.close(); } catch (SQLException e) {}
  try { if(ps!=null) ps.close(); } catch (SQLException e) {}
}

上記のような記述だと、getString("DATA_DEFAULT") とした時点で「ORA-17027 ストリームはすでにクローズ済です」が発生する

ここは、以下のようにすると問題が回避できた

final String SQL_COLUMNINFO =
  "SELECT " +
    "col.column_name COLUMN_NAME, " +
    "col.data_default DATA_DEFAULT, " + //こいつがLONG列
    "co.comments COMMENTS " +
  "FROM " +
    "user_tab_columns col " +
  "WHERE " +
    "col.table_name = 'HOGE' " +
  "ORDER BY " +
    "col.table_name, " +
    "col.column_id ";
Statement stmt = null;
ResultSet rs = null;
try {
  stmt = conn.createStatement();
  rs = stmt.executeQuery(SQL_COLUMNINFO);
  while(rs.next()) {
    String dataDefault = rs.getString("DATA_DEFAULT"); //最初に読む
    String columnName = rs.getString("COLUMN_NAME");
    String comments = rs.getString("COMMENTS");
    System.out.println(ColumnName + ':' + comments + ':' + dataDefault);
  }
} finally {
  try { if(rs!=null) rs.close(); } catch (SQLException e) {}
  try { if(ps!=null) ps.close(); } catch (SQLException e) {}
}

Oracle Database JDBC開発者ガイドおよびリファレンスにはたしかに「LONG列に後でアクセスしようとしても、データは使用できず、ドライバは「ストリームがクローズされています。」エラーを戻します。」と書いてあるけど。

気づかないよこんなの。

このOracle JDBCドライバのLONG列に対する制限って、Oracle+Javaな界隈では常識レベルの有名な話だったりするのかなあ?ググっても全然出てこなかったよ…

2010/11/13 Javaのコードに一部誤りがあったので修正しました。また、文章の一部を修正しました

Windows7 で Oracle10g を

職場で Windows7 に Oracle10g クライアントをインストールすることになった。

Oracle10gってWindows7対応はどうなってるのかな〜とおもってググって見ると、Oracle10g Exはうまくいっているようだけど、それ以外はなんかうまくいってない話ばかりが検索されてくる。

実際、手持ちのOracle10g Client(10.2)のインストーラを使うと、互換性がどうのといわれるのでひとまず途中でやめた。

ま、こういうときは普通にドキュメントにあたるのが一番なので、ドキュメントにあたってみると、Vistaには対応しているらしく、Vistaにインストールするにはこうしなさいとかかれていた(要約)

2点目の「管理者権限で実行せよ」はわかるけど、最新のインストーラはどうするの?とおもって調べてみると、以下からダウンロードできることがわかった

http://www.oracle.com/technology/software/products/database/oracle10g/htdocs/10203vista.html

なんと、本国OTNのページ。

で、ここはOTN JapanのIDは通用しないので本国IDを取得してダウンロード。

これを使うことで、問題なくWindows7にインストールできました。

しかし、このダウンロードサイト、10.2.0.3のCDを丸ごとダウンロードできるんだがいいのか…?

scheduleAtFixedRate() を実行中のタスク側から停止する

Javaで、一定時間ごとに実行するプログラムをかく必要があったのでjava.util.concurrentのScheduledExecutorServiceを利用してプログラムを書いた。

一定時間ごとの実行についてはscheduleAtFixedRate()を使って難なく書けた。
しかし、このとき実行されるタスクが異常終了した場合に、定期的な実行を停止するように書きたかったのだがそこでちょっと引っかかった。

scheduleAtFixedRate()のドキュメントによると、

いずれかのタスク実行が例外に遭遇すると、後続の実行は抑制されます。

と書かれてある。

でも、scheduleAtFixedRate()に渡すタスクはRunnable。
Runnable.run()って例外スローできないよね?じゃ、どうやって例外スローするのさ。

と、ここまで考え至って思いついたのは、throws宣言を書かなくてもいいThrowableなクラス、Errorを投げればいいんじゃないか?ということ。

これでうまくいくか下のソースを書いて試してみた。

public class ScheduledAtFixedRateTest {
	public static void main(String[] args) {
		ScheduledExecutorService exSvc = Executors.newSingleThreadScheduledExecutor();

		final ScheduledFuture<?> f = exSvc.scheduleAtFixedRate(new Runnable() {
			int execCnt = 0;
			@Override
			public void run() {
				System.out.println("[" + ++execCnt + "]" + new Date());
				if(execCnt>=2) throw new Error(); //2回超えたらストップ
			}
		}, 2, 2, TimeUnit.SECONDS); //2秒毎に実行

		exSvc.schedule(new Callable<Boolean>() {
			@Override
			public Boolean call() throws Exception {
				return f.cancel(true);
			}
		}, 10, TimeUnit.SECONDS); //10秒後に停止

		System.out.println("StartTime = " + new Date());
		while(!f.isDone()) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO 自動生成された catch ブロック
				e.printStackTrace();
			}
		}
		exSvc.shutdown();
		System.out.println("EndTime = " + new Date());

	}
}

結果:

StartTime = Tue Mar 23 07:09:03 JST 2010
[1]Tue Mar 23 07:09:05 JST 2010
[2]Tue Mar 23 07:09:07 JST 2010
EndTime = Tue Mar 23 07:09:07 JST 2010

10秒後の停止ではなく、2回目の実行でうまく停止した様子。
今回はこのやり方で書いてみるが、この使い方、正しいんだろうか?