LoginSignup
12
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-01-11

はじめに

このメモは、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
12
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8