時刻の扱いにご用心
シェルフィー株式会社のエンジニア、hinoです。
最近の業務の中でシステム経由でDBに格納された時刻を比較しようとしてハマった瞬間があったので、せっかくなのでブログに書きつつ軽くまとめようと思います!
時刻や日付の扱いに気をつけよう!というのはよく話題になることかと思いますが、こんなパターンもあるんだなぁ...とどなたかの学びのきっかけになることを祈って🙏
背景
弊社のシステムAではユーザーの勤怠時刻を入力する部分があります。
この時刻は一度入力された後に変化することはあまりありませんが、勤怠承認時にミスが発覚し修正されたり等は稀に発生します。また、別のシステムBと勤怠時刻を同期することが可能なので、定期的にシステムA・B間で時刻に差分がないかチェックする処理があります。
今回ここで起きていたバグの調査でハマりました。
何が起きたか
ある日、過去の勤怠時刻の中で時刻が修正されたわけではないにも関わらず、2つのシステム間で時刻のズレを検知しているものがあることに気づきました。
2つのシステム上で表示されている時刻は同じ。
調査のためにDBを見ても格納されている時刻は同じ。
周辺で若干複雑な処理をしている部分でもあるので、そのあたりで不具合が?と思って調査するも分からず...。
結局、頑張って関連しそうなデータを集めてローカルで再現させてデバッグしていくことにしました。
結果
なんとかローカルでの再現に成功!デバッガで値をみていくと...
退場時刻
---
12:12:12.012345
元の勤怠時刻にマイクロ秒が!
この勤怠時刻の値はブラウザ経由で勤怠の手動入力をする他に、ICカードのタッチ、顔認証での入退場等、いくつか入力されるルートがあるのですが、そのうちのいくつかはマイクロ秒も送信するようになっていたのです。
それを考慮した時刻の比較になっていなかったため、片方はマイクロ秒あり、もう片方はマイクロ秒なしという状態となり差分を検知していたわけです。
IntelliJでDBの値を見ていた時にはマイクロ秒以下が勝手に丸められていて気づきませんでした...。
どうしたか
ぱっと考えられる対策としては以下3つかなと考えました
- テーブル定義で秒未満が入らないようにしておく
たとえばPostgresで既存テーブルのカラムを変換するなら下記
ALTER TABLE テーブル名 ALTER COLUMN カラム名 SET DEFAULT CURRENT_TIMESTAMP::time(0);
場合によってはhour, minute, secondでそれぞれカラムを用意しておく方がいいかもしれません。(弊社でも一部こうなっています)
- アプリ側で時刻を比較する時に丸める
実際に使うロジックが限定されているなら、比較時にマイクロ秒が入る可能性を考慮して丸めてから比較すればいいだけなので単純ですね!
例えばpythonで下記のようにするとか
time.replace(microseconde=0)
- 取得時に丸める
SQLで取得する必要があるなら下記
select カラム名::time(0) from テーブル名
ORMで取得するなら取得した値をオブジェクトにマッピングする際に丸めて格納しちゃえばいいですね!
今回の場合は
- すでに入力された値を変更するわけにはいかない
- データ取得部分が若干複雑で、SQLで取得しておりさらに工夫を加えたくなかった(ベースとしてDjangoを使用しています)
- 取得した値をオブジェクトにマッピングしており、そこで丸めてしまえば抜け漏れなくできそうだった
という点から取得時にデータを格納するオブジェクトで丸めることにしました。
所感とまとめ
これまで自分の経験としてファイルシステムを扱う上でのタイムスタンプをみることはあったのですが、DBに格納された時刻を扱う機会はあまりなかったのでいい経験になりました!
今回の場合は値が格納される経路が複数あったので、そういう場合は特に注意が必要だと思いました。
ローカル環境で再現させるためのデータの用意が大変そうだったので最初は気が重かったですが、結果的にローカルでの再現 ⇛ デバッグ実行したことですぐに発見できたのでよかったです!
バグ調査では愚直に手を動かすことも大事だなと改めて感じました。
Discussion