Java String → Oracle varchar2 入りきらない文字列をByte単位で切り捨てるための策
今回の経緯
お仕事ではJava7とOracle11を使っている。今回の改修で問題になったのが、インポートする予定のCSVファイルのある文字列データが、何文字入るのか不定であること。
もし、入力文字列が100Byteで、受け止めるカラムでvarchar2(64 Byte)しか用意してないとき、INSERTしたらORA-12899 でエラー吐いてしまう。
対応としては以下ものがパッと考えられる。
- まずそんなデータを入力するな。データ仕様くらい決めておけ。
- ほんそれ
- CROBで受け止めればいいじゃん。&varchar2(4000)にしよう。
- データ量と速度の観点から、実用的でない。ウン十万行あるテーブルなんでできるだけ、データ容量もメモリバッファも減らしたくない。
- 文字列オーバーしたら切り捨てよう。
- え、データなくなってもいいんだ。。。そっか。。。
ということで、今回の方策としては、「文字列オーバーしたら切り捨てよう」なった。そっか。。。
そもそも入ってくるだけで使わないデータだという。でもDBには保存してほしいんだと。そっか。。。
既存のCSVtoDBの処理では、PLSQL使わずに直INSERTしているため、Javaでのバリデートである。
文字列の切り捨ての方策
String.substr()
で行こうと思ったが、少し考えればそれじゃダメなことが分かる。varchar2のサイズ指定は、デフォルトでByte単位だ。文字数じゃない。それに、DB側はMS932なのに対し、Javaの内部処理は決まってUTF-16である。これじゃいかん。
とりあえず、2通りの方法で切り捨て可能だと思う。
String.substr()
を使うなら、テーブル定義の変更で吸収する必要が出てくる。navarchar2か、varchar(20 CHAR)で宣言するか、もしくはNLS_LENGTH_SEMANTICSをCHARにするか。これまでのDB設計や、特殊文字入ることも考えると良くないなと思う。変更箇所が分かれるのは混乱を招くし、そもそもDBは専門外なんで、余計なことはしたくない(甘え)。JavaでちゃんとByte単位で切り捨ててからINSERTしてやる。これが一番健康的であろう。なら、
String.substr()
は使えない。
java.nioで切り捨てる
参考にしたのは、Qiita - Java 文字列をバイト数で切り捨てる
上記の例だと、特殊文字がうまいことエスケープできていなかったようなので、手直しした。でもこれでいいかは不安である。
import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; public class Converter { /** * @param s 文字列 * @param charset 文字コード * @param maxBytes 最大バイト数 * @return 最大バイト数に入りきる分だけの文字列 */ public String cutString(String s, Charset charset, int maxBytes) { ByteBuffer bb = ByteBuffer.allocate(maxBytes); CharBuffer cc = CharBuffer.wrap(s); CharsetEncoder encoder = charset.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE) .reset(); encoder.encode(cc, bb, true); encoder.flush(bb); bb.flip(); return charset.decode(bb).toString(); } }
いきなりjava.nio
のクラスがぞろぞろ出てきて嫌だけど、1行1行理解すれば読めると思う。
- まず
ByteBuffer bb
にmaxBytes
分の箱を用意しておく。 - つぎに
CharBuffer cb
に入力値を入れる。 - 読み取れないバイトコードをデフォルトのリプレース文字(
?
)に変更するエンコーダーを作成する。 - エンコードを通す。すると、
bb
に入る分だけ文字が入る。2バイト文字等で最後まで入りきらない場合は、あきらめてくれる。 flush()
でメモリ内に残ったデータを排出する。bb
を反転させて、データが入っているところだけを有効化する。- デコードする(ここは簡易メソッドでいいはず)。
バイトに直した後に、わざと小さい箱を用意して入れるだけ入れて、文字列に戻すという操作をする。
もっといい方法はありそうだけど、とりあえずこれでいいか相談だ。
雑感
文字コードを忌避していた節があったから、たった一つのメソッドだけど勉強になった。
参考にしたのはこの辺
すごい煽ってくるけどすごく参考になりました。ありがとうございます。
Shift-JISとMS932の違いについて解説している
あと、記事書くのに時間かかり過ぎだ。もっと気楽な気持ちで書きたいな。