JDBCでバインドできる文字列のサイズの制限→複数のバインド変数を使って解決
以下で紹介されているOracle JDBCのJava.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();
小手先ですが・・・