JDBCでバインドできる文字列のサイズの制限→複数のバインド変数を使って解決

以下で紹介されているOracle JDBCJava.sql.PreparedStatement#setString()の制限について。
[Oracle] JDBCでバインドできる文字列のサイズの制限 | Archive Redo Blog

例えばデータベースキャラクタセットがJA16SJISのDBにvarchar2(4000)の列があったとしても、PreparedStatment#setString()ではUTF-8換算で2000バイトまでしか挿入できない。
UTF-8は日本語が3バイトなので、全て日本語の場合だと666文字までしか入らないという問題があります。
上記のページではsetCharacterStream()を使って解決としていますが、
以下のページではまた別の問題が起きることを指定しています。
http://rryu.sakura.ne.jp/nisenise-fuhito/200402.html

setCharacterStream()を使っている複数の列のうち2つ以上が一定以上、つまり前述の長さ制限よりも長いデータになるとORA-01483: invalid length for DATE or NUMBER bind variableという不可思議なエラーが出るのだ。
いろいろやっているうちにふとエラーになる原因を思いついた。LONG型の列はテーブルにつき1個までしか存在できないという制限がある。そして実のところPreparedStatement#setCharacterStream()はそのLONG型専用のメソッドである。したがってこのメソッドを複数の列に対して使用するとLONG型の列が複数あるという事になるのでエラーになると。一定以下の長さの場合にはエラーにならないのは、おそらくリテラルの限界以下の場合はそのメソッドを使用してもsetString()を使用したのと同じ事になるよう最適化されているのだろう。と、考えれば納得できる挙動だが、エラーメッセージが激しく間違っているのはバグだろうか。

ということで、こんな解決方法を考えてみた。

テーブルHatenaにLongTextというvarchar2(4000)の列があったとして、
これにインサートするSQLを次のように書きます。

insert into Hatena (LongText) values ( ? || ? || ? || ? );

もうお分かりかと思いますが、一列に対するバインド変数を4つに分けて、更新値をそれにあわせて分割するというやり方です。

final String sql = "insert into Hatena (LongText) values ( ? || ? || ? || ? );";

PreparedStatement stmt = conn.prepareStatement(sql);

int ix	= 0;
String[] sqls	= {"","","",""};
final String utf8	=	"UTF-8";
final String sjis	=	"Windows-31J";

// 更新値がSJIS換算で4000バイトを超えていないことをチェック
if(updateVal.getBytes(sjis).length > 4000){
	throw new Exception("値が4000バイトを超えています");
}
					
// SQLがUTF-8で2000バイト以内ならバインド変数一つで十分
if ( sql.getBytes(utf8).length > 2000 ) {

	/**
	 * SQLがUTF-8で2000バイト以上なら、複数のバインド変数に分けて送信する。
	 * 分けた後の各部分文字列がUTF-8で2000バイト以内で、
	 * かつこれらの結合文字列がSJISに変換後4000バイト以内になるようにする。
	 */

	/**
	 * 666文字までなら全て日本語だとしても2000バイトは超えない。
	 * 従ってこれを分割単位とし、4分割まで行う。
	 * SJISの4000バイトはUTF-8になると最大6000バイトまで膨らむが、
	 * 4分割なら666×3×4=7992まで送ることができる。
	 * (最初にSJISで4000バイトより大きいSQLをはねているので、実際はUTF-8換算で6000バイトが最大)
	 */
	final int maxLength	=	666;
	int startIx	=	0;
	int size	=	sql.length();
			
	for ( int i = 0 ; i < 4 ; i++ ) {
		if ( size < maxLength ) {
			sqls[i] = sql.substring(startIx, startIx + size);
			break;
		}
		sqls[i] = sql.substring(startIx, startIx + maxLength);
		startIx	=	startIx + maxLength;
		size -= maxLength;
	}
}else {
	sqls[0] = sql;
}
for(int i = 0; i < 4; i++){
	stmt.setString(++ix, sqls[i]);
}
stmt.executeUpdate();

小手先ですが・・・