としたにあんの左脳

備忘録です.

mongoのISODateのtimezone問題に対処する

mongodbのISODateはtimezoneに対応してないらしい. いろいろ検証してみて,最後はAggregation Frameworkでどうにかしてみようと思う.

準備

以下のスクリプトを動かすとサンプルデータがmongodbに入る.

データの形式は以下の通り.

日本時間で2013/01/01 00:00:00 から 2013/01/02 23:59:00 の1分おきのデータが入る.

2013/01/01 は火曜日 2013/01/02は水曜日

{
        "_id" : ObjectId("52af0bc3090fdd09be0c6ff5"),
        "gmt_time" : ISODate("2012-12-31T15:00:00Z"),
        "gender" : "female",
        "jst_strf" : "2013/01/01 00:00:00"
}

Timezone問題

mongodbのISODateは現在のところ,GMTにしか対応していない.

先ほど挿入したデータも,タイムゾーン付きでdatetimeを生成したので,jst_strfは文字列なので日本時間で挿入されている(2013/01/01 00:00:00 ~ 2013/01/02 23:59:00)

一方で,gmt_timeはTimezone情報が捨てられてしまうのでGMTで挿入されている.(2012-12-31T15:00:00 ~ 2013-01-02T14:59:00)

このコレクションに対して,クエリをかけるときに問題が発生する.

例えば,日本時間の1月1日のデータだけを取り出したいときなどである.

以下のクエリをかけると,当然,GMTに対してクエリがかかるので,日本時間では,2013/01/01 09:01:00 ~ 2013/01/02 08:59:00の結果がかえってくる.

> db.gender.find({gmt_time:{$gt:ISODate("2013-01-01T00:00:00Z"), $lt:ISODate("2013-01-01T23:59:59Z")}})
{ "_id" : ObjectId("52af0bc4090fdd09be0c7212"), "gmt_time" : ISODate("2013-01-01T00:01:00Z"), "gender" : "female", "jst_strf" : "2013/01/01 09:01:00" }
...
{ "_id" : ObjectId("52af0bc4090fdd09be0c77b0"), "gmt_time" : ISODate("2013-01-01T23:59:00Z"), "gender" : "male", "jst_strf" : "2013/01/02 08:59:00" }

そこで,次に考えるのは,クエリに使う時刻に,予め9時間のオフセットを引いておくことだ.

> db.gender.find({gmt_time:{$gt:ISODate("2012-12-31T15:00:00Z"), $lt:ISODate("2013-01-01T14:59:59Z")}})
{ "_id" : ObjectId("52af0bc4090fdd09be0c6ff6"), "gmt_time" : ISODate("2012-12-31T15:01:00Z"), "gender" : "male", "jst_strf" : "2013/01/01 00:01:00" }
...
{ "_id" : ObjectId("52af0bc4090fdd09be0c7594"), "gmt_time" : ISODate("2013-01-01T14:59:00Z"), "gender" : "male", "jst_strf" : "2013/01/01 23:59:00" }

これで,本来取り出したかった2013/01/01 00:01:00 ~ 2013/01/01 23:59:00 のデータが取り出せる.

しかし,何かイケてない

いろいろと使いまわせない感じかする.

後述するアグリゲーションフレームワークを利用した場合,曜日でのクエリをかけることができるのだが,上記の方法だとTimezone問題に対応できない.

aggregation framework

mongodbにはaggregation frameworkというものが存在する.

Aggregations operations process data records and return computed results. Aggregation operations group values from multiple documents together, and can perform a variety of operations on the grouped data to return a single result.

簡単に言うと,レコードに対して処理を行い,計算結果のみを返してくれものらしい.

> db.gender.aggregate({$match:{gender:'male'}})
{
        "result" : [
                {
                        "_id" : ObjectId("52af0bc4090fdd09be0c6ff6"),
                        "gmt_time" : ISODate("2012-12-31T15:01:00Z"),
                        "gender" : "male",
                        "jst_strf" : "2013/01/01 00:01:00"
                },
                 ...
        ],
        "ok" : 1
}

gender='male'にmatchするものを取り出すというアグリゲーションフレームワークのクエリをかけると,result というリストに結果のドキュメントが入ってくる.

ソートをする場合は

>db.gender.aggregate({$match:{gender:'male'}}, {$sort:{gmt_time:1}})

リミットをかける場合は

> db.gender.aggregate({$match:{gender:'male'}}, {$sort:{gmt_time:1}}, {$limit:1})

ISODataに対するAggregate

Aggregation Framework Operators - Date Operators

上記のリンクにあるように,Aggregation Frameworkを利用することで,ISODateから,年や月,曜日などを数値として取り出すことができる.

> db.gender.aggregate({$project:{weekday:{$dayOfWeek:"$gmt_time"}}})
{
        "result" : [
                {
                        "_id" : ObjectId("52af0bc4090fdd09be0c7423"),
                        "weekday" : 3
                },
                 ....
                {
                        "_id" : ObjectId("52af0bc5090fdd09be0c7b34"),
                        "weekday" : 4
                }
                 ...
        ],
        "ok" : 1
}

このように,gmt_timeから曜日のみを取り出すことが可能だ.

Aggregation Frameworkは処理をつなげることができる(パイプライン処理と呼ばれている)

以下の処理は,曜日が火曜日のドキュメント群のみを取り出す処理である.

> db.gender.aggregate(
                   {$project:{jst_strf:"$jst_strf",weekday:{$dayOfWeek:"$gmt_time"}}},
                   {$match: {weekday:{$gte:3, $lt:4}}}
)

{
        "result" : [
                {
                        "_id" : ObjectId("52af0bc4090fdd09be0c7211"),
                        "jst_strf" : "2013/01/01 09:00:00",
                        "weekday" : 3
                },
                ...
                {
                        "_id" : ObjectId("52af0bc4090fdd09be0c77b0"),
                        "jst_strf" : "2013/01/02 08:59:00",
                        "weekday" : 3
                }
         ],
         "ok":1
}

これは,

  1. jst_strfweekdayのみをフィールドにもつドキュメント群をつくる.($project)

  2. weekdayが3以上4未満のドキュメント群をつくる($match)

を連続して行っている.

この結果では火曜日のドキュメント群を取り出したつもりなのに,1/1と1/2のドキュメントが混じっている.

ここで,思い出してほしいのは,2013/01/02は火曜日だった.一方,weekdayが3と言うのは火曜日であることを表す.(日=1, 月=2, 火=3,...)

ズレが生じてしまっている.これはweekdayがgmt_dateから計算されているためである.(このドキュメントのISODateは2012-12-31T15:00:00であるため)

本当は月曜日のドキュメントがほしいのに火曜日のドキュメントがとれてしまっている.

対処法

対処法として,以下のリンクを教えてもらった. Timezone support in date operators at query time How to agregate by year-month-day on a different timezone

要は,最初にISODateにTimezone分だけ時間を加算してから,残りの処理を行えばいいらしい.

> db.gender.aggregate(
                          {$project: {jst_strf:"$jst_strf", gmt_time:{$add:["$gmt_time", 9 * 60 * 60 * 1000]}}},
                          {$project:{jst_strf:"$jst_strf",weekday:{$dayOfWeek:"$gmt_time"}}}, 
                          {$match: {weekday:{$gte:3, $lt:4}}}
)

{
        "result" : [
                {
                        "_id" : ObjectId("52af0bc3090fdd09be0c6ff5"),
                        "jst_strf" : "2013/01/01 00:00:00",
                        "weekday" : 3
                },
                ...
                {
                        "_id" : ObjectId("52af0bc4090fdd09be0c7594"),
                        "jst_strf" : "2013/01/01 23:59:00",
                        "weekday" : 3
                }
         ],
         "ok":1
}

これで正しくとれた!

{$project: {jst_strf:"$jst_strf", gmt_time:{$add:["$gmt_time", 9 * 60 * 60 * 1000]}}},

このように,あらかじめISODateを日本の時差の分だけずらしたあとにクエリをかけて上げればよい.

まとめ

mongoのtimezoneで困ったらAggregation Frameworkを使いましょう.

次回以降,mongoのC++ DriverでAggregation Frameworkを触ることについて書いてみようかなと思います.