Справочник по MongoDB · модуль 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, а также что должна напечатать каждая программа.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.cpphello. Как поставить драйвер — глава 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+srv://.localhost — «эта же машина». В рабочем проекте здесь будет имя хоста или адрес в сети.В настоящем проекте в строку добавляются логин и пароль: mongodb://user:secret@db.example.com:27017. На стенде курса их нет: сервер поднят без проверки подлинности. Это допустимо, потому что он стоит на вашей машине и наружу не смотрит.
§4MongoClient: создание клиента
В Python работа начинается с объекта MongoClient: это клиент, живущий внутри вашей программы.
from pymongo import MongoClient # Локальный сервер стенда курса client = MongoClient("mongodb://localhost:27017/") # Облачный Atlas — другая схема и обязательный пароль # client = MongoClient("mongodb+srv://user:pass@cluster0.mongodb.net/")
#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"}
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")
require "mongo" # Локальный сервер стенда курса client = Mongo::Client.new("mongodb://localhost:27017/") # Облачный Atlas — другая схема и обязательный пароль # client = Mongo::Client.new("mongodb+srv://user:pass@cluster0.mongodb.net/")
# то же подключение из терминала mongosh "mongodb://localhost:27017" # внутри оболочки клиент уже создан, # текущая база доступна как db
Строка MongoClient(...) выполнится мгновенно даже при выключенном сервере: клиент только запоминает адрес, а соединяется лениво — при первом настоящем запросе. Поэтому ошибка «сервер недоступен» появляется не на создании клиента, а на первой операции с данными.
§5Выбор базы и коллекции
От клиента спускаемся на уровень ниже — к базе, затем к коллекции. В 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__)
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;
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)
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Что уже есть на сервере
Спросим у сервера, какие базы он держит, а у базы — какие в ней коллекции. Это первое, что делают, подключившись к незнакомой базе.
print("Базы данных:", client.list_database_names()) db = client["shop"] print("Коллекции shop:", db.list_collection_names()) collection = db["products"] print("Документов:", collection.count_documents({}))
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;
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)
puts "Базы данных: #{client.database_names}" db = client.use("shop").database puts "Коллекции shop: #{db.collection_names}" collection = db[:products] puts "Документов: #{collection.count_documents({})}"
show dbs
use shop
show collections
db.products.countDocuments({})
Базы данных: ['admin', 'config', 'hh', 'local', 'logs', 'org', 'sandbox', 'shop'] Коллекции shop: ['customers', 'products', 'orders'] Документов: 21
Базы данных: admin config hh local logs org sandbox shop Коллекции shop: products customers orders Документов: 21
Базы данных: [admin config hh local logs org sandbox shop] Коллекции shop: [products orders customers] Документов: 21
Базы данных: ["admin", "config", "hh", "local", "logs", "org", "sandbox", "shop"] Коллекции shop: ["customers", "orders", "products"] Документов: 21
Пять баз наши: shop, hh, logs, org и песочница sandbox. Ещё три сервер завёл сам, и трогать их не нужно:
Порядок в ответе произвольный: коллекции пришли не по алфавиту и не в порядке создания — у разных драйверов он и выглядит по-разному. Сервер порядок в этом списке не гарантирует; нужен алфавит — сортируйте сами.
§7Тот же осмотр в Compass
Всё, что мы сейчас получили тремя вызовами, Compass показывает одним экраном. Слева — список баз, у каждой раскрывается список коллекций, справа — размер и число документов.
На кадре есть то, чего нет в выводе Python. У каждой базы указаны размер хранения, объём данных, число коллекций и число индексов. На кадре видна разница между двумя базами: logs — одна коллекция и 207.02 kB данных, shop — три коллекции и 79.32 kB. Документов в shop больше по числу коллекций, но данных втрое меньше: объём зависит не от количества документов, а от того, что внутри них.
Отдельно стоит база hh: 37.72 MB и десять коллекций против четырёх, которые мы перечислили. В ней остались коллекции на 20 000 документов с прошлых занятий.
shop — три коллекции, 21 товар, 120 заказов, 20 покупателей§8Один клиент на программу
MongoClient создаётся один раз на программу. Внутри него — пул соединений: несколько открытых каналов к серверу, которые переиспользуются между запросами. Создавать клиента в каждой функции — значит каждый раз поднимать этот пул заново.
# создаётся один раз при старте программы client = MongoClient("mongodb://localhost:27017/") products = client["shop"]["products"] def count_all(): return products.count_documents({})
// один пул на программу: клиент живёт рядом с приложением 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()); }
// создаётся один раз при старте программы 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{}) }
# создаётся один раз при старте программы CLIENT = Mongo::Client.new("mongodb://localhost:27017/") PRODUCTS = CLIENT.use("shop").database[:products] def count_all PRODUCTS.count_documents({}) end
def count_all(): # новый пул соединений на каждый вызов client = MongoClient("mongodb://localhost:27017/") return client["shop"]["products"].count_documents({})
auto count_all() { // новый клиент и новый пул на каждый вызов mongocxx::client client{mongocxx::uri{"mongodb://localhost:27017"}}; return client["shop"]["products"] .count_documents(make_document()); }
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 }
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::instance | cannot 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 | Адрес верный, но по нему никто не отвечает. Смотреть надо на сервер: запущен ли он и на том ли порту |
Константа названа URI | TypeError: «mongodb://…» is not a class/module | URI — модуль стандартной библиотеки. Константу называют иначе, например SERVER |
| Опечатка в имени коллекции | Ошибки нет, результат пустой | Коллекция создаётся при первой записи, поэтому обращение к несуществующей — законная операция |
| Логи драйвера в консоли | Каждая операция печатает строку D, [timestamp] DEBUG | Mongo::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
Итог главы
Сервер держит базы, база — коллекции, коллекция — документы; клиент только спрашивает. В программе путь до данных — это три строки: клиент, база, коллекция, — и ни одна из них ещё не обращается к серверу. База и коллекция появляются сами при первой записи, поэтому при опечатке в имени запрос возвращает пустой результат без ошибки. Дальше разберём, что именно лежит в коллекции: как устроен документ и почему он хранится не как текст.