jfluteの日記 このページをアンテナに追加

2017-08-20

DBFlute-1.1.4 Released

|

Java8対応のDBFluteの十四回目のリリース。
DBFlute-1.1.4 です。

DBFlute Top
Change Log
移行の注意点 1.1.2 to 1.1.4

LastaFlute/SAFluteを使われている方は、
若干お知らせがありますので、
移行の注意点をお読みください。

今回も、様々な微調整が多いですが、
一番大きなトピックはこちら!

MySQLのミリ秒なしDATETIMEのミリ秒問題

MySQLでは、
もともとDATETIME型がミリ秒をサポートしていませんでした。
つまり、2017/08/20 12:34:56 の秒までしか、
入りませんでした。

そして、MySQL-5.6 のどこかのバージョンから!?
ミリ秒やマイクロ秒を指定できるDATETIMEがサポートされました。
DDLでは、DATETIME(3) とか DATETIME(6) などと指定します。

それに伴いJDBCドライバーも、ミリ秒に対する扱いが変更されました。
要は、ミリ秒を意識するようになりました。

ミリ秒があるDATETIMEに対して、
ミリ秒が意識されるのは良いことなので、
それはそれでいいのですが...

なんだかんだ、既存のDBだと、
ミリ秒ない時代に作った DATETIME がたくさんあるので、
ミリ秒が意識されるとちょっと変なことになります。

JDBCの「5.1.若い頃バージョン(12とか)」

(ミリ秒のないDATETIMEに対して)

insert や update などのとき、
Javaの中で、2017/08/20 12:34:56.789 という風に、
ミリ秒ありの日時をバインド変数に指定しても、
単純に 789 が切り取られるだけでした。
2017/08/20 12:34:56 が登録されます。

また、select の where 句の条件のとき、
Javaの中で、2017/08/20 12:34:56.789 という風に、
ミリ秒ありの日時をバインド変数に指定しても、
同じく単純に 789 が切り取られるだけでした。
2017/08/20 12:34:56 として比較されます。

少なくとも MySQL Connector/J 5.1.12 では、
そのような挙動をしました。
MySQL本体は、5.7.13 で試しています。

JDBCの「5.1.進んだ頃バージョン(37とか)」

(ミリ秒のないDATETIMEに対して)

insert や update などのとき、
Javaの中で、2017/08/20 12:34:56.789 という風に、
ミリ秒ありの日時をバインド変数に指定すると...

ミリ秒が四捨五入されて

2017/08/20 12:34:57 が登録されます。
秒が一つ増えています。

これつまり...
2017/08/20 23:59:59.789 だと、
2017/08/21 00:00:00:000 になるということです。

また、select の where 句の条件のとき、
Javaの中で、2017/08/20 12:34:56.789 という風に、
ミリ秒ありの日時をバインド変数に指定すると、

ミリ秒が考慮されて

比較されます。
2017/08/20 12:34:56 が入ってるDATETIMEカラムに、
2017/08/20 12:34:56.789 を <= とかやっても、
ヒットしないということです。

where END_DATETIME >= 2017/08/20 12:34:56.789
(2017/08/20 12:34:56が入ってるカラム)

これがヒットしません。
四捨五入されるわけでもなく、
カラム側が .000 と解釈されて比較されます。

少なくとも MySQL Connector/J 5.1.37 では、
そのような挙動をしました。
執筆時点での最新 5.1.43 でも変わりませんでした。
MySQL本体は、5.7.13 で試しています。
(フィードバックをくれた現場では、
5.6 系で同じ現象になっているようです)

...

MySQLJDBCドライバーは、
ミリ秒がないDATETIMEに対して
何か特別な処理をするわけでもなく、
ミリ秒があるDATETIMEに対する処理と
同じ扱いをするようです。

登録時の四捨五入のジレンマ

そのギャップでちょっと困ることがあるかもしれません。
例えば、23:59:59 と日時を埋めて登録するとき、
そのままだと四捨五入で次の日になってしまいます。
Java側でミリ秒を切り取ればOKですが、
そこまでDBの挙動を意識した処理が
業務ロジックに入ってしまいますし、
どのみち抜けると思います。

そもそもDB設計的に、
23:59:59を埋めるようなカラムを作らない、
というところかもしれませんが、
既存DBだとそうもいかないでしょう。

検索時の考慮されジレンマ

selectのwhere句の方は、さらに困る事態に。
BEGIN_DATETIME, END_DATETIME と、
有効期間で積み重なるようなテーブルで、
END_DATETIME が「終了直前の最後の瞬間」を表現してる場合、

BEGIN_DATETIME: 2017/08/12 00:00:00
END_DATETIME: 2017/08/19 23:59:59

BEGIN_DATETIME: 2017/08/20 00:00:00
END_DATETIME: 2017/08/31 23:59:59

where
BEGIN_DATETIME <= 現在日時 <= END_DATETIME
(現在日時: 2017/08/19 23:59:59.001)

これで、ヒットしません。
つまり、DB設計としては、
現在日時を指定してヒットしない瞬間がないようにしてても、
「一秒の隙」で何もヒットしない瞬間があるということです。

これは、プログラムの制御によっては、
処理がエラーで落ちてしまう可能性があります。

Java側では、ミリ秒のない日時型ってありませんから、
現在日時を取得するとどうしてもミリ秒は入ります。
現在日時の取得インターフェースでミリ秒を削るか?
と言ったら、ミリ秒が必要なこともあるでしょうから、
それはそれで将来への負債へとなりかねません。
(ミリ秒ありDATETIMEが混じったらなおさら)

こちらも、END_DATETIMEの仕様を、
「終了直前の最後の瞬間」ではなく、
「終了直後の最初の瞬間」にすればOKですが、
(つまり、次の期間のBEGINと同じ日時にする)
既存DBではそうもいかないでしょう。

# そもそもどっちの方が良い、好き、とか、
# 他の要因でもあったりするでしょうから。

CBの条件でミリ秒を削る

そこで、今回 DBFlute では、
ひとまず、selectのwhere句の方のジレンマだけ、
解決というか回避するオプションを導入しました。

ConditionBeanが、
ミリ秒のないDATETIMEに対する条件値のとき、
引数で指定された日時データのミリ秒を削る

というものです。
昔のMySQLJDBCドライバーがやっていたことを、
ConditionBeanが再現します。

littleAdjustmentMap.dfprop にて、

isDatetimePrecisionTruncationOfCondition

を true に設定して自動生成し直すと、
そのような挙動になります。

もし、この問題をすでに抱えている、
もしくは、これを読んで問題があることがわかった方は、
こちらのオプションを検討してみてください。

...

ちなみに、
ミリ秒がないDATETIMEに対してのみの処理になっています。
例えば、DATETIME(1)のような、
ミリ秒の最初の一桁だけサポートしているカラムでも、
論理的には「0.1秒の隙」が発生する可能性があります。
ですが、そもそもミリ秒の最初の一桁だけのDATETIMEを
作ることはほとんどないでしょう、ということで。

...

こちら実現方法はちょっと大変でした。
「DATETIMEのミリ秒ありなし」を判断しないといけません。
ミリ秒ありのDATETIMEに対して何か余計なことを
やってはいけないので、ちゃんと判断しないといけません。

でも、JDBCドライバーのメタデータから、
DATETIMEのミリ秒精度を取得することができないのです。
DatabaseMetaData@getColumns()の項目にないのです。
なので、苦肉の策で、INFORMATION_SCHEMAのテーブルを、
直接selectして、DATETIME_PRECISOIN を取得しています。
SchemaXML にも、datetimePrecision が追加されました。

マニアックですが、けっこう手間かかってます笑
トラックバック - http://d.hatena.ne.jp/jflute/20170820/release114