Модуль 1 · Подключение и первые данные

Справочник по MongoDB · модуль 1 · подключение и первые данные

Глава 1.1

1.1

Сервер, база,
коллекция

В этой главе разобрано, что такое сервер MongoDB, чем он отличается от клиента, как из программы дойти до конкретной коллекции и почему база данных создаётся сама, без единой команды на создание.

Категория
Подключение и устройство базы
Уровень
Начальный
Связанные темы
Документ и BSON, вставка, чтение
Зачем это знать
Понимать, куда именно программа кладёт и откуда берёт данные

Как запускать примеры этой главы

БазыГлава только читает данные — сбрасывать базы не нужно.
Архивmongodb-glava-1.1-python.zip, 58 КБmongodb-glava-1.1-ruby.zip, 58 КБmongodb-glava-1.1-go.zip, 61 КБmongodb-glava-1.1-cpp.zip, 60 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.
ФайлЗаготовки нет: глава сама собирает программу подключения. Готовая программа из §4–§6 — hello/hello.pyhello/hello.rbhello/hello.gohello/hello.cpp.
Запускpython hello/hello.pypython3 hello/hello.py · в Docker: docker compose run --rm python hello/hello.pyruby hello/hello.rb · в Docker: docker compose run --rm ruby hello/hello.rbcd hello, затем go run hello.go · в Docker: docker compose run --rm go hello/hello.gog++ -std=c++17 hello/hello.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp hello/hello.cpp
ПорядокПример из §4 и §6 складываются в одну программу — ровно так устроен hello. Как поставить драйвер — глава 1.0, §9.

§1Сервер и клиент — разные программы

MongoDB — это сервер: программа, которая держит данные на диске и слушает сетевой порт. По умолчанию порт — 27017. Сервер ничего не показывает: он ждёт, когда к нему обратятся, и отвечает на запросы.

Обращается к нему клиент — и клиентов несколько:

  • MongoDB Compass — графическое приложение. В нём видны базы, документы, типы полей и результат запроса. С ним удобно разбираться и показывать;
  • mongosh — консольная оболочка. Те же команды, но текстом; ей удобно автоматизировать;
  • драйвер — библиотека внутри вашей программы. Для Python это PyMongo. Так с базой работает приложение.

Если закрыть Compass, данные останутся: они лежат на сервере. Если остановить сервер, ни один клиент до них не доберётся.

На полях: где взять сервер

На занятиях сервер уже поднят и слушает localhost:27017 — это стенд курса. Как поднять такой же у себя, описано в репозитории практик: там лежит и файл запуска, и данные всех четырёх баз курса. Как скачать эти данные и загрузить в MongoDB — глава 1.0. Если сервер или Compass не устанавливаются, стенд поднимается в Docker или в браузере — это глава 1.1а.

§2Три уровня хранения

Внутри сервера данные разложены по трём уровням. Ни один нельзя пропустить: документ всегда лежит в коллекции, коллекция — в базе, база — на сервере.

сервер

Одна установленная MongoDB. Держит все базы сразу и отвечает на порту.

mongodb://localhost:27017
база данных

Пространство имён под один проект. У нас их четыре: shop, hh, logs, org. Базы друг о друге ничего не знают.

shop
коллекция

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

shop.products
документ

Единица хранения: пары «поле — значение». Его кладут и достают целиком.

{ _id: "p-003", title: "Ноутбук HP Pavilion 14", price: 67900 }

Полное имя коллекции пишется через точку: shop.products — коллекция products в базе shop. Такая запись встречается в сообщениях об ошибках и в документации, и читать её нужно справа налево: «товары, которые лежат в магазине».

§3Строка подключения по частям

Адрес сервера записывается одной строкой — её называют строкой подключения или URI. Одна и та же строка работает и в Compass, и в mongosh, и в коде.

mongodb://localhost:27017

mongodb://Схема: по ней клиент понимает, каким протоколом говорить. У облачного MongoDB Atlas схема другая — mongodb+srv://.
localhostАдрес машины с сервером. localhost — «эта же машина». В рабочем проекте здесь будет имя хоста или адрес в сети.
27017Порт, который слушает сервер. Значение по умолчанию; если сервер запущен на другом порту, пишем его.

В настоящем проекте в строку добавляются логин и пароль: mongodb://user:secret@db.example.com:27017. На стенде курса их нет: сервер поднят без проверки подлинности. Это допустимо, потому что он стоит на вашей машине и наружу не смотрит.

§4MongoClient: создание клиента

В Python работа начинается с объекта MongoClient: это клиент, живущий внутри вашей программы.

Python · PyMongoклиент
from pymongo import MongoClient

# Локальный сервер стенда курса
client = MongoClient("mongodb://localhost:27017/")

# Облачный Atlas — другая схема и обязательный пароль
# client = MongoClient("mongodb+srv://user:pass@cluster0.mongodb.net/")
C++ · mongocxxклиент
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>

mongocxx::instance instance{};   // ровно один на программу

// Локальный сервер стенда курса
mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}};

// Атлас: mongocxx::uri{"mongodb+srv://user:pass@cluster0.mongodb.net"}
Go · mongo-driverклиент
import (
    "go.mongodb.org/mongo-driver/v2/mongo"
    "go.mongodb.org/mongo-driver/v2/mongo/options"
)

// Локальный сервер стенда курса
client, err := mongo.Connect(options.Client().
    ApplyURI("mongodb://localhost:27017"))
if err != nil {
    log.Fatal(err)
}
defer client.Disconnect(context.Background())

// Атлас: ApplyURI("mongodb+srv://user:pass@cluster0.mongodb.net")
Ruby · mongoклиент
require "mongo"

# Локальный сервер стенда курса
client = Mongo::Client.new("mongodb://localhost:27017/")

# Облачный Atlas — другая схема и обязательный пароль
# client = Mongo::Client.new("mongodb+srv://user:pass@cluster0.mongodb.net/")
mongosh · оболочкаподключение
# то же подключение из терминала
mongosh "mongodb://localhost:27017"

# внутри оболочки клиент уже создан,
# текущая база доступна как db
Осторожно: строка создаётся, соединение — нет

Строка MongoClient(...) выполнится мгновенно даже при выключенном сервере: клиент только запоминает адрес, а соединяется лениво — при первом настоящем запросе. Поэтому ошибка «сервер недоступен» появляется не на создании клиента, а на первой операции с данными.

§5Выбор базы и коллекции

От клиента спускаемся на уровень ниже — к базе, затем к коллекции. В PyMongo это обычное обращение по ключу.

Python · PyMongoспуск по трём уровням
client = MongoClient("mongodb://localhost:27017/")   # сервер
db = client["shop"]                                # база
collection = db["products"]                         # коллекция

# Тот же смысл через методы — так пишут, когда имя лежит в переменной
db = client.get_database("shop")
collection = db.get_collection("products")

print("Тип client:", type(client).__name__, "| тип db:", type(db).__name__,
      "| тип collection:", type(collection).__name__)
C++ · mongocxxспуск по трём уровням
mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}};  // сервер
auto db = client["shop"];                                    // база
auto collection = db["products"];                             // коллекция

// Тот же смысл через методы — так пишут, когда имя лежит в переменной
db = client.database("shop");
collection = db.collection("products");

std::cout << "база: " << db.name() << ", коллекция: " << collection.name() << std::endl;
Go · mongo-driverспуск по трём уровням
client, _ := mongo.Connect(options.Client().
    ApplyURI("mongodb://localhost:27017"))          // сервер
db := client.Database("shop")                         // база
collection := db.Collection("products")              // коллекция

// Имя из переменной подставляется так же: Database(name)

fmt.Printf("Тип client: %T | тип db: %T | тип collection: %T\n", client, db, collection)
Ruby · mongoспуск по трём уровням
client = Mongo::Client.new("mongodb://localhost:27017/")  # сервер
db = client.use("shop").database                        # база
collection = db[:products]                              # коллекция

# Имя из переменной: db[name.to_sym] или db.collection(name)
collection = db.collection("products")

puts "Тип client: #{client.class} | тип db: #{db.class} | тип collection: #{collection.class}"

Ни одна из этих строк не обращается к серверу: они создают объекты-указатели. Это видно по именам типов:

Вывод в терминалрезультат запуска
Тип client: MongoClient | тип db: Database | тип collection: Collection
Вывод в терминалрезультат запуска
база: shop, коллекция: products
Вывод в терминалрезультат запуска
Тип client: *mongo.Client | тип db: *mongo.Database | тип collection: *mongo.Collection
Вывод в терминалрезультат запуска
Тип client: Mongo::Client | тип db: Mongo::Database | тип collection: Mongo::Collection

От SQL-баз MongoDB отличается ещё одним: база и коллекция создаются сами — в тот момент, когда в них впервые что-то записали. Команды «создать базу» нет. Обратная сторона: опечатка в имени не вызовет ошибку — запрос к db["prodcuts"] вернёт пустой результат, потому что такая коллекция ещё не существует.

§6Что уже есть на сервере

Спросим у сервера, какие базы он держит, а у базы — какие в ней коллекции. Это первое, что делают, подключившись к незнакомой базе.

Python · PyMongoосмотр сервера
print("Базы данных:", client.list_database_names())

db = client["shop"]
print("Коллекции shop:", db.list_collection_names())

collection = db["products"]
print("Документов:", collection.count_documents({}))
C++ · mongocxxосмотр сервера
std::cout << "Базы данных:";
for (const auto& name : client.list_database_names())
    std::cout << " " << name;
std::cout << std::endl;

auto db = client["shop"];
std::cout << "Коллекции shop:";
for (const auto& name : db.list_collection_names())
    std::cout << " " << name;
std::cout << std::endl;

auto collection = db["products"];
std::cout << "Документов: " << collection.count_documents(make_document()) << std::endl;
Go · mongo-driverосмотр сервера
names, _ := client.ListDatabaseNames(ctx, bson.D{})
fmt.Println("Базы данных:", names)

db := client.Database("shop")
collections, _ := db.ListCollectionNames(ctx, bson.D{})
fmt.Println("Коллекции shop:", collections)

count, _ := db.Collection("products").CountDocuments(ctx, bson.D{})
fmt.Println("Документов:", count)
Ruby · mongoосмотр сервера
puts "Базы данных: #{client.database_names}"

db = client.use("shop").database
puts "Коллекции shop: #{db.collection_names}"

collection = db[:products]
puts "Документов: #{collection.count_documents({})}"
mongosh · оболочкато же самое
show dbs
use shop
show collections
db.products.countDocuments({})
Вывод в терминалстенд курса, MongoDB 7.0.40
Базы данных: ['admin', 'config', 'hh', 'local', 'logs', 'org', 'sandbox', 'shop']
Коллекции shop: ['customers', 'products', 'orders']
Документов: 21
Вывод в терминалстенд курса, MongoDB 7.0.40
Базы данных: admin config hh local logs org sandbox shop
Коллекции shop: products customers orders
Документов: 21
Вывод в терминалстенд курса, MongoDB 7.0.40
Базы данных: [admin config hh local logs org sandbox shop]
Коллекции shop: [products orders customers]
Документов: 21
Вывод в терминалстенд курса, MongoDB 7.0.40
Базы данных: ["admin", "config", "hh", "local", "logs", "org", "sandbox", "shop"]
Коллекции shop: ["customers", "orders", "products"]
Документов: 21

Пять баз наши: shop, hh, logs, org и песочница sandbox. Ещё три сервер завёл сам, и трогать их не нужно:

adminСлужебная база: пользователи, роли и административные команды сервера.сервер
configВнутренние настройки. Становится важной, когда данные разложены по нескольким серверам.сервер
localДанные, которые не покидают эту машину: журнал операций и служебные записи.сервер

Порядок в ответе произвольный: коллекции пришли не по алфавиту и не в порядке создания — у разных драйверов он и выглядит по-разному. Сервер порядок в этом списке не гарантирует; нужен алфавит — сортируйте сами.

§7Тот же осмотр в Compass

Всё, что мы сейчас получили тремя вызовами, Compass показывает одним экраном. Слева — список баз, у каждой раскрывается список коллекций, справа — размер и число документов.

Список баз данных в MongoDB Compass: shop, hh, logs, org и служебные admin, config, local
Compass 1.50 · стенд курса — четыре базы курса и три служебные

На кадре есть то, чего нет в выводе Python. У каждой базы указаны размер хранения, объём данных, число коллекций и число индексов. На кадре видна разница между двумя базами: logs — одна коллекция и 207.02 kB данных, shop — три коллекции и 79.32 kB. Документов в shop больше по числу коллекций, но данных втрое меньше: объём зависит не от количества документов, а от того, что внутри них.

Отдельно стоит база hh: 37.72 MB и десять коллекций против четырёх, которые мы перечислили. В ней остались коллекции на 20 000 документов с прошлых занятий.

Коллекции базы shop в Compass: products, orders, customers с числом документов
Compass · база shop — три коллекции, 21 товар, 120 заказов, 20 покупателей

§8Один клиент на программу

MongoClient создаётся один раз на программу. Внутри него — пул соединений: несколько открытых каналов к серверу, которые переиспользуются между запросами. Создавать клиента в каждой функции — значит каждый раз поднимать этот пул заново.

Так делают · один на приложениеPython
# создаётся один раз при старте программы
client = MongoClient("mongodb://localhost:27017/")
products = client["shop"]["products"]

def count_all():
    return products.count_documents({})
Так делают · один на приложениеC++
// один пул на программу: клиент живёт рядом с приложением
mongocxx::instance instance{};
mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}};
auto products = client["shop"]["products"];

auto count_all() {
    return products.count_documents(make_document());
}
Так делают · один на приложениеGo
// создаётся один раз при старте программы
var products *mongo.Collection

func init() {
    client, _ := mongo.Connect(options.Client().
        ApplyURI("mongodb://localhost:27017"))
    products = client.Database("shop").Collection("products")
}

func countAll(ctx context.Context) (int64, error) {
    return products.CountDocuments(ctx, bson.D{})
}
Так делают · один на приложениеRuby
# создаётся один раз при старте программы
CLIENT = Mongo::Client.new("mongodb://localhost:27017/")
PRODUCTS = CLIENT.use("shop").database[:products]

def count_all
  PRODUCTS.count_documents({})
end
Так не делают · клиент внутри функцииPython
def count_all():
    # новый пул соединений на каждый вызов
    client = MongoClient("mongodb://localhost:27017/")
    return client["shop"]["products"].count_documents({})
Так не делают · клиент внутри функцииC++
auto count_all() {
    // новый клиент и новый пул на каждый вызов
    mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}};
    return client["shop"]["products"]
        .count_documents(make_document());
}
Так не делают · клиент внутри функцииGo
func countAll(ctx context.Context) int64 {
    // новый пул соединений на каждый вызов
    client, _ := mongo.Connect(options.Client().
        ApplyURI("mongodb://localhost:27017"))
    defer client.Disconnect(ctx)

    count, _ := client.Database("shop").Collection("products").
        CountDocuments(ctx, bson.D{})
    return count
}
Так не делают · клиент внутри функцииRuby
def count_all
  # новый пул соединений на каждый вызов
  client = Mongo::Client.new("mongodb://localhost:27017/")
  client.use("shop").database[:products].count_documents({})
end

Закрывать клиента методом client.close() нужно в конце работы программы — в коротком скрипте это последняя строка. В веб-приложении клиент живёт всё время работы сервиса, и закрывать его вручную не нужно.

§9Частые ошибки

Что происходитЧто видноВ чём дело
Сервер не запущенServerSelectionTimeoutError: localhost:27017: [Errno 61] Connection refusedАдрес верный, но по нему никто не отвечает. Смотреть надо на сервер: запущен ли он и на том ли порту
Опечатка в имени хостаServerSelectionTimeoutError после 30 секунд ожиданияИмя не разрешилось в адрес. Отличается от предыдущего тем, что ответа нет вовсе, а не «отказ»
Опечатка в имени коллекцииОшибки нет, результат пустойКоллекция создаётся при первой записи, поэтому обращение к несуществующей — законная операция
col.count()AttributeError: 'Collection' object has no attribute 'count'Метод убран из драйвера. Считает count_documents({}), и фильтр обязателен
Клиент создан внутри циклаПрограмма работает, но с каждым шагом медленнееНа каждой итерации поднимается новый пул соединений. Клиент создаётся один раз
Что происходитЧто видноВ чём дело
Сервер не запущенNo suitable servers found: [connection refused calling hello]Адрес верный, но по нему никто не отвечает. Смотреть надо на сервер: запущен ли он и на том ли порту
mongocxx::instance не созданПрограмма падает при первом обращении к драйверуЭкземпляр создаётся ровно один раз за время жизни программы и раньше любого клиента
Два mongocxx::instancecannot create a mongocxx::instance object if one has already been createdЭкземпляр в программе один; клиентов при этом может быть сколько угодно
Опечатка в имени коллекцииОшибки нет, результат пустойКоллекция создаётся при первой записи, поэтому обращение к несуществующей — законная операция
Клиент создан внутри функцииРаботает, но каждый вызов поднимает новый пул соединенийКлиент живёт рядом с приложением; для многопоточности берут mongocxx::pool
Что происходитЧто видноВ чём дело
Сервер не запущенserver selection error: context deadline exceeded, current topology: { Type: Unknown …Connect ошибки не вернёт: соединение ленивое. Проверять надо сервер
Ошибка Connect не проверенаПрограмма идёт дальше с нерабочим клиентомВ Go ошибка возвращается значением, и проверяют её сразу
Забыт defer client.Disconnect(ctx)Соединения остаются открытыми до конца процессаКлиент закрывают там же, где создали
Опечатка в имени коллекцииОшибки нет, результат пустойКоллекция создаётся при первой записи, поэтому обращение к несуществующей — законная операция
mongo.Connect в каждом обработчикеРаботает, но с каждым запросом медленнееКлиент создаётся один раз при старте и живёт всё время работы сервиса
Что происходитЧто видноВ чём дело
Сервер не запущенMongo::Error::NoServerAvailable: No primary_preferred server is available in clusterАдрес верный, но по нему никто не отвечает. Смотреть надо на сервер: запущен ли он и на том ли порту
Константа названа URITypeError: «mongodb://…» is not a class/moduleURI — модуль стандартной библиотеки. Константу называют иначе, например SERVER
Опечатка в имени коллекцииОшибки нет, результат пустойКоллекция создаётся при первой записи, поэтому обращение к несуществующей — законная операция
Логи драйвера в консолиКаждая операция печатает строку D, [timestamp] DEBUGMongo::Logger.logger.level = Logger::WARN в начале программы
Клиент создан внутри методаРаботает, но каждый вызов поднимает новый пул соединенийКлиент создаётся один раз; закрывают его client.close в конце программы

§10Проверьте себя

Compass закрыт. Что произошло с данными?

Ничего. Compass — клиент, он только показывает то, что лежит на сервере. Данные исчезнут, если удалить их запросом или удалить хранилище сервера, но не от закрытия окна.

Строка client = MongoClient("mongodb://localhost:27017/") выполнилась без ошибки. Значит ли это, что сервер доступен?

Нет. Клиент соединяется лениво: адрес запомнен, но проверка произойдёт только на первом запросе к данным. Поэтому ServerSelectionTimeoutError вы увидите на строке с count_documents или find, а не на создании клиента.

Почему в списке баз есть admin, config и local, хотя мы их не создавали?

Их заводит сам сервер. admin хранит пользователей и роли, config — внутренние настройки, local — данные, которые не покидают эту машину. Работать с ними напрямую не нужно.

Что вернёт db["prodcuts"].count_documents({}) — с опечаткой в имени?

0, и никакой ошибки. Для сервера это обращение к ещё не созданной коллекции; ошибкой оно не считается. Поэтому пустой результат проверяют на опечатку в имени: сверяют его с list_collection_names().

Чем shop.products отличается от products?

Первое — полное имя: коллекция products в базе shop. Второе — имя внутри уже выбранной базы. В mongosh текущая база выбирается командой use shop, после чего к коллекции обращаются как db.products.

§11Шпаргалка

Подключиться from pymongo import MongoClient client = MongoClient( "mongodb://localhost:27017/")
Спуститься к коллекции db = client["shop"] col = db["products"] # или client["shop"]["products"]
Осмотреться client.list_database_names() db.list_collection_names() col.count_documents({})
То же в оболочке show dbs use shop show collections
Подключиться #include <mongocxx/client.hpp> mongocxx::instance instance{}; mongocxx::client client{ mongocxx::uri{"mongodb://localhost:27017"}};
Спуститься к коллекции auto db = client["shop"]; auto col = db["products"]; // или client["shop"]["products"]
Осмотреться client.list_database_names() db.list_collection_names() col.count_documents(make_document())
Собрать g++ -std=c++17 main.cpp \ $(pkg-config --cflags --libs \ libmongocxx1)
Подключиться import "go.mongodb.org/mongo-driver/v2/mongo" client, err := mongo.Connect( options.Client().ApplyURI( "mongodb://localhost:27017"))
Спуститься к коллекции db := client.Database("shop") col := db.Collection("products")
Осмотреться client.ListDatabaseNames(ctx, bson.D{}) db.ListCollectionNames(ctx, bson.D{}) col.CountDocuments(ctx, bson.D{})
Поставить драйвер go get go.mongodb.org/\ mongo-driver/v2/mongo
Подключиться require "mongo" client = Mongo::Client.new( "mongodb://localhost:27017/")
Спуститься к коллекции db = client.use("shop").database col = db[:products]
Осмотреться client.database_names db.collection_names col.count_documents({})
Поставить драйвер gem install mongo # Mongo::Logger.logger.level = # Logger::WARN

Итог главы

Сервер держит базы, база — коллекции, коллекция — документы; клиент только спрашивает. В программе путь до данных — это три строки: клиент, база, коллекция, — и ни одна из них ещё не обращается к серверу. База и коллекция появляются сами при первой записи, поэтому при опечатке в имени запрос возвращает пустой результат без ошибки. Дальше разберём, что именно лежит в коллекции: как устроен документ и почему он хранится не как текст.

Глава 1.1 · Сервер, база, коллекция · Справочник по MongoDB Автор: Макаров Максим Николаевич