Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~20 мин +50 XP

Фильтры: $eq, $gt, $in и bson.M

Фильтры — язык вопросов к mongo, и в Go этот язык записывается обычным bson-документом: от bson.m с парой условий до составных диапазонов с $gt и $in. Разберём неявный AND, операторы сравнения, $or, $exists и ту ловушку, из-за которой собранный в цикле фильтр молча теряет половину условий. Один и тот же словарь условий работает и в Find, и в обновлениях, и в удалениях.

Содержание урока · 10 разделов

До сих пор фильтры выглядели игрушечно: bson.M{"sku": "kb-87-tkl"} — найди по ключу. Настоящие страницы каталога спрашивают иначе: товары в наличии дешевле пяти тысяч, заказы в статусах «оплачен» и «отправлен», пользователи, у которых вообще не заполнен телефон. Всё это — условия, и MongoDB выражает их тем же материалом, что и данные: фильтр — это BSON-документ, в котором ключи называют поля, а значения задают требования. Никакого отдельного языка запросов в Go нет: вы собираете документ, драйвер кодирует его в BSON, сервер сравнивает с каждым документом коллекции. Отсюда и сила подхода, и его ловушки. Сила: один и тот же словарь условий принимают Find, FindOne, UpdateOne, UpdateMany, DeleteOne, DeleteMany — выучив фильтры один раз, вы владеете половиной CRUD. Ловушка: документ — это данные, и компилятор Go почти ничего в нём не проверяет; опечатка в операторе или потерянное условие не сломают сборку, а просто изменят смысл запроса. Поэтому в этом уроке мы пройдём от простых сравнений до составных условий и разберём места, где фильтр тихо означает не то, что вы имели в виду. Проверяйте примеры на живом контейнере: песочница сайта показывает тематическую программу на чистом Go, а mongosh с db.collection.find() покажет те же фильтры глазами сервера.

Фильтры mongodb в Go: документ как условие#

Начнём с правила, которое снимает половину вопросов: пара "поле": значение в фильтре — это сравнение на равенство. bson.M{"status": "paid"} и развёрнутый bson.M{"status": bson.M{"$eq": "paid"}} — один и тот же запрос: $eq подразумевается и пишется явно только для симметрии или внутри составных условий. mongo фильтры go-драйвера принимают в любом виде документа: bson.M для набросков, bson.D там, где важен порядок, структуру с тегами, если условие собрано заранее. Сервер читает документ сверху: каждое поле фильтра — отдельное требование, и документ совпадает, только когда выполнены все требования сразу. Это неявный AND, и он покрывает большинство реальных запросов: статус и диапазон цены и наличие — три ключа в одном bson.M. Операторы сравнения вкладываются в значение поля: bson.M{"price": bson.M{"$gt": 1000}} читается как «price больше 1000». Вложенный документ может нести несколько операторов сразу — "$gte" и "$lte" рядом дадут диапазон с включёнными границами. И важная деталь про типы: сравнение в MongoDB строгое по классам BSON-типов — число 42 и строка "42" не равны никогда, зато все числовые типы сравниваются между собой как числа: int32, int64 и double в равенстве совместимы, и в сортировке сервер расставит их по значению, а не по разновидности. Если фильтр «не находит» документ, который глазами есть в коллекции, первым делом проверяйте тип значения в mongosh.

Три рабочих фильтра: равенство, диапазон, список

GO
coll := client.Database("shop").Collection("products")

// равенство: $eq подразумевается
f1 := bson.M{"in_stock": true}

// диапазон: два оператора в одном вложенном документе
f2 := bson.M{"price": bson.M{"$gte": 1000, "$lte": 5000}}

// список значений: $in со срезом
f3 := bson.M{
        "status": bson.M{"$in": []string{"paid", "shipped"}},
        "total":  bson.M{"$gte": 1000},
}

for _, f := range []bson.M{f1, f2, f3} {
        n, err := coll.CountDocuments(ctx, f)
        if err != nil {
                log.Fatal(err)
        }
        fmt.Printf("%v -> %d документов
", f, n)
}

Три уровня сложности в одном списке. f1 — неявный $eq: пара поле-значение. f2 — диапазон: операторы $gte и $lte живут внутри вложенного документа у одного поля, границы включены. f3 — два условия сразу: $in принимает срез допустимых значений, второе поле добавляет неявный AND. CountDocuments здесь — дешёвый способ проверить, сколько документов отвечает фильтру, не тянув их в память.

Операторы сравнения одним списком в таблицу#

ОператорСмыслПример
$eqравенство (подразумевается без записи)bson.M{"status": "paid"}
$neне равноbson.M{"status": bson.M{"$ne": "draft"}}
$gt / $gteбольше / больше или равноbson.M{"price": bson.M{"$gte": 1000}}
$lt / $lteменьше / меньше или равноbson.M{"price": bson.M{"$lte": 5000}}
$inзначение из спискаbson.M{"status": bson.M{"$in": []string{"paid"}}}
$ninзначения нет в спискеbson.M{"status": bson.M{"$nin": []string{"draft"}}}
$existsполе существует (или нет при false)bson.M{"deleted_at": bson.M{"$exists": false}}

bson.M фильтры: неявный AND и вложенность#

Почему bson.M стал дефолтом для условий: фильтры в девяти случаях из десяти — unordered-документ, где порядок полей не влияет на результат, а компактность map-литерала экономит глаза. bson.m фильтры собираются по двум правилам. Первое: несколько ключей верхнего уровня — это AND; «статус оплачен и сумма от тысячи» пишется двумя ключами, без всяких $and. Второе: все операторы поля живут в одном вложенном документе-значении — и диапазон, и список, и существование. Из этих правил вытекает самое популярное место ошибки у новичков: попытка написать два условия для одного поля двумя ключами. В литерале Go такой код не скомпилируется — duplicate key in map literal, и компилятор прав. Но если фильтр собирается динамически, через добавление в карту, второй m["price"] молча перезапишет первый, и запрос станет короче на одно условие; об этом — отдельный коллаут ниже. Ещё одна тонкость bson.M: ключи — строки, и опечатка в имени поля не поймается нигде до момента, когда запрос вернёт пустоту; поэтому фильтры для постоянных запросов объявляют через переменные с именами полей — var fieldPrice = "price" — или через структуры с тегами. В пятом уроке мы уже договаривались: bson.M законен в тестах и разовых проверках, в продовом коде имя поля обязано прийти из константы.

$or, $and и порядок: когда нужен bson.D#

Неявный AND не умеет «или» — за этим приходит логический оператор $or: он принимает массив условий, из которых достаточно любого. bson.M{"$or": []bson.M{{"status": "paid"}, {"total": bson.M{"$gte": 10000}}}} найдёт и оплаченные, и просто дорогие заказы. Внутри каждого элемента массива действует тот же неявный AND, поэтому ветки получаются сколь угодно сложными. Когда же $and пишут явно? В двух случаях: несколько условий на одно и то же поле — "$and": [{"price": {"$gt": 100}}, {"price": {"$lt": 500}}] — и точный контроль над порядком условий, который иногда влияет на план запроса. И вот здесь появляется bson.D: логические операторы — это операторы сервера, а массив условий — упорядоченная структура, и драйвер ждёт, что порядок сохранён. Мера жёсткости у v2 показательная: если передать unordered-карту туда, где порядок значим — например, в сортировку через options.Find().SetSort(), — драйвер отклонит аргумент ошибкой ErrMapForOrderedArgument ещё на клиенте. Фильтры верхнего уровня остаются территорией bson.M, но всё, что внутри $or и $and, привычнее писать через bson.D: визуально совпадает с документацией сервера и не вызывает вопросов при чтении. Полный разбор $regex для поиска по подстроке и комбинаций логических операторов — четырнадцатый урок.

Составное условие: $or поверх неявного AND

GO
// статус в двух «живых» значениях И (сумма от 10 тысяч ИЛИ скидка есть)
filter := bson.M{
        "status": bson.M{"$in": []string{"paid", "shipped"}},
        "$or": []bson.M{
                {"total": bson.M{"$gte": 10000}},
                {"discount": bson.M{"$gt": 0}},
        },
}

cursor, err := coll.Find(ctx, filter)
if err != nil {
        return fmt.Errorf("find orders: %w", err)
}
defer cursor.Close(ctx)

var orders []Order
if err := cursor.All(ctx, &orders); err != nil {
        return fmt.Errorf("collect orders: %w", err)
}

Фильтр читается как фраза: статус из списка — неявный AND с блокой $or; внутри $or каждая ветка — самостоятельное условие, и достаточно любой. Массив для $or собран из bson.M-литералов — порядок элементов массива драйвер сохранит, а карта верхнего уровня не мешает. Дальше — знакомый каркас из седьмого урока: Find, проверка ошибки, defer Close, All в срез.

Правила фильтров одним списком#

  • Пара "поле": значение — неявный $eq; разворачивать без нужды не стоит.
  • Несколько ключей в одном документе — AND; отдельных операторов для него не нужно.
  • Операторы поля живут в одном вложенном значении: $gte и $lte рядом — диапазон.
  • Условия одного поля не пишутся двумя ключами: map перезапишет, а не сложит.
  • $or и $and принимают массивы условий — внутри ветки работает тот же AND.
  • Имена полей — из констант: опечатка в строке не поймается компилятором.
  • Сравнение строгое по типам: 42 и "42" — разные значения; сверяйтесь с mongosh.

$exists и типы: тонкие места условий#

Два условия, вокруг которых накопилось больше всего недоумений. Первое — $exists: он проверяет наличие поля в документе, и это не то же самое, что проверка на nil. Запрос bson.M{"phone": bson.M{"$exists": false}} найдёт документы, у которых поля phone нет вообще; а фильтр bson.M{"phone": nil} матчит и отсутствие поля, и поле со значением null — null в MongoDB означает «поле есть, значения нет», и равенство null специально расширено на пропущенные поля. Для мягкого удаления разница принципиальна: deleted_at отсутствует у живых записей и равна null — у «обнулённых» вручную; выбирайте семантику осознанно, мы вернёмся к ней в уроке про удаление. Второе тонкое место — типы в сравнениях: $gt по числовому полю сравнивает числа с числами, но документ, в котором цена по случайности записана строкой "1000", в диапазон не попадёт и ошибки не вызовет; $type в таких запросах вычисляет предателей. Это плата за схему без валидации — и аргумент за валидатор коллекции, до которого дойдём в двадцать втором уроке. Практика для отладки: сомневаетесь, почему документ не матчился, — воспроизведите фильтр в mongosh и сравните типы через typeof; расхождение BSON-типов всплывает быстрее, чем любую логику перечитывать.

Один словарь на все глаголы: фильтры в Update и Delete#

Недооценённая выгода единого языка условий: фильтры не обслуживают только чтение. UpdateOne, UpdateMany, DeleteOne и DeleteMany принимают ровно тот же документ-условие, что и Find: неявный AND, операторы сравнения, $in и $or — всё работает в операциях записи без единого изменения. Практически это значит, что трудозатраты окупаются дважды: собрал и отладил фильтр «просроченные сессии» для выборки — тот же документ завтра пойдёт в UpdateMany, чтобы пометить их истёкшими, и в DeleteMany, чтобы почистить. Есть и нюансы. В операциях записи фильтр работает по тем же правилам типов и null-семантики, поэтому осторожность с {"поле": nil} важна вдвойне: под такой фильтр попадёт и отсутствие поля, и обновление затронет неожиданный документ. Составные условия в записи проверяются тем же CountDocuments до вызова: сначала посчитали, сколько документов ответит фильтру, потом отправили обновление — дешёвая страховка от пакетной операции по не тому множеству. И последнее: записи — территория уникальных индексов и валидаторов, о которых поговорим в семнадцатом и двадцать втором уроках; фильтр может быть сколь угодно хитрым, но быстрым его делает индекс, а безопасным — схема.

Проверка: неявный AND

Фильтр bson.M{"status": "paid", "total": bson.M{"$gte": 1000}} выбирает документы, у которых...

Проверка: диапазон цены

Как найти документы с ценой от 100 до 500 включительно?

Какие документы найдёт фильтр?

Прочитайте код и выберите, что он выведет:

// коллекция orders:
// {sku: "a", status: "paid",    total: 1500}
// {sku: "b", status: "returned", total: 3000}
// {sku: "c", status: "paid",    total: 800}
// {sku: "d", status: "shipped", total: 2000}

f := bson.M{
        "status": bson.M{"$in": []string{"paid", "shipped"}},
        "total":  bson.M{"$gte": 1000},
}
n, _ := coll.CountDocuments(ctx, f)
fmt.Println(n)

Соберите составной фильтр

Восстановите операторы: нижняя граница, верхняя граница и список допустимых статусов.

filter := bson.M{ "total": bson.M{"": 1000, "": 5000}, "status": bson.M{"": []string{"paid", "shipped"}}, } cursor, err := coll.Find(ctx, filter)

Соберите запрос с составным фильтром

Расставьте строки по порядку: фильтр, запрос, страховка закрытия, сбор результата.

Строки

Ваша программа

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

Карточки: операторы фильтров

Термин

Неявный $eq

нажмите, чтобы перевернуть
Определение

Пара "поле": значение — сравнение на равенство без оператора; bson.M{"status": "paid"} эквивалентен {$eq: "paid"}.

1 / 6

Словарь условий собран: неявный AND для повседневных запросов, операторы сравнения в одном вложенном значении, $or для альтернатив, $exists для различения «нет поля» и «пусто». Дальше этот же словарь заработает в записи: в уроке про UpdateOne и UpdateMany фильтр выберет документы, которые поменяют операторы $set и $inc. Сложные комбинации — $regex, вложенные $and, поиск по массивам — тема урока про $and, $or и $regex. А пока фильтры гоняются по всей коллекции, скорость держится на честном слове: в уроке про индексы покажем, как превратить частые фильтры из сканирования в поиск по дереву. Практическое задание на закрепление: возьмите три реальных запроса из своего проекта и перепишите их с $in и диапазонами; каждый проверьте в mongosh тем же документом — совпадение количества документов в консоли и в CountDocuments лучший тест на то, что Go-фильтр означает ровно то, что вы имели в виду.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Фильтр — BSON-документ: ключи называют поля, значения задают требования; один словарь работает в Find, Update и Delete.
  • Пара "поле": значение — неявный $eq; несколько ключей — неявный AND.
  • Операторы одного поля живут в одном вложенном документе: {"$gte": 100, "$lte": 500} — диапазон с включёнными границами.
  • $in принимает срез значений; $or — массив условий, из которых достаточно любого.
  • Два условия на одно поле — через один оператор-документ или явный $and: карта перезапишет ключ молча.
  • $exists:false — поля нет; {"поле": nil} матчит и null, и отсутствие: семантики разные.
  • Сравнения строгие по BSON-типам: 42 и "42" не равны; для порядка значимых мест — bson.D.

Домашнее задание

Возьмите коллекцию заказов из прошлых уроков и напишите три фильтра: диапазон суммы с включёнными границами, список из трёх статусов через $in и комбинацию «оплачен ИЛИ сумма больше N» через $or. Каждый фильтр прогоните и через CountDocuments в Go, и через db.orders.find() в mongosh — количества обязаны совпасть. Отдельно попробуйте собрать фильтр в цикле с перезаписью ключа и посмотрите, как запрос молча худеет.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · Фильтры: $eq, $gt, $in и bson.M)

Вопросы и ответы по уроку

bson.M или bson.D для фильтров — что выбрать по умолчанию?

По умолчанию bson.M: фильтры не чувствительны к порядку полей, а map-литерал читается легче. bson.D нужен внутри $or и $and с массивами условий, в сортировках и там, где порядок операторов значим; драйвер сам отклонит карту в порядке-зависимых местах ошибкой ErrMapForOrderedArgument.

Почему фильтр не находит документ, который есть в коллекции?

Три частые причины: опечатка в имени поля, несовпадение BSON-типа (строка вместо числа) и молчаливая перезапись ключа при динамической сборке. Проверьте фильтр тем же документом в mongosh и сравните типы значений через typeof — расхождение видно сразу.

Чем отличается {"поле": nil} от {"поле": {"$exists": false}}?

Первое матчит документы, где поле равно null, и где поля нет вообще — равенство null в MongoDB намеренно расширено. Второе требует именно отсутствия поля. Для мягкого удаления это разные множества: «обнулённая» запись и «никогда не имевшая» — не одно и то же.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

zerolog25 уроков~12 чпрактика25 уроков уже доступны

Zerolog в Go: быстрое JSON-логирование

Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.

  • Писать структурированные JSON-логи с нуля аллокаций
  • Выбирать уровни и поля события осознанно
  • Настраивать ConsoleWriter для dev и JSON для прода
  • Строить сублоггеры и контекстные поля на запрос