徒然

日常を垂れ流します。

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 bbmaxBytes分の箱を用意しておく。
  • つぎにCharBuffer cb に入力値を入れる。
  • 読み取れないバイトコードをデフォルトのリプレース文字(?)に変更するエンコーダーを作成する。
    • onMalformedInputは入力値に不正な(Malformed)文字がないか判定する
    • onUnmapppableCharacterエンコード後のバイトが変換先の文字コードに存在するか確認する(例:SHIFT_JISに変換するなら①とか)。
    • reset()は礼儀。
  • エンコードを通す。すると、bbに入る分だけ文字が入る。2バイト文字等で最後まで入りきらない場合は、あきらめてくれる。
  • flush()でメモリ内に残ったデータを排出する。
  • bbを反転させて、データが入っているところだけを有効化する。
  • デコードする(ここは簡易メソッドでいいはず)。

バイトに直した後に、わざと小さい箱を用意して入れるだけ入れて、文字列に戻すという操作をする。

もっといい方法はありそうだけど、とりあえずこれでいいか相談だ。

雑感

文字コードを忌避していた節があったから、たった一つのメソッドだけど勉強になった。

参考にしたのはこの辺

www5d.biglobe.ne.jp

すごい煽ってくるけどすごく参考になりました。ありがとうございます。

qiita.com

Shift-JISとMS932の違いについて解説している

 

あと、記事書くのに時間かかり過ぎだ。もっと気楽な気持ちで書きたいな。