読者です 読者をやめる 読者になる 読者になる

09

ZipInputStream から BLOB へ流す

今日のハマりをさらすコーナー。

複数のファイルが格納されたZipファイルを読んで、ファイルごとにレコードを追加する形でそのままデータベースのBlobに書き込もうと思った。DBMSはPostgresql。 Postgresqlの場合、Binary Large Object は bytea型にしておけば1Gまでは入るらしい。ドキュメントによるとあまり大きなオブジェクトは良くないようではある。

アップロードされたZipファイルを ZipInputStream で読み込んで、ストリームそのままDBのBlobに書き込もう、と考えた。ここでintra-mart Accel Platformのスクリプト開発モデルのAPIリストを見ていて、TenantDatabase に次のようなメソッドがあるのを見つける。

getByteReader (String tableName, String columnName, String condition, Array [params], Function [callback])

テーブルとカラムを指定して読み出している。ということは、書き込むときも同じようなメソッドがあるのだろうか?と思ってAPIリストを眺めてみてもそれらしきメソッドは存在しないようだ。仕方ない、と思ってJavaでZipアーカイブをストリームで読み込みつつBlobレコードをインサートするユーティリティを書いた。

ここでJavaではinsertを実行する際にパラメータとしてバイナリのStreamをそのまま渡している。それを見てスクリプト開発モデルでも同様の扱いなのではないかと気づき、APIリストを確認してみるとやはり DbParameter.blob() が定義されており、insert文の引数として指定できるようになっていた。わざわざJavaで書く事無かった。

若干がっくりとしながらも、 スクリプト開発モデルではZipアーカイブをストリームで処理できないはずと気を取り直して動かしてみたが、どうもinsert がうまく行かない。Javaで書いたユーティリティではストリームをパラメータにセットする際JDBCの以下のメソッドを仕様しているのだが

setBinaryStream(int index, InputStream binaryStream, int length) 

Postgresql の方からエラーを返してくる。どうもblobの長さをうまく渡せていないようなエラーだ。 Zipアーカイブを読むときには ZipEntry というエントリの情報を持つオブジェクトからファイル名やファイルサイズの情報を得る事ができるが、なぜかこのオブジェクトがファイル名は返してくるが、その他の情報を渡してこない。ファイルサイズについては-1(不明の意)を返してきている。このためにPoatgresql側では値の長さが不正としてエラーにしているようだ。 なぜそんな値が帰ってくるのか分からなかったが、読み込んでいるのはストリームなのだから、長さに関わらずすべて読み込むメソッドは無いかと探してみると、

setBinaryStream(int index, InputStream binaryStream) 

という最後のlengthを省略した形のメソッドが定義されている事が分かった。javadocによればこのメソッドはストリームの最後まで読み込むという。 これだと思って実行してみると、 JDBCドライバの方からそのメソッドはサポートしていないよという例外であっさり終了。

仕方なく ZipEntry が長さを返してこない理由をあれこれ試しながら調べていたが、そもそもこのオブジェクトは ZipInputStream から取得した場合はファイル名しかまともな情報を返さない物らしい。ZipInputStream の代わりに ZipFile というクラスがあり、ZipFile オブジェクトから取得した ZipEntry であれば各種の情報をちゃんと返してくる。ドキュメントには何も書いてないようだが・・・。

しかしこれでも問題があって、このコンストラクタは引数に File または File へのパスを受け取る。よくよく考えると今回のファイルはiAPのストレージサービスから取得する必要があり、 API的にはなんらかのストレージオブジェクトだが、もちろんこのオブジェクトはFileとは何の関係もないから、ZipFile のコンストラクタに渡し様がない。( ストレージオブジェクトからStreamは取得できるので、ZipInputStream は使える)

ここでとりうる手段は2つ。 一つ目は、あくまで ZipInputStream を使い、ストリームを2回読む。1回目でサイズを算出し、2回目のストリームをJDBCのパラメータとしてサイズとともに渡す。 二つ目は、StorageServiceのオブジェクトを File に複製し、これを ZipFile として読む。

取り合えず一つ目を試してみたところうまく行ったので、これでいいや(この処理はどうせバッチジョブなので性能も許容できる)。二つ目を試しても良いが、正直どちらの対応も微妙なのであんまり意味ないだろう。

でもよくよく考えたら、iAPのストレージサービスの場合、ファイルパスさえ分かればそのパスを使って ZipFile 使っても悪くないのかな?基本的にはローカルシステムにマウントされたファイルシステムを前提としていたような気が。