Edited at

Railsの「その日の終わりの時刻」(end_of_day)で生成する時刻とMySQLに格納される値

More than 1 year has passed since last update.


はじめに

このメモは、Railsのspecを書いていた時に、どうしてもテストが通らなくてハマった点について書いています。

内容としては表題の通りですが、かいつまんで言うと以下のような感じです。

前提条件


  • DateTime型の列に、その月の最終日の値を設定する

  • その月の最終日、かつ、できれば最終時刻を格納したい

  • Railsの日付関数 end_of_day を使って最終時刻を出し、その値を格納する

書いたテスト


  1. その日の最終日時を生成 ( Exp. a = Time.current.end_of_day )

  2. 1の値をテーブルに格納, save ( Exp. xxxx.closed = a )

  3. save後に1の値を含むインスタンスを取り出して、1の値 (Exp. expect(xxxx.closed).to eq a )


単純にテストした結果

単純に比較したら、ミリ秒単位で差が生じてエラーになってしまいました....

こんなメッセージが。

expected: 2016-12-31 23:59:59.999999999 +0900

got: 2016-12-31 23:59:59.000000000 +0900


失敗した理由

saveしてDBに格納される前は、精度は落ちません。

saveしてデータを取り出すと、ミリ秒分は0で埋まってしまいます。

知らなかった自分が悪いのですが、MySQLのDATETIME型はミリ秒省略されてしまうようで、例えば23:59:59.9999 の元データを生成したとしても、 23:59:59.000 で入るんですね...

今回、DoRuby様の記事が大変参考になりました。ありがとうございました!


とりあえずの対応

save前のデータもsave後に取り出したデータも、型はActiveSupport::TimeWithZoneで同じなのですが、精度が違っているため単純に eq ができませんでした。

結局、to_sで文字列に変換して比較をしています....

it '対象月のレコードのcloseは対象月の月末になる' do

#
# NOTE: MySQLのDatetimeはミリ秒が省略されるため、
    # 23:59:99:99 -> 23:59:00:00に丸められてしまう
# 文字列で比較を実施
expect(xxxx.closed.to_s).to eq target_month.end_of_month.end_of_day.to_s
end


まとめという名の感想

今回はMySQLなので、このような状況になっています。他のDBに関しては試していないのですが、いろいろ差異はありそう。DBの際や制約もうっかりして気がつかないでいると、妙なところでハマるんですね。

Railsに限った話ではないですが、何か日付の最終時刻まで有効といった条件は、「その日のend_of_day」 ではなく 「翌日のbeginning_of_dayより小さい」という条件に設定したほうが安全そうだ...というのも納得した次第です。

恥ずかしながらのメモでした。


20170203 追記:

DB的な精度の関係でどうしても丸められてしまう場合、上記の例では to_s で対応してみましたが、ある範囲内での誤差を許容したテストを書きたい場合は、こんなものを利用します。

書き直してみると、こんな感じでしょうか。

it '対象月のレコードのcloseは対象月の月末になる' do

#
# NOTE: MySQLのDatetimeはミリ秒が省略されるため、
    # 23:59:99:99 -> 23:59:00:00に丸められてしまう
# be_withinで誤差を許容して比較
expect(xxxx.closed).to be_within(1.second).of(target_month.end_of_month.end_of_day)
end