Содержание урока · 10 разделов
Сервер MongoDB выглядит изнутри как аккуратная матрёшка: инстанс содержит базы данных, база — коллекции, коллекция — документы. В SQL-мире вы привыкли, что таблицы нужно создавать командой и миграцией, а в документном мире половина этой церемонии исчезает — но взамен появляются правила, о которых молчат туториалы: ленивое создание, ограничения имён, лимит размера документа. Драйвер отражает ту же матрёшку типами: mongo.Client, mongo.Database, *mongo.Collection. Понимание того, что каждый из этих типов делает — а что только изображает, — отличает код, который случайно работает, от кода, который работает предсказуемо. В этом уроке пройдём все три уровня сверху вниз, потрогаем документы как единицу хранения и зафиксируем правила, которые спасут от коллекций с опечатками в имени. Всё это — на коде, который можно запустить локально сразу после урока: разница между «прочитал» и «потрогал» здесь особенно заметна, потому что драйвер поощряет эксперименты.
Client, Database, Collection: три уровня драйвера#
Вопрос «mongo коллекции go-код как достаёт» решается двумя вызовами. client.Database("shop") возвращает mongo.Database — дескриптор базы, db.Collection("orders") возвращает mongo.Collection — дескриптор коллекции. Важнейший факт про оба типа: это лёгкие дескрипторы, а не сетевые объекты. Ни Database, ни Collection ничего не отправляют на сервер в момент создания — они просто запоминают имя; создать их можно сколько угодно, даже в цикле, без единого пакета по сети. Вся сетевая жизнь начинается на операциях: InsertOne, FindOne, CountDocuments — вот где ходят пакеты. Оба типа потокобезопасны: один дескриптор коллекции можно смело разделять между горутинами, мьютексы в вашем коде для этого не нужны. Дешевизна дескрипторов даёт рабочий стиль: дескрипторы коллекций создаются в одном месте — в конструкторе хранилища или пакете моделей — и живут столько же, сколько приложение; никаких Collection-на-запрос. Два вызова Database с одним именем вернут два независимых дескриптора — они равнозначны, и с какими работать — вопрос стиля: половина кодовых баз берёт дескриптор базы один раз в конструкторе, а коллекции от него при инициализации хранилища.
Дескрипторы в действии
GOclient, err := mongo.Connect(options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(context.Background())
db := client.Database("shop")
orders := db.Collection("orders")
users := client.Database("shop").Collection("users")
n, err := orders.EstimatedDocumentCount(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("заказов примерно: %d
", n)Два уровня иерархии в четырёх строках: client.Database возвращает mongo.Database, .Collection — mongo.Collection. Ни один из этих вызовов не обращается к серверу — это дескрипторы-имена. EstimatedDocumentCount читает метаданные коллекции и отвечает быстро, но без гарантии точности; для точного счётчика с условиями существует CountDocuments с фильтром — о нём в уроке про фильтры.
Документы mongodb: BSON, поля и _id#
Документы mongodb — единица хранения и обмена: именно они летят по сети в BSON, именно их видит mongosh. Документ — упорядоченный набор полей: в BSON порядок значим, и драйвер это уважает, когда пишет структуры и bson.D. Типы полей богаче JSON: int32 и int64 различаются, дата — time.Time с миллисекундной точностью, деньги — Decimal128, идентификатор — ObjectID, значения могут быть массивами и вложенными документами без ограничения глубины, кроме лимита размера. Есть и привычные по JSON штуки: null, бинарные данные, логические значения. У каждого документа есть _id — первичный ключ коллекции: он существует всегда, уникален и неизменяем. Если вы его не задали, драйвер сгенерирует ObjectID на стороне клиента при вставке — об этом подробнее в уроке про ObjectID. По умолчанию в коллекции нет требования совпадения форм: соседние документы могут отличаться полями, и это осознанная свобода — схема переезжает в ваши Go-структуры с bson-тегами, а база остаётся исполнителем. Поле _id после вставки неизменяемо — обновить его не даст ни драйвер, ни сервер, поэтому продумайте ключ заранее: миграция идентификаторов на живой коллекции делается перезаписью всех документов.
Документ не может весить больше 16 мегабайт — ограничение на уровне сервера, и оно защитит вас от себя: полноразмерные видео в документ не запихнуть. Медиа хранят на диске, в S3 или GridFS, а в документе оставляют ссылки и метаданные.
Коллекции создаются лениво#
В MongoDB нет CREATE TABLE: база и коллекция возникают в момент первой записи. Вставили документ в shop.orders — появились и база shop, и коллекция orders, с дефолтными настройками. До первой вставки пустые коллекции не существуют — show collections в mongosh их не покажет. Ленивость удобна для прототипов и раздражает в проде, и вот почему: коллекция создаётся по имени, а имя берётся из кода, где живут опечатки. Плюс драйвера в том, что дескриптор с несуществующим именем ошибки не даст — минус в том, что ошибки и не даст: вставка молча создаст коллекцию с кривым именем, и половина данных окажется в order вместо orders. Для настроек — capped-коллекции фиксированного размера для кольцевых логов, валидация схемы, индексы — существуют явные команды создания, их разберём во второй половине курса — в уроках про индексы и валидацию схемы, а типовая вставка обходит их стороной.
db.Collection("orderS") компилируется, вставляется и не ругается — вы просто получите вторую пустую коллекцию рядом с настоящей, а отчёты пойдут вразнос. Лечится скучно и надёжно: имена баз и коллекций объявляются константами в одном месте, а не строковыми литералами по всему коду. Туда же — обработка конфигурации: если имя коллекции приходит из переменной окружения, добавьте валидацию на старте, а не найдите её по кривым отчётам.
Ленивое создание в живом виде
GO// дескрипторы объявили — база ещё не существует
db := client.Database("shop")
orders := db.Collection("orders")
colls, _ := db.ListCollectionNames(ctx, bson.D{})
fmt.Println("коллекций:", len(colls)) // 0
// первая запись материализует базу и коллекцию
_, err := orders.InsertOne(ctx, bson.D{{Key: "sku", Value: "kb-87-tkl"}})
if err != nil {
log.Fatal(err)
}
colls, _ = db.ListCollectionNames(ctx, bson.D{})
fmt.Println("коллекций:", len(colls)) // 1 — orders на местеЖивая демонстрация ленивого создания. ListCollectionNames читает список коллекций базы: до первой вставки он пуст, хотя дескриптор orders уже объявлен. InsertOne — глагол из следующего урока — создаёт и базу shop, и коллекцию orders одним пакетом. Повторите это локально рядом с mongosh: show dbs и show collections подтвердят каждый шаг.
Capped-коллекции: кольцевой буфер в базе#
Единственный распространённый сценарий, где коллекцию создают явно и заранее, — capped: фиксированный кольцевой буфер. db.CreateCollection(ctx, "events", options.CreateCollection().SetCapped(true).SetSizeInBytes(10_000_000)) создаёт коллекцию, в которой новые записи вытесняют старые, когда место кончилось: последние десять мегабайт событий всегда на месте, чистить ничего не нужно. Лимит по числу документов задаётся SetMaxDocuments. Типовые обитатели таких коллекций — аудиторские события, поток логов, очередь сообщений для чтения tailable-курсором — курсором, который не закрывается на конце, а ждёт новых записей, как tail -f. Альтернатива capped для чистки — TTL-индекс, который удаляет документы по возрасту: он требует отдельного индекса и разбирается в соответствующем уроке. Те же опции CreateCollection умеют навешивать валидатор схемы — до него дойдём отдельным уроком; для обычных коллекций дефолтного ленивого создания достаточно. Проверить настройки можно в mongosh через db.events.isCapped() — маленькая команда, которая иногда отвечает на большой вопрос «почему мои старые данные исчезли».
Кто есть кто в иерархии#
| Уровень | Аналог в SQL | Тип в драйвере | Что умеет |
|---|---|---|---|
| Client | — (соединение) | *mongo.Client | пул соединений, Ping, сессии, старт транзакций |
| Database | база данных | *mongo.Database | дескрипторы коллекций, команды уровня базы |
| Collection | таблица | *mongo.Collection | CRUD, индексы, агрегации, счётчики |
| Document | строка | структура, bson.M, bson.D | данные; лимит 16 МБ, обязателен _id |
Таблица подсказывает и ограничения, которые стоит знать до первого code review. Имя базы: от одного до 63 символов, чувствительно к регистру, без пробелов, точек, слэшей и знака доллара. Имя коллекции: не пустое, без знака доллара, не начинается с system. — этот префикс зарезервирован сервером; вместе с именем базы полное пространство имён не должно превышать 255 байт. Регистр важен: orders и Orders — две разные коллекции, и Windows-хостинги с нечувствительной файловой системой превращают это в отдельный вид боли, если вы решите переносить данные руками. Имена с автогенерированными сборками вроде tmp.orders_copy_2024_03_12 — признак того, что пора заводить константы и миграции.
Правила имён одной строкой#
- База: 1-63 символа, без пробела, точки, слэша и $; регистр значим.
- Коллекция: без $, не пустая, не начинается с system.; регистр значим.
- Пространство имён db.coll — не больше 255 байт.
- orders и Orders — разные коллекции; проверяйте регистр в конфигах.
- Временные имена вида copy_2024_03_12 — запах ручных операций.
Один клиент на процесс#
Сколько нужно клиентов — вопрос с почти всегда одним ответом: один. mongo.Client потокобезопасен и ведёт внутренний пул соединений: горутины берут сокеты по мере надобности и возвращают их обратно, а вам не нужно ничего синхронизировать. Дескрипторы Database и Collection создаются от клиента в один момент — обычно в конструкторе приложения — и передаются зависимостям дальше: репозиториям, хендлерам, воркерам. Второй клиент оправдан в двух случаях: тесты с отдельным окружением и редкие операции, которым нужна изоляция настроек, вроде отдельного read preference для аналитики; оба сценария мы тронем позже — в уроках про тесты и производительность. Клиент на каждый HTTP-запрос — антипаттерн из первого дня: каждый несёт собственный пул, сервер копит лишние сокеты, а latency растёт на установке соединения. Если мигрируете с ORM-мира, ориентир тот же, что и у sql.DB или GORM: долгоживущий хендл, короткоживущие операции. Отдельный плюс одного клиента — единая точка настройки: параметры пула, read preference по умолчанию и мониторинг настраиваются на клиенте один раз, а не в каждой коллекции.
Мультитенантность — сценарий, где вопрос «одна база или несколько» возникает всерьёз. Три рабочих раскладки. Первая: общая база, поле tenant_id в каждом документе и индекс, начинающийся с него; проще всего в эксплуатации, требует дисциплины в фильтрах. Вторая: общая база, коллекция на клиента (orders_acme, orders_globex); изоляция данных нагляднее, но количество коллекций растёт линейно с клиентами, а кросс-тенантные отчёты превращаются в обход. Третья: база на клиента; максимальная изоляция — бэкапы и экспорт клиента делаются командой, но лимиты на количество коллекций и администрирование наступают быстрее, чем кажется. Я вижу первую раскладку по умолчанию и откат к третьей, когда у клиента есть требование физической изоляции. Выбирайте до того, как появятся данные: переезд между раскладками — это ручная миграция каждой коллекции. И не переусложняйте раньше времени: если клиентов меньше пяти, первая раскладка выигрывает почти всегда, а изоляция добирается дисциплиной фильтров и индексом с префиксом tenant_id.
Проверка: ленивое создание
Что произойдёт при первой вставке в несуществующую коллекцию?
Проверка: сколько клиентов
Когда оправдан второй экземпляр *mongo.Client?
Какой порядок полей уйдёт в BSON?
Прочитайте код и выберите, что он выведет:
doc := bson.D{
{Key: "type", Value: "стол"},
{Key: "price", Value: 100},
}
data, err := bson.MarshalExtJSON(doc, false, false)
fmt.Println(string(data), err)Соберите дескрипторы
Допишите два уровня иерархии и быстрый счётчик документов.
Соберите работу с дескрипторами
Расставьте строки в порядке выполнения: клиент, база, коллекция, счётчик, вывод, отключение.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: иерархия драйвера
Иерархия собрана: клиент с пулом, дескрипторы баз и коллекций без сетевой цены, документы с обязательным _id и лимитом 16 МБ, имена — в константах, второй клиент — только для тестов. Дальше логично сделать первый шаг записи: в уроке про InsertOne и InsertMany вставим документы и разберёмся с автогенерацией _id и порядком вставки. Потом прочитаем их обратно в уроке про FindOne и разберём ErrNoDocuments. Отдельный совет для практики: дескрипторы и ленивое создание проверяются только на живом сервере — песочница сайта показывает тематическую программу на чистом Go, поэтому поднимите контейнер из второго урока и посмотрите глазами, как после первого InsertOne появятся база и коллекция: эта визуализация стоит половины теории. Пока учитесь, держите mongosh открытым постоянно: show dbs и show collections после каждого нового дескриптора мгновенно показывают, что и где создалось, а MongoDB Compass — бесплатный GUI от тех же разработчиков — открывает документы, индексы и планы запросов без консоли. И последний мостик вперёд: когда дойдём до чтения, окажется, что дескрипторы коллекций — ещё и точка настройки курсоров, проекций и агрегаций; поэтому имена и структура хранилища, выстроенные сейчас, останутся с вами до конца проекта.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Иерархия: Client — пул соединений, Database и Collection — лёгкие дескрипторы-имена без сетевых операций.
- Дескрипторы потокобезопасны и дёшевы: создаются один раз и передаются зависимостям.
- Документы mongodb: BSON-поля с типами, обязательный _id, лимит 16 МБ, порядок полей значим.
- Базы и коллекции создаются лениво при первой записи — опечатка в имени молча плодит новые коллекции.
- Имена: база до 63 символов без пробелов и $; коллекция без $, не с system., регистр значим.
- Один клиент на процесс; второй — только тесты или изолированные настройки доступа.
- Запуск — локально с MongoDB из Docker; mongosh и Compass показывают, что создалось.
Домашнее задание
Поднимите MongoDB из второго урока и напишите программу: подключитесь, возьмите дескрипторы client.Database("shop") и db.Collection("orders"), вставьте один документ через coll.InsertOne (сигнатура подглядывается в следующем уроке), а после этого выполните в mongosh show dbs и show collections и найдите созданную базу. Затем переименуйте коллекцию в коде в "orderS", повторите вставку и убедитесь, что появилась вторая коллекция — цена опечатки в имени.
Личные заметкиПусто
Вопросы и ответы по уроку
Как создать коллекцию явно, без вставки?
Методом coll := db.CreateCollection(ctx, "orders") с опциями вроде capped или validator. Обычные коллекции создавать заранее не нужно — они появятся при первой вставке; явное создание оправдано только для настроек.
Можно ли переименовать коллекцию?
Да, командой renameCollection на уровне базы, но она блокирует коллекцию и не работает между базами без оговорок на шардинге. Поэтому в коде имена коллекций держат в константах: переименование на живых данных — операция, а не правка строки.
Что будет, если в одну коллекцию вставить документы с разными полями?
Ничего страшного: без валидатора схема коллекции не навязывается, документы сосуществуют. Контракт формы в этом случае несёт ваш код — Go-структуры с bson-тегами; формализовать его можно JSON Schema-валидатором коллекции, этому посвящён отдельный урок курса.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Zerolog в Go: быстрое JSON-логирование
Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.
- Писать структурированные JSON-логи с нуля аллокаций
- Выбирать уровни и поля события осознанно
- Настраивать ConsoleWriter для dev и JSON для прода
- Строить сублоггеры и контекстные поля на запрос