Как найти документ и отдельные поддокументы, соответствующие заданным критериям в коллекции MongoDB

У меня есть коллекция продуктов. Каждый продукт содержит массив элементов.

> db.products.find().pretty()
{
    "_id" : ObjectId("54023e8bcef998273f36041d"),
    "shop" : "shop1",
    "name" : "product1",
    "items" : [
            {
                    "date" : "01.02.2100",
                    "purchasePrice" : 1,
                    "sellingPrice" : 10,
                    "count" : 15
            },
            {
                    "date" : "31.08.2014",
                    "purchasePrice" : 10,
                    "sellingPrice" : 1,
                    "count" : 5
            }
    ]
}

Итак, можете ли вы дать мне совет, как я могу запросить MongoDB для извлечения всех продуктов только с одним элементом, дата которого равна дате, которую я передаю в качестве параметра как параметр.

Результат для "31.08.2014" должен быть:

    {
    "_id" : ObjectId("54023e8bcef998273f36041d"),
    "shop" : "shop1",
    "name" : "product1",
    "items" : [
            {
                    "date" : "31.08.2014",
                    "purchasePrice" : 10,
                    "sellingPrice" : 1,
                    "count" : 5
            }
    ]
}

Ответ 1

То, что вы ищете, это позиционный оператор $ и "проекция". Для одного поля вам нужно сопоставить требуемый элемент массива с помощью "точечной нотации", для более чем одного поля используйте $elemMatch:

db.products.find(
    { "items.date": "31.08.2014" },
    { "shop": 1, "name":1, "items.$": 1 }
)

Или $elemMatch для более чем одного совпадающего поля:

db.products.find(
    { "items":  { 
        "$elemMatch": { "date": "31.08.2014",  "purchasePrice": 1 }
    }},
    { "shop": 1, "name":1, "items.$": 1 }
)

Они работают только для одного элемента массива и только один будет возвращен. Если вы хотите, чтобы из ваших условий было возвращено более одного элемента массива, вам потребуется более сложная обработка с помощью структуры агрегации.

db.products.aggregate([
    { "$match": { "items.date": "31.08.2014" } },
    { "$unwind": "$items" },
    { "$match": { "items.date": "31.08.2014" } },
    { "$group": {
        "_id": "$_id",
        "shop": { "$first": "$shop" },
        "name": { "$first": "$name" },
        "items": { "$push": "$items" }
    }}
])

Или, возможно, в более короткой/быстрой форме, начиная с MongoDB 2.6, где ваш массив элементов содержит уникальные записи:

db.products.aggregate([
    { "$match": { "items.date": "31.08.2014" } },
    { "$project": {
        "shop": 1,
        "name": 1,
        "items": {
            "$setDifference": [
                { "$map": {
                    "input": "$items",
                    "as": "el",
                    "in": {
                        "$cond": [
                            { "$eq": [ "$$el.date", "31.08.2014" ] },
                            "$$el",
                            false 
                        ]
                    }
                }},
                [false]
            ]
        }
    }}
])

Или, возможно, с $redact, но немного надуманным:

db.products.aggregate([
    { "$match": { "items.date": "31.08.2014" } },
    { "$redact": {
        "$cond": [
             { "$eq": [ { "$ifNull": [ "$date", "31.08.2014" ] }, "31.08.2014" ] },
             "$$DESCEND",
             "$$PRUNE"
         ]
    }}
])

Более современный, вы бы использовали $filter:

db.products.aggregate([
  { "$match": { "items.date": "31.08.2014" } },
  { "$addFields": {
    "items": {
      "input": "$items",
      "cond": { "$eq": [ "$$this.date", "31.08.2014" ] }
    }
  }}
])

И с несколькими условиями, $elemMatch и $and внутри $filter:

db.products.aggregate([
  { "$match": { 
    "$elemMatch": { "date": "31.08.2014",  "purchasePrice": 1 }
  }},
  { "$addFields": {
    "items": {
      "input": "$items",
      "cond": { 
        "$and": [
          { "$eq": [ "$$this.date", "31.08.2014" ] },
          { "$eq": [ "$$this.purchasePrice", 1 ] }
        ]
      }
    }
  }}
])

Так что это зависит только от того, ожидаете ли вы, что один элемент совпадет с несколькими, и какой подход лучше. Но там, где это возможно, метод .find(), как правило, будет быстрее, поскольку в нем отсутствуют издержки других операций, которые в последних формах не сильно отстают.

Как примечание, ваши "даты" представлены в виде строк, что не очень хорошая идея в будущем. Подумайте о том, чтобы изменить их на надлежащие типы объектов Date, что очень поможет вам в будущем.

Ответ 2

Mongo поддерживает точечную нотацию для подзапросов.

Смотрите: http://docs.mongodb.org/manual/reference/glossary/#term-dot-notation

В зависимости от вашего драйвера вы хотите что-то вроде:

db.products.find({"items.date":"31.08.2014"});

Обратите внимание, что атрибут находится в кавычках для точечной нотации, даже если обычно ваш драйвер не требует этого.