ちょっと前にConnector/Jが1.3.0にアップデートされたのですが、とたんにTINYINT(1)のカラムの値を正しく取得できなくなりました。
MySQLはBOOLEANをTINYINT(1)の別名として扱います。アプリケーションから見たらこのカラムはBoolean型です。
で、Connector/Jのソースを追っかけて行った所、ドライバ側のバグだろうという結論に。
org.mariadb.jdbc.internal.queryresults.MariaDBValueObjectのgetObjectメソッドが怪しいです。以下抜粋。
public Object getObject(int datatypeMappingFlags, Calendar cal) throws ParseException { if (this.getBytes() == null) { return null; } switch (dataType) { case BIT: if (columnInfo.getLength() == 1) { return (getBytes()[0] != 0); // 正しい判定方法 } return getBytes(); case TINYINT: if ((datatypeMappingFlags & TINYINT1_IS_BIT) != 0 && columnInfo.getLength() == 1) { /* ココがおかしい (byte) '0' == (byte) 48 である "case BIT"の判定方法は正しいのに、何故こちらは文字との比較? */ return (getBytes()[0] != '0'); } return getInt(); ... }
ちなみに、getBytes
はDBから読み出した生のバイト列を返します。
getBooleanはセーフ
getBooleanを使う場合は正しく値を取得できます。文字列で比較しているからです。
// MariaDBValueObject#getBoolean public boolean getBoolean() { if (rawBytes == null) { return false; } final String rawVal = new String(rawBytes, StandardCharsets.UTF_8); return rawVal.equalsIgnoreCase("true") || rawVal.equalsIgnoreCase("1") || (rawBytes[0] & 0x1) == 1; }
しかしまあ、よくもこんな手抜きコードを書けたものですなあ。コードレビューしてないのか?
MySQLをJDBCで扱う場合、Boolean型のカラムが大量にあると性能に悪影響がありそうです。
回避策
上記のようにBoolean型のカラムが常にtrueになってしまうので、TINYINTをBITとして扱わせるのをやめて数値で扱うようにしました。
JDBCのURLのクエリパラメータに
tinyInt1isBit=false
を追加すればOK。
なぜか1.2.2でも同じコード
しかし解せないのは、このバグが発生しない1.2.2でも全く同じコードだったということです。以下抜粋。
// MySQLValueObject#getObject public Object getObject(int datatypeMappingFlags, Calendar cal) throws ParseException { if (this.getBytes() == null) { return null; } switch (dataType) { case BIT: if (columnInfo.getLength() == 1) { return (getBytes()[0] != 0); // 正しい } return getBytes(); case TINYINT: if ((datatypeMappingFlags & TINYINT1_IS_BIT) != 0) { if (columnInfo.getLength() == 1) { return (getBytes()[0] != '0'); // 正しくない } } return getInt(); ... }
何なんでしょうね一体…これ以上調べる気力が起きないので一先ずはここまで。
2016-03-04追記
リリース1.3.6で修正されました。
https://mariadb.atlassian.net/browse/CONJ-255
問題の箇所が
case TINYINT: if (options.tinyInt1isBit && columnInfo.getLength() == 1) { if (!this.isBinaryEncoded) { return rawBytes[0] != '0'; } else { return rawBytes[0] != 0; } } return getInt();
のように修正されており、byte値の0
とcharの'0'
の場合でそれぞれ正しい判定がなされるようになっています。
ついでに、ひどい手抜き実装だったgetBooleanも
public boolean getBoolean() throws SQLException { if (rawBytes == null) { return false; } if (!this.isBinaryEncoded) { if (rawBytes.length == 1 && rawBytes[0] == 0) { return false; } final String rawVal = new String(rawBytes, StandardCharsets.UTF_8); return !("false".equals(rawVal) || "0".equals(rawVal)); } else { switch (dataType) { case BIT: return rawBytes[0] != 0; case TINYINT: return getTinyInt() != 0; case SMALLINT: case YEAR: return getSmallInt() != 0; case INTEGER: case MEDIUMINT: return getMediumInt() != 0; case BIGINT: return getLong() != 0; case FLOAT: return getFloat() != 0; case DOUBLE: return getDouble() != 0; default: final String rawVal = new String(rawBytes, StandardCharsets.UTF_8); return !("false".equals(rawVal) || "0".equals(rawVal)); } } }
のようにリファクタリングされました。