Python MongoEngineでEmbeddedDocumentListFieldの中から日付の範囲を指定して抽出する

MongoEngineで指定した日付の範囲内のドキュメントを取り出す。

コレクションSomeCollectionは以下のようになっています。 なお、MongoEngineを使っているので、Python上ではSomeDocumentクラスでコレクションを定義しています。

{
    "_id": ObjectID(...),
    "name": "abc1",
    "days": [
        {
            "day": ISODate("2017-09-30T00:00:00Z"),
            ...  // Other fields..
        },
        {
            "day": ISODate("2017-10-01T00:00:00Z"),
            ...  // Other fields..
        },
        {
            "day": ISODate("2017-10-02T00:00:00Z")
            ...  // Other fields..
        },
        ...
        ...  // More documents here
        ...
    ]
}

ここから、該当する日付のドキュメントを取り出す場合を考えました。 例えば、2017年11月のデータを取り出した時は次のようになります。

[
    {
        "day": ISODate("2017-10-01T00:00:00Z"),
        ...  // Other fields..
    },
    {
        "day": ISODate("2017-10-02T00:00:00Z")
        ...  // Other fields..
    },
    ...
    ...  // More November 2017 documents here
    ...
]

結構苦労して探した結果、3. API Reference — MongoEngine 0.15.0 documentationAggregation Pipeline — MongoDB Manual 3.4によると、 次のようにSomeDocument.objects.aggregate(*pipeline, **kwargs)を使うとよいです。パイプラインの作成が大事です。

実例の前にPythonで上の定義を載せておきます。 MongoEngineは、flask-mongoengine==0.9.3から利用しています。

# MongoEngineインスタンスを取得
from app import db

# ドキュメントの定義
class Day(db.EmbeddedDocument):
    day = db.DateTimeField(required=True)
    # ... other fileds here

class SomeDocument(db.Document):
    name = db.StringField(max_length=255, required=True)
    days = db.EmbeddedDocumentListField('Day')

必要なところで、抽出を行います。

import datetime as dt

# 抽出する日付の始端と終端。ここでは2017年10月分
start_day = dt.datetime(2017, 10, 1)
end_day = dt.datetime(2017, 11, 1)

# パイプラインを作成
pipeline = [
    {'$unwind': '$days'},
    {'$match': { 'days.day': {'$gte': start_day,
                              '$lt': end_day}
                }},
    # $projectで抽出するフィールド名と作成されるフィールド名を指定できる
    {'$project': {"day": "$days.day",
                  "somefield": "$days.somefield",
                 }},
]

# カーソルオブジェクトが返ってくる
cursor = SomeDocument.objects.aggregate(*pipeline)
for doc in cursor:
    do_something(doc)
# リストが欲しいなら作る
doclist = [doc for doc in cursor]

$gteが、>=のはずなのに10/2からのデータになってしまう問題があります。 これは、"$gt": dt.datetime(2017, 9, 30)に取り替えれば回避できそうです。 原因ははっきりしませんが、Pythondatetimeクラスのタイムゾーンとかnativeとかawareあたりにありそうです。

$unwindは、$unwind (aggregation) — MongoDB Manual 3.4を、 $matchなどについては、$match (aggregation) — MongoDB Manual 3.4が、参考になります。

MongoDBDateFieldがあればいいのにと思いました。

タイトルとURLをコピーしました