Перейти к содержанию

Запросы для определенных типов

MongoDB имеет широкий спектр типов, которые можно использовать в документе. Некоторые из этих типов ведут себя особым образом при выполнении запросов.

null

Тип null ведет себя немного странно. Он соответствует самому себе, поэтому если у нас есть коллекция со следующими документами:

1
> db.c.find()
1
2
3
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

мы можем запросить документы, у которых ключ y равен нулю, ожидаемым образом:

1
> db.c.find({"y" : null})
1
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }

Однако null также соответствует значению «не существует». Таким образом, запрос ключа со значением null вернет все документы, в которых этот ключ отсутствует:

1
> db.c.find({«z» : null})
1
2
3
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

Если мы хотим найти только ключи со значением null, можно проверить, что ключ имеет значение null и что он существует, используя условный оператор $exists:

1
> db.c.find({"z" : {"$eq" : null, "$exists" : true}})

Регулярные выражения

Оператор $regex предоставляет возможности регулярных выражений для сопоставления с образцом в запросах. Регулярные выражения полезны для гибкого сопоставления строк. Например, если нам нужно найти всех пользователей с именем «Joe» или «joe», можно использовать регулярное выражение для нечувствительного к регистру сопоставления:

1
> db.users.find( {"name" : {"$regex" : /joe/i}})

Флаги регулярных выражений (например, i) допустимы, но не обязательны. Если мы хотим сопоставить не только различные варианты написания имени «joe», но и «joey», можно продолжить улучшать наше регулярное выражение:

1
> db.users.find({"name": /joey?/i})

MongoDB использует библиотеку PCRE (Perl Compatible Regular Expressions), которая реализует работу регулярных выражений в стиле Perl; любой синтаксис регулярных выражений, разрешенный PCRE, разрешен в MongoDB. Рекомендуется проверить синтаксис в оболочке JavaScript, прежде чем использовать его в запросе, чтобы убедиться, что он совпадает с тем, с чем, по вашему мнению, он должен совпадать.

MongoDB может использовать индекс для запросов, когда речь идет о префиксных регулярных выражениях (например, /^joey/). Индексы нельзя использовать для нечувствительного к регистру поиска (/^joey/i). Регулярное выражение является «префиксным выражением», когда оно начинается со знака каретки (^) или наклонной черты (\A). Если регулярное выражение использует регистрозависимый запрос, тогда, если для поля существует индекс, сопоставления могут проводиться со значениями в индексе. Если оно также является префиксным выражением, поиск может быть ограничен значениями в диапазоне, созданном этим префиксом из индекса.

Регулярные выражения также могут совпадать друг с другом. Мало кто вставляет регулярные выражения в базу данных, но если вы вставите одно из них, можно сопоставить его с самим собой:

1
2
> db.foo.insertOne({"bar" : /baz/})
> db.foo.find({"bar" : /baz/})
1
2
3
4
{
"_id" : ObjectId("4b23c3ca7525f35f94b60a2d"),
"bar" : /baz/
}

Запросы элементов массива

Запросы элементов массива разработаны таким образом, чтобы вести себя так же, как запросы скаляров. Например, если массив представляет собой список фруктов:

1
> db.food.insertOne({"fruit": ["apple", "banana", "peach"]})

приведенный ниже запрос будет успешно совпадать с документом:

1
> db.food.find({"fruit" : "banana"})

Мы можем запросить его практически так же, как если бы у нас был документ, который выглядел бы как (недопустимый) документ {"fruit" : "apple", "fruit": "banana", "fruit": "peach"}.

$all

Если вам нужно сопоставить массивы по нескольким элементам, можно использовать оператор $all. Он позволяет сопоставить список элементов. Например, предположим, что мы создаем коллекцию из трех элементов:

1
2
3
> db.food.insertOne({"_ id" : 1, "fruit": ["apple", "banana", "peach"]})
> db.food.insertOne({"_ id" : 2, "fruit": ["apple", "kumquat", "orange"]})
> db.food.insertOne({"_ id" : 3, "fruit": ["cherry", "banana", "apple"]})

Затем мы можем найти все документы с элементами apple и banana, выполнив запрос с помощью оператора $all:

1
> db.food.find({fruit : {$ all : ["apple", "banana"]}})
1
2
{"_id" : 1, "fruit" : ["apple", "banana", "peach"]}
{"_id" : 3, "fruit" : ["cherry", "banana", "apple"]}

Порядок не имеет значения. Обратите внимание, что banana стоит перед apple во втором результате. Использование одноэлементного массива с $all эквивалентно неиспользованию $all. Например, {fruit : {$ all: ['apple']} будет соответствовать тем же документам, что и {fruit : 'apple'}. Вы также можете выполнить запрос по точному совпадению, используя весь массив. Однако точное совпадение не будет соответствовать документу, если какие-либо элементы отсутствуют или являются лишними. Например, этот запрос будет соответствовать первому из трех наших документов:

1
> db.food.find({"fruit" : ["apple", "banana", "peach"]})

А этот не будет:

1
> db.food.find({"fruit" : ["apple", "banana"]})

Равно как и этот:

1
> db.food.find({"fruit" : ["banana", "apple", "peach"]})

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

1
> db.food.find({"fruit.2" : "peach"})

В массивах индексация всегда начинается с 0, поэтому третий элемент массива сопоставляется со строкой peach.

$size

Полезным условным оператором для запроса массивов является оператор $size, который позволяет запрашивать массивы заданного размера. Пример:

1
> db.food.find({"fruit" : {"$ size": 3}})

Один из распространенных запросов – получение диапазона размеров. Оператор $size нельзя объединить с другим условным оператором (в данном примере с $gt), но этот запрос можно выполнить, добавив в документ ключ size. Затем каждый раз, когда вы добавляете элемент в массив, увеличивайте значение size. Если исходное обновление выглядело так:

1
> db.food.update(criteria, {"$push" : {"fruit" : "strawberry"}})

можно просто поменять его на это:

1
2
3
4
> db.food.update(
    criteria,
    {"$push" : {"fruit" : "strawberry"}, "$inc" : {"size" : 1}}
)

Прирост идет очень быстро, поэтому любое снижение производительности незначительно. Хранение документов, подобных этому, позволяет вам выполнять такие запросы:

1
> db.food.find({"size": {"$ gt": 3}})

К сожалению, данный метод не работает с оператором $addToSet.

$slice

Как упоминалось ранее в этой главе, второй необязательный аргумент для метода find определяет ключи, которые должны быть возвращены. Можно использовать специальный оператор $slice, чтобы вернуть подмножество элементов для ключа массива.

Например, предположим, что у нас был документ поста в блоге, и нам нужно вернуть первые 10 комментариев:

1
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : 10}})

Также, если бы нам были нужны последние 10 комментариев, мы могли бы использовать −10:

1
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -10}})

Оператор $slice еще может возвращать страницы посреди результатов, принимая смещение и число возвращаемых элементов:

1
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : [23, 10]}})

Первые 23 элемента будут пропущены, и вернутся элементы с 24-го по 33-й. Если бы в массиве было меньше 33 элементов, он вернул бы как можно больше элементов.

Если не указано иное, при использовании оператора $slice возвращаются все ключи в документе. Это отличается от других спецификаторов ключей, которые подавляют возврат неупомянутых ключей. Например, если бы у нас был документ поста блога следующего вида:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "joe",
"email" : "[email protected]",
"content" : "nice post."
},
{
"name" : "bob",
"email" : "[email protected]",
"content" : "good post."
}
]
}

и мы бы использовали оператор $slice, чтобы получить последний комментарий, вот что бы вышло:

1
> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "bob",
"email" : "[email protected]",
"content" : "good post."
}
]
}

И title, и content по-прежнему возвращаются, хотя они не были явно включены в спецификатор ключа.

Возврат совпадающего элемента массива

Оператор "$slice" полезен, когда вы знаете индекс элемента, но иногда вам нужно, чтобы любой элемент массива совпадал с вашими критериями. Совпадающий элемент можно вернуть с помощью оператора $. Учитывая предыдущий пример с блогом, можно получить комментарий Боба с помощью этого запроса:

1
> db.blog.posts.find({"comments.name" : "bob"}, {"comments.$" : 1})
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"comments" : [
{
"name" : "bob",
"email" : "[email protected]",
"content" : "good post."
}
]
}

Обратите внимание на то, что для каждого документа возвращается только первое совпадение: если бы Боб оставил несколько комментариев к этому посту, был бы возвращен только первый комментарий в массиве comments.

Взаимодействия запросов по диапазону и запросов к массиву

Скаляры (элементы, не являющиеся элементами массива) в документах должны совпадать с каждым предложением критериев запроса.

Например, если вы запросили {"x": {"$gt": 10, "$lt": 20}}, x должно быть больше 10 и меньше 20. Однако если поле документа x является массивом, документ совпадает, если существует элемент x, который совпадает с каждой частью критерия, но каждое предложение запроса может совпадать с другим элементом массива.

Лучший способ понять это поведение – посмотреть пример. Предположим, у нас есть следующие документы:

1
2
3
4
{"x" : 5}
{"x" : 15}
{"x" : 25}
{"x" : [5, 25]}

Если бы мы хотели найти все документы, где x находится между 10 и 20, то могли бы наивно структурировать запрос в виде db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}), ожидая получить один документ: {"x" : 15}. Однако, выполнив этот запрос, мы получим два документа:

1
> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}})
1
2
{"x" : 15}
{"x" : [5, 25]}

Ни 5, ни 25 не находятся между 10 и 20, но документ возвращается, потому что 25 совпадает с первым предложением (это больше 10), а 5 совпадает со вторым (это меньше 20).

Таким образом запросы по диапазону к массивам становятся практически бесполезными: диапазон будет совпадать с любым многоэлементным массивом. Есть несколько способов получить ожидаемое поведение. Во-первых, можно использовать оператор $elemMatch, чтобы заставить MongoDB сравнивать оба предложения с одним элементом массива. Однако подвох заключается в том, что этот оператор не будет соответствовать элементам без массивов:

1
2
> db.test.find({"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}}})
> // Никаких результатов;

Документ {"x" : 15} больше не совпадает с запросом, поскольку поле x не является массивом. Тем не менее у вас должна быть веская причина для смешивания массивов и скалярных значений в поле. Во многих случаях использования смешивания не требуется. Для них оператор $elemMatch предлагает хорошее решение для запросов по диапазону к элементам массива.

Если у вас есть индекс по полю, по которому вы выполняете запрос, можно использовать min и max, чтобы ограничить диапазон индекса, пройденного запросом, значениями $gt и $lt:

1
> db.test.find({"x" : {"$gt": 10, "$lt" : 20}}).min ({"x" : 10}).max ({"x" : 20} )
1
{"x" : 15}

Теперь мы проходим индекс только с 10 до 20, пропуская записи 5 и 25. min и max можно использовать только тогда, когда у вас есть индекс по полю, по которому вы выполняете запрос, и вы должны передать min и max все поля индекса.

Использование min и max при запросе по диапазону для документов, которые могут включать в себя массивы, – как правило, неплохая идея. Индексные границы для запроса $gt/$lt по массиву неэффективны. В основном он принимает любое значение, поэтому будет искать все записи индекса, а не только те, которые находятся в диапазоне.

Запросы по вложенным документам

Существует два способа запроса по вложенному документу: запросить весь документ или запросить его отдельные пары типа «ключ/значение».

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

1
2
3
4
5
6
7
{
  "name": {
    "first": "Joe",
    "last": "Schmoe"
  },
  "age": 45
}

мы можем выполнить запрос, чтобы найти человека по имени Джо Шмо:

1
> db.people.find ({"name" : {"first" : "Joe", "last" : "Schmoe"}})

Однако запрос всего вложенного документа должен точно совпадать с ним. Если Джо решит добавить поле для указания среднего имени, этот запрос больше не будет работать; он не совпадает с документом! Данный тип запроса также чувствителен к порядку: {"last" : "Schmoe", "first" : "Joe"} не будет совпадением.

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

1
> db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})

Теперь если Джо добавит дополнительные ключи, этот запрос все равно будет соответствовать его имени и фамилии.

Эта точечная нотация является основным отличием между документами запроса и документами других типов. Документы запроса могут содержать точки, которые означают «добраться до вложенного документа». Точечная нотация также является причиной того, что документы, которые нужно вставить, не могут содержать символ «.». Часто люди сталкиваются с этим ограничением при попытке сохранить URL-адреса в качестве ключей. Один из способов обойти эту проблему – всегда выполнять глобальную замену перед вставкой или после извлечения, заменяя символ, который не разрешен в URL-адресах, символом точки.

Сопоставление встроенных документов может стать немного сложнее, поскольку структура документа усложняется. Например, предположим, что мы сохраняем посты в блоге и хотим найти комментарии Джо, которые были оценены как минимум в 5 баллов. Можно смоделировать этот пост следующим образом:

1
> db.blog.find()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "content": "...",
  "comments": [
    {
      "author": "joe",
      "score": 3,
      "comment": "nice post"
    },
    {
      "author": "mary",
      "score": 6,
      "comment": "terrible post"
    }
  ]
}

Теперь мы не можем делать запросы, используя db.blog.find({"comments" : {"author" : "joe", "Score" : {"$ gte" : 5}}}). Соответствия встроенного документа должны совпадать со всем документом, а это не совпадает с ключом comment. db.blog.find({"comments.author" : "joe", "comments.score": {"$gte" : 5}}) также не сработает, поскольку критерий автора может совпадать с другим комментарием, а не с критерием оценки. То есть в результате запроса мы бы получили документ, показанный выше: он соответствовал бы "author" : "joe" в первом комментарии и "score" : 6 во втором комментарии.

Чтобы правильно сгруппировать критерии без указания каждого ключа, используйте оператор $elemMatch. Этот условный оператор со смутным названием позволяет вам частично указать критерии для совпадения с одним вложенным документом в массиве. Правильный запрос выглядит так:

1
2
3
4
5
6
7
> db.blog.find(
    {"comments" : {
        "$elemMatch" : {
            "author" : "joe", "score" : {"$gte" : 5}
        }
    }}
)

$elemMatch позволяет вам «группировать» свои критерии. Таким образом, он необходим только тогда, когда у вас есть несколько ключей, которым вам нужно совпадение во вложенном документе.

Комментарии