Получить только запрошенный элемент в массиве объектов в коллекции MongoDB

304

Предположим, что в моей коллекции есть следующие документы:

{  
   "_id":ObjectId("562e7c594c12942f08fe4192"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"blue"
      },
      {  
         "shape":"circle",
         "color":"red"
      }
   ]
},
{  
   "_id":ObjectId("562e7c594c12942f08fe4193"),
   "shapes":[  
      {  
         "shape":"square",
         "color":"black"
      },
      {  
         "shape":"circle",
         "color":"green"
      }
   ]
}

Сделайте запрос:

db.test.find({"shapes.color": "red"}, {"shapes.color": 1})

или

db.test.find({shapes: {"$elemMatch": {color: "red"}}}, {"shapes.color": 1})

Возвращает согласованный документ (документ 1), но всегда со всеми элементами массива в shapes:

{ "shapes": 
  [
    {"shape": "square", "color": "blue"},
    {"shape": "circle", "color": "red"}
  ] 
}

Однако я хотел бы получить документ (Документ 1) только с массивом, содержащим color=red:

{ "shapes": 
  [
    {"shape": "circle", "color": "red"}
  ] 
}

Как я могу это сделать?

Теги:
mongodb-query
aggregation-framework

11 ответов

311
Лучший ответ

MongoDB 2.2 новый $elemMatch проецирования $elemMatch предоставляет другой способ изменить возвращенный документ, содержащий только первый элемент согласованных shapes:

db.test.find(
    {"shapes.color": "red"}, 
    {_id: 0, shapes: {$elemMatch: {color: "red"}}});

Возвращает:

{"shapes" : [{"shape": "circle", "color": "red"}]}

В 2.2 вы также можете сделать это, используя $ projection operator, где $ в имени объекта проекционного объекта представляет собой индекс элемента первого совпадающего массива из запроса. Следующее возвращает те же результаты, что и выше:

db.test.find({"shapes.color": "red"}, {_id: 0, 'shapes.$': 1});

Обновление MongoDB 3.2

Начиная с версии 3.2, вы можете использовать новый $filter агрегации $filter для фильтрации массива во время проекции, который имеет преимущество, включая все совпадения, а не только первый.

db.test.aggregate([
    // Get just the docs that contain a shapes element where color is 'red'
    {$match: {'shapes.color': 'red'}},
    {$project: {
        shapes: {$filter: {
            input: '$shapes',
            as: 'shape',
            cond: {$eq: ['$$shape.color', 'red']}
        }},
        _id: 0
    }}
])

Результаты:

[ 
    {
        "shapes" : [ 
            {
                "shape" : "circle",
                "color" : "red"
            }
        ]
    }
]
  • 59
    Обратите внимание, что, как указано в документации, $elemMatch и $ возвращают только первое совпадение .
  • 12
    любое решение, если я хочу, чтобы он возвращал все элементы, которые соответствуют ему, а не только первый?
Показать ещё 9 комментариев
92

Новая Агрегирующая структура в MongoDB 2.2+ предоставляет альтернативу Map/Reduce. Оператор $unwind может использоваться для разделения вашего массива shapes на поток документов, который можно сопоставить:

db.test.aggregate(
  // Start with a $match pipeline which can take advantage of an index and limit documents processed
  { $match : {
     "shapes.color": "red"
  }},
  { $unwind : "$shapes" },
  { $match : {
     "shapes.color": "red"
  }}
)

Результаты в:

{
    "result" : [
        {
            "_id" : ObjectId("504425059b7c9fa7ec92beec"),
            "shapes" : {
                "shape" : "circle",
                "color" : "red"
            }
        }
    ],
    "ok" : 1
}
  • 6
    @JohnnyHK: в этом случае $elemMatch - еще один вариант. На самом деле я попал сюда через вопрос группы Google, где $ elemMatch не будет работать, потому что он возвращает только первое совпадение для каждого документа.
  • 1
    Спасибо, я не знал об этом ограничении, так что это полезно знать. Извините за удаление моего комментария, на который вы отвечаете, я решил вместо этого опубликовать другой ответ и не хотел вводить людей в заблуждение.
Показать ещё 6 комментариев
28

Предостережение: Этот ответ представляет собой решение, которое было актуальным в то время, до того, как были введены новые возможности MongoDB 2.2 и выше. См. Другие ответы, если вы используете более новую версию MongoDB.

Параметр выбора поля ограничен полными свойствами. Он не может использоваться для выбора части массива, только для всего массива. Я попытался использовать $positional operator, но это не сработало.

Самый простой способ - просто отфильтровать фигуры в клиенте.

Если вам действительно нужен правильный вывод непосредственно из MongoDB, вы можете использовать сокращение карты для фильтрации фигур.

function map() {
  filteredShapes = [];

  this.shapes.forEach(function (s) {
    if (s.color === "red") {
      filteredShapes.push(s);
    }
  });

  emit(this._id, { shapes: filteredShapes });
}

function reduce(key, values) {
  return values[0];
}

res = db.test.mapReduce(map, reduce, { query: { "shapes.color": "red" } })

db[res.result].find()
27

Другим интересным способом является $redact, который является одной из новых функций агрегации MongoDB 2.6. Если вы используете 2.6, вам не нужно развязывать $unwind, что может вызвать проблемы с производительностью, если у вас большие массивы.

db.test.aggregate([
    { $match: { 
         shapes: { $elemMatch: {color: "red"} } 
    }},
    { $redact : {
         $cond: {
             if: { $or : [{ $eq: ["$color","red"] }, { $not : "$color" }]},
             then: "$$DESCEND",
             else: "$$PRUNE"
         }
    }}]);

$redact "ограничивает содержимое документов на основе информации, хранящейся в самих документах". Таким образом, он будет работать только внутри документа. Он в основном сканирует верхнюю часть документа в нижней части и проверяет, соответствует ли это вашему условию if, которое находится в $cond, если есть совпадение, оно будет либо сохранять содержимое ($$DESCEND), либо удалять ($$PRUNE).

В приведенном выше примере первый $match возвращает весь массив shapes, а $redact привязывает его к ожидаемому результату.

Обратите внимание, что {$not:"$color"} необходимо, так как он также сканирует верхний документ, а если $redact не найдет поле color на верхнем уровне, это вернет false, что может лишить весь документ которые мы не хотим.

  • 1
    идеальный ответ. Как вы упоминали, $ unwind будет занимать много оперативной памяти. Так что это будет лучше, если сравнивать.
  • 0
    Я сомневаюсь. В этом примере «shape» - это массив. Будет ли "$ redact" сканировать все объекты в массиве "shape" ?? Как это будет хорошо в отношении производительности?
Показать ещё 4 комментария
16

Лучше вы можете запросить в соответствующем элементе массива с помощью $slice полезно ли возвращать значительный объект в массив.

db.test.find({"shapes.color" : "blue"}, {"shapes.$" : 1})

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

11
 db.getCollection('aj').find({"shapes.color":"red"},{"shapes.$":1})

ВЫХОДЫ

{

   "shapes" : [ 
       {
           "shape" : "circle",
           "color" : "red"
       }
   ]
}
11

Синтаксис поиска в mongodb равен

    db.<collection name>.find(query, projection);

и второй запрос, который вы написали, то есть

    db.test.find(
    {shapes: {"$elemMatch": {color: "red"}}}, 
    {"shapes.color":1})

в этом вы использовали оператор $elemMatch в части запроса, тогда как если вы используете этот оператор в проекционной части, вы получите желаемый результат. Вы можете записать свой запрос как

     db.users.find(
     {"shapes.color":"red"},
     {_id:0, shapes: {$elemMatch : {color: "red"}}})

Это даст вам желаемый результат.

  • 1
    Это работает для меня. Однако оказывается, что "shapes.color":"red" в параметре запроса (первый параметр метода find) не обязателен. Вы можете заменить его на {} и получить те же результаты.
  • 2
    @ErikOlson Ваше предложение верно в приведенном выше случае, когда нам нужно найти все документы, которые выделены красным цветом, и применить проекцию только к ним. Но скажем, если кому-то нужно выяснить все документы, которые имеют синий цвет, но он должен вернуть только те элементы этого массива фигур, которые имеют красный цвет. В этом случае на вышеуказанный запрос может ссылаться кто-то еще ..
Показать ещё 1 комментарий
7

Благодаря JohnnyHK.

Здесь я просто хочу добавить более сложное использование.

// Document 
{ 
"_id" : 1
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 

{ 
"_id" : 2
"shapes" : [
  {"shape" : "square",  "color" : "red"},
  {"shape" : "circle",  "color" : "green"}
  ] 
} 


// The Query   
db.contents.find({
    "_id" : ObjectId(1),
    "shapes.color":"red"
},{
    "_id": 0,
    "shapes" :{
       "$elemMatch":{
           "color" : "red"
       } 
    }
}) 


//And the Result

{"shapes":[
    {
       "shape" : "square",
       "color" : "red"
    }
]}
5

Вам просто нужно запустить запрос

db.test.find(
{"shapes.color": "red"}, 
{shapes: {$elemMatch: {color: "red"}}});

вывод этого запроса

{
    "_id" : ObjectId("562e7c594c12942f08fe4192"),
    "shapes" : [ 
        {"shape" : "circle", "color" : "red"}
    ]
}

как вы ожидали, это даст точное поле из массива, соответствующее цвету: "красный".

2

вместе с $project будет более подходящим, чтобы другие мудрые элементы соответствия были сгруппированы вместе с другими элементами документа.

db.test.aggregate(
  { "$unwind" : "$shapes" },
  { "$match" : {
     "shapes.color": "red"
  }},
{"$project":{
"_id":1,
"item":1
}}
)
  • 0
    Можете ли вы, пожалуйста, опишите, что это достигается с помощью набора ввода и вывода?
0
db.test.find( {"shapes.color": "red"}, {_id: 0})
  • 1
    Добро пожаловать в переполнение стека! Спасибо за фрагмент кода, который может оказать некоторую ограниченную, немедленную помощь. Правильное объяснение значительно улучшило бы его долгосрочную ценность , объяснив, почему это хорошее решение проблемы, и сделало бы его более полезным для будущих читателей с другими подобными вопросами. Пожалуйста, измените свой ответ, чтобы добавить некоторые объяснения, в том числе предположения, которые вы сделали.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню