[MongoDB]文字列でJSTの日時情報を扱う

前回の補足記事。

はじめに

MongoDBにおける日時は、ISODate型かつUTCが基本となります。

ですが、string型かつJSTで扱いたいケースもあるかもしれません。その場合、若干の加工が必要になります。

いくつかやり方はあるかと思いますが、その一つを紹介します。

環境・前提

  • 環境
    • MacOS 10.13.6
    • MySQL 5.6
    • MongoDB 4.0.6
  • 前提
    • データは前回の記事で使用したもの。
      • uid, device, created_at : すべて文字列
    • created_atの格納データは、文字列(string型)かつJST。2019-04-22 00:00:00のような形式
    • やりたいことも前回と同様、uidとdeviceのユニークな組み合わせのリストを、日別で取得したい。加えて、日別の日時がきちんとJSTになっていることを目的とする。

まずはcol.find()して文字列を確認

最初にデータの中身を確認してみます。

# クエリ
col.find()

# 結果
{ "_id" : ObjectId("5cbc9f47bc27db275f687fad"), "uid" : "hoge", "device" : "xxx", "created_at" : "2019-02-01 00:00:00" }
{ "_id" : ObjectId("5cbc9f47bc27db275f687fae"), "uid" : "fuga", "device" : "yyy", "created_at" : "2019-02-01 00:00:01" }
{ "_id" : ObjectId("5cbc9f47bc27db275f687faf"), "uid" : "hoge", "device" : "xxx", "created_at" : "2019-02-01 00:00:02" }
...

現状では、created_atはただの文字列です。
なので、aggregateでこのフィールドをISODate型と同じノリで扱おうとするとエラーになります。
$toDate で変換しましょう。

※ちなみに、$lteや$gteなどの比較自体は文字列のままでも大丈夫です。

文字列を$toDateでシンプルに変換

以下のクエリでは、文字列をISODate型に変換していますが、JSTではなくUTCになってしまいます。(末尾のZはUTCを意味しています)

# クエリ
col.aggregate(
    [
    {$project:
        {
            _id: 0,
            uid: 1,
            device: 1,
            created_at: 
            { 
                $toDate: "$created_at"
            }
        }
    }
    ]);

# 結果
{ "uid" : "hoge", "device" : "xxx", "created_at" : ISODate("2019-02-01T00:00:00Z") }
{ "uid" : "fuga", "device" : "yyy", "created_at" : ISODate("2019-02-01T00:00:01Z") }
{ "uid" : "hoge", "device" : "xxx", "created_at" : ISODate("2019-02-01T00:00:02Z") }
...

時間差を考慮して変換する

時間差を考慮した変換をするためには、
変換する前に、$concat を使い、JSTの時間差分を文字列に追記します。

例えば 2019-02-01 00:00:00+0900 という文字列にしてから$toDateで変換する、ということです。

# クエリ
col.aggregate(
    [
    {$project:
        {
            _id: 0,
            uid: 1,
            device: 1,
            created_at: 
            { 
                $toDate: 
                {
                    $concat: ["$created_at", "+0900"]
                }
            }
        }
    }
    ]);

# 結果
{ "uid" : "hoge", "device" : "xxx", "created_at" : ISODate("2019-01-31T15:00:00Z") }
{ "uid" : "fuga", "device" : "yyy", "created_at" : ISODate("2019-01-31T15:00:01Z") }
{ "uid" : "hoge", "device" : "xxx", "created_at" : ISODate("2019-01-31T15:00:02Z") }
...

これで、JSTの時間をUTCで表記した結果になりました。

$groupでまとめる

次に、日別の結果を取得するために $dateToString でグルーピングします。
しかし、このままではUTCでまとめられてしまいます。

なので、まとめる前に時間差分を $add で足します。
$dateToString の引数dateに、$created_at(前項のISODate型データ)+9時間した値を渡します。

ISODate型はUNIXTIMEのミリ秒なので、60(秒)*60(分)*9(時間)*1000(ミリ秒)を足しています。

ついでに$sortも指定。

# クエリ
col.aggregate(
    [
    {$project:
        {
            _id: 0,
            uid: 1,
            device: 1,
            created_at: 
            { 
                $toDate: 
                {
                    $concat: ["$created_at", "+0900"]
                }
            }
        }
    },
    {$group:
        {
            _id:
            {
                uid: "$uid",
                device: "$device",
                created_at:
                {
                    $dateToString:
                    {
                        "format" : "%Y-%m-%d",
                        "date": 
                        {
                            $add: ["$created_at", 60 * 60 * 9 * 1000]
                        }
                    }
                }
            }
        }
    },
    {$sort:
        {
            "_id.created_at": 1, "_id.uid": 1
        }
    }
    ]);

# 結果
{ "_id" : { "uid" : "fuga", "device" : "yyy", "created_at" : "2019-02-01" } }
{ "_id" : { "uid" : "hoge", "device" : "yyy", "created_at" : "2019-02-01" } }
{ "_id" : { "uid" : "hoge", "device" : "xxx", "created_at" : "2019-02-01" } }
...

これで目的の値が取れました!

まとめ

かなりまどろっこしい処理ですが、Pipelineの途中で日付を使った$matchなどの処理が入った時に対応できなくなりそうなので、面倒ですが対応してみました。
とはいえ、可能ならばデータを格納する段階で文字列ではなくISODate型にしておくのが無難だと思います。

参考