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

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

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

Урок 3 из 25 ~16 мин +50 XP

Базы, коллекции и документы

В mongo-driver вся работа раскладывается на три объекта — Client, Database и Collection: между ними распределяются базы, коллекции и документы mongodb. Урок про то, что живёт на каждом уровне, почему коллекции создаются лениво, какими правилами ограничены имена и почему один клиент на процесс — почти всегда правильное число.

Содержание урока · 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 с одним именем вернут два независимых дескриптора — они равнозначны, и с какими работать — вопрос стиля: половина кодовых баз берёт дескриптор базы один раз в конструкторе, а коллекции от него при инициализации хранилища.

Дескрипторы в действии

GO
client, 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 после вставки неизменяемо — обновить его не даст ни драйвер, ни сервер, поэтому продумайте ключ заранее: миграция идентификаторов на живой коллекции делается перезаписью всех документов.

Коллекции создаются лениво#

В MongoDB нет CREATE TABLE: база и коллекция возникают в момент первой записи. Вставили документ в shop.orders — появились и база shop, и коллекция orders, с дефолтными настройками. До первой вставки пустые коллекции не существуют — show collections в mongosh их не покажет. Ленивость удобна для прототипов и раздражает в проде, и вот почему: коллекция создаётся по имени, а имя берётся из кода, где живут опечатки. Плюс драйвера в том, что дескриптор с несуществующим именем ошибки не даст — минус в том, что ошибки и не даст: вставка молча создаст коллекцию с кривым именем, и половина данных окажется в order вместо orders. Для настроек — capped-коллекции фиксированного размера для кольцевых логов, валидация схемы, индексы — существуют явные команды создания, их разберём во второй половине курса — в уроках про индексы и валидацию схемы, а типовая вставка обходит их стороной.

Ленивое создание в живом виде

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.CollectionCRUD, индексы, агрегации, счётчики
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)

Соберите дескрипторы

Допишите два уровня иерархии и быстрый счётчик документов.

db := client.("shop") orders := db.("orders") n, err := orders.(context.Background()) if err != nil { log.Fatal(err) } fmt.Println("заказов примерно:", n)

Соберите работу с дескрипторами

Расставьте строки в порядке выполнения: клиент, база, коллекция, счётчик, вывод, отключение.

Строки

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

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

Карточки: иерархия драйвера

Термин

*mongo.Client

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

Корень всего: пул соединений, Ping, сессии, транзакции. Потокобезопасен, один на процесс.

1 / 6

Иерархия собрана: клиент с пулом, дескрипторы баз и коллекций без сетевой цены, документы с обязательным _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", повторите вставку и убедитесь, что появилась вторая коллекция — цена опечатки в имени.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · Базы, коллекции и документы)

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

Как создать коллекцию явно, без вставки?

Методом coll := db.CreateCollection(ctx, "orders") с опциями вроде capped или validator. Обычные коллекции создавать заранее не нужно — они появятся при первой вставке; явное создание оправдано только для настроек.

Можно ли переименовать коллекцию?

Да, командой renameCollection на уровне базы, но она блокирует коллекцию и не работает между базами без оговорок на шардинге. Поэтому в коде имена коллекций держат в константах: переименование на живых данных — операция, а не правка строки.

Что будет, если в одну коллекцию вставить документы с разными полями?

Ничего страшного: без валидатора схема коллекции не навязывается, документы сосуществуют. Контракт формы в этом случае несёт ваш код — Go-структуры с bson-тегами; формализовать его можно JSON Schema-валидатором коллекции, этому посвящён отдельный урок курса.

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

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

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

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

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

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

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