2011年07月02日

H2 Databaseを組込みモードで使ってみたメモ

H2とはあだ・・・いや、なんでもない。組み込みとしてもサーバとしても使えるJava製データベース。

組み込みで使うSQLというとSQLiteがメジャーだけど、JavaやScalaのプログラムに組み込む場合はH2を見かけることもけっこうある。

速度面もそれなりに良い感じだったり、同時アクセスに対してもそれなりに対処できるようなので、ちょっとしたDB操作を伴うScalaのアプリを作ろうとした際に利用できそうに見える。



とりあえず公式サイトからダウンロードできる。
http://www.h2database.com/html/main.html

ライセンスはMozillaとEclipseのライセンスが使えるらしい。

JavaなのでMavenでも落とせる。この辺りは導入が楽でいい。SQLiteも同じ感じで導入できるけど。
<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>[1.3,)</version>
</dependency>




利用方法はTutorialを見ればわさわさ書いてある。
http://www.h2database.com/html/tutorial.html

とりあえずさらっとScalaから利用するところだけやってみる。
import java.sql.DriverManager

// ユーザ名はsa(System Administrator)
val conn = DriverManager.getConnection("jdbc:h2:sample", "sa", "")

conn.close

こうするとカレントディレクトリにsample.h2.dbというファイルができて、データベースの情報はファイル中に格納される。

実行されている間はロックファイルが作られて、その間に他のアプリケーションから使おうとすると以下のようなエラーが出る。

org.h2.jdbc.JdbcSQLException: データベースが使用中です: "Locked by another process". 可能な解決策: 他の接続を全て閉じる; サーバモードを使う

複数アプリから使う場合は、組込みモードだと難しそうです。SQLiteであれば複数アプリからもそのまま使えた気がする(同時にINSERTするとすぐエラーになるけど)。



別アプリからはダメですが、マルチスレッドで複数のコネクションを使う分には問題がないようです。

試しに2つのスレッドで別々のConnectionを取得して、insertをわらわらやってみてロックで落ちたりしないかを確認してみます。

複数スレッドを利用する場合、Scalaではparを使うと何かと便利です。parに2つの関数を渡すと、片方をカレントスレッドで、もう片方を別スレッドで実行してくれます。
// こんな感じで、前がmainスレッド、後ろが子供になります
par({ Thread.currentThread }, { Thread.currentThread })
//=> (Thread[main,5,main], Thread[Thread-0,5,])

ちなみにREPLだと毎回スレッド作って実行しているみたいなので、上記のような結果にはなりません。
// まずCREATE(identityはauto inclementな数値)
val conn = DriverManager.getConnection("jdbc:h2:sample", "sa", "")
val stmt = conn.createStatement
stmt.execute("drop table sample")
stmt.execute("create table sample (id identity, value varchar(255))")
conn.close

// 100件INSERTする関数
def insert(id: Int) {
val conn = DriverManager.getConnection("jdbc:h2:sample", "sa", "")
val stmt = conn.createStatement
for (i <- 0 to 100) {
stmt.execute("insert into sample (value) values ( '%d-%d' )" format (id, i))
}
conn.close
}

// 並列でinsert関数を走らせる
par(insert(1), insert(2))

// 結果
(1,1-0)
(2,2-0)
(3,1-1)
(4,1-2)
(5,1-3)
(6,2-1)
(7,1-4)
(8,1-5)
(9,2-2)
(10,1-6)
以下略

ちゃんとスレッド1とスレッド2に対して非同期に値が入れられている。この辺りの処理はファイルベースの組込みDBにしては軽やかな気がした。

ちなみにSQLiteで同じ事を実行したところ、CurrentThread側だけの情報が登録され、子スレッドの方は下記のようなエラーを吐いて落ちた。

java.sql.SQLException: [SQLITE_BUSY] The database file is locked (database is locked)

この辺りの特性はDBを選ぶ時に大切になるんだろうなと思う。



H2のライセンスはMPL(Mozillaのヤツ)とEPL(Eclipseのヤツ)らしい。

MPLはGPLとBSDの真ん中くらいのライセンス。H2自体を改変しないならソースコードの公開の義務はないし、改変した場合は改変箇所のみ公開みたいな内容だったと記憶している。

組込みでそのまま使う分にはそれほど害はないし、改変してもGPL汚染みたいな全公開的な話にはならないはず。

SQLiteだと天下のパブリックドメインなのでやりたい放題。



Tutorialを見ると、H2はCSVと仲が良いことが分かる。

その中でもCSVREADとCSVWRITE関数は面白そう。

例えば下記のようなCSVファイルがあったとする。

id,param
1,line1
2,line2
3,line3

以下のようなSQLを実行すると、CSVファイルの情報がResultSetで取れる。

select id, param from csvread('file_name.csv')

もちろんWHERE句なども使える。

select id, param from csvread('test.csv') where id > 1



CSVWRITEはTutorialによると以下のような文法になるらしい。

CALL CSVWRITE('test.csv', 'SELECT * FROM TEST');

試しにさっきのcsvファイルからCSVWRITEしてみる。paramだけ持ってくるように指示している。シングルコーテは2つ続けてエスケープする。

call csvwrite('test2.csv', 'select param from csvread(''file_name.csv'')')

結果はこんな感じ。

"PARAM"
"line1"
"line2"
"line3"

うまく使えば、データ操作機能付きCSVライブラリとして使えたりするのだろうか。機会があったらいろいろ使ってみたい。



絶対使いたくなりそうな予感がしたので、CSVの読み書き時の文字コード指定と区切り文字指定も試しておく。特にタブ区切りにしたいケースは多そう。

文字コード、区切り文字の順で引数を追加してあげれば良いらしい。タブ区切りについては\tと

Shift_JISのタブ区切りを読みだしてみる場合はこんな感じ。

select id, param from csvread('file_name.csv', 'Shift_JIS', '\t')

CSVWRITEで出力する場合はこんな感じ。
"call csvwrite('test2.csv', 'select * from csvread(''file_name.csv'')', 'Shift_JIS', chr(9))"



今日のところはここまで。