Справочник по MongoDB · модуль 1 · подключение и первые данные
1.9
Скрипт целиком:
отчёт по магазину
В главах модуля каждый пример показывал одну операцию. Здесь собрана готовая программа: она подключается к серверу, читает данные девятью запросами, меняет документы в песочнице и печатает отчёт, который можно показать заказчику. Глава разбирает скрипт по частям и приводит его целиком вместе с выводом.
- Категория
- Готовая программа
- Уровень
- Итоговая глава модуля
- Связанные темы
- Все главы модуля 1, практическая работа ПР-01
- Зачем это знать
- Собрать из отдельных операций работающую программу
Как запускать примеры этой главы
docker compose run --rm reset или скрипт из главы 1.0, §6.mongodb-glava-1.9-python.zip, 58 КБmongodb-glava-1.9-ruby.zip, 58 КБmongodb-glava-1.9-go.zip, 61 КБmongodb-glava-1.9-cpp.zip, 60 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.lessons/ch1_9.py — пример вставляется в конец файлаlessons/ch1_9.rb — пример вставляется в конец файлаlessons/ch1_9.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch1_9.cpp — пример внутрь скобок в конце main, функции — выше maincd lessons, затем python ch1_9.pycd lessons, затем python3 ch1_9.py · в Docker: docker compose run --rm python lessons/ch1_9.pycd lessons, затем ruby ch1_9.rb · в Docker: docker compose run --rm ruby lessons/ch1_9.rbcd lessons, затем go run ch1_9.go · в Docker: docker compose run --rm go lessons/ch1_9.gocd lessons, затем g++ -std=c++17 ch1_9.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch1_9.cpp§1Что делает скрипт
Скрипт shop_report.py собирает отчёт по интернет-магазину: сколько в базе товаров и заказов, какие товары самые дорогие, как выглядит каталог по категориям, что лежит в одном заказе. В конце он проделывает в песочнице полный цикл записи — вставку, обновление и удаление — и печатает, сколько документов затронул каждый запрос.
Отчёт разбит на девять разделов, и каждый показывает свою операцию из модуля 1.
| Раздел отчёта | Что показывает | Глава |
|---|---|---|
| 1. Связь и база shop | подключение, список баз и коллекций, счётчики документов | 1.1 |
| 2. Один товар целиком | find_one и поля документа всех типов | 1.2, 1.4 |
| 3. Пять самых дорогих товаров | проекция, сортировка, предел | 1.5, 1.6 |
| 4. Каталог по категориям | сортировка по двум полям | 1.6 |
| 5. Товары по категориям | distinct и count_documents с фильтром | 1.4, 1.6 |
| 6. Заказы страницами | skip и limit | 1.6 |
| 7. Заказ с доставкой и позициями | вложенные документы и массивы в выводе | 1.2 |
| 8. Песочница | insert_many, update_one, update_many, delete_one | 1.3, 1.7, 1.8 |
| 9. Итог | числа, которые попадают в отчёт | — |
Скрипт лежит в репозитории практик, в папке scripts, и запускается одной командой (§7). Главу читают вместе с открытым файлом.
§2Подключение и разделы отчёта
Начало скрипта — то же, что в главе 1.1: клиент, база, коллекции. Адрес сервера берётся из переменной окружения MONGO_URI, а если её нет — подставляется localhost. Так один и тот же файл работает и на компьютере, и в контейнере.
Рядом объявлена функция title: она печатает заголовок раздела и подчёркивает его. Отчёт читает человек, и разделы должны быть видны с первого взгляда.
import os from pymongo import MongoClient client = MongoClient(os.environ.get("MONGO_URI", "mongodb://localhost:27017/")) shop = client["shop"] products = shop["products"] orders = shop["orders"] box = client["sandbox"]["products"] def title(text): """Заголовок раздела: отчёт читают глазами, разделы видно сразу.""" print("\n" + text) print("─" * len(text))
#include <algorithm> #include <chrono> #include <cstdlib> #include <ctime> #include <iomanip> #include <iostream> #include <sstream> #include <string> #include <vector> #include <bsoncxx/builder/basic/array.hpp> #include <bsoncxx/builder/basic/document.hpp> #include <bsoncxx/builder/basic/kvp.hpp> #include <bsoncxx/json.hpp> #include <bsoncxx/types.hpp> #include <mongocxx/client.hpp> #include <mongocxx/instance.hpp> #include <mongocxx/options/find.hpp> #include <mongocxx/uri.hpp> using bsoncxx::builder::basic::kvp; using bsoncxx::builder::basic::make_document; // Ширина столбца считается по символам: в кириллице на букву приходится // два байта, и обычный setw сдвинул бы столбцы. int simvolov(const std::string& text) { int n = 0; for (unsigned char c : text) if ((c & 0xC0) != 0x80) ++n; return n; } std::string pad(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? text + std::string(add, ' ') : text; } std::string right(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? std::string(add, ' ') + text : text; } void title(const std::string& text) { std::cout << "\n" << text << "\n"; for (int i = 0; i < simvolov(text); ++i) std::cout << "─"; std::cout << std::endl; } // Значение любого типа BSON в строку — для карточки документа. std::string znachenie(const bsoncxx::document::element& field) { switch (field.type()) { case bsoncxx::type::k_string: return std::string(field.get_string().value); case bsoncxx::type::k_int32: return std::to_string(field.get_int32().value); case bsoncxx::type::k_int64: return std::to_string(field.get_int64().value); case bsoncxx::type::k_double: { std::ostringstream out; out << field.get_double().value; // без лишних нулей: 4.2, а не 4.200000 return out.str(); } case bsoncxx::type::k_bool: return field.get_bool().value ? "true" : "false"; case bsoncxx::type::k_date: { std::time_t sekundy = field.get_date().to_int64() / 1000; std::ostringstream out; out << std::put_time(std::gmtime(&sekundy), "%Y-%m-%d %H:%M:%S"); return out.str(); } case bsoncxx::type::k_document: return bsoncxx::to_json(field.get_document().value); case bsoncxx::type::k_array: return bsoncxx::to_json(field.get_array().value); default: { // to_json берёт документ или массив, поэтому значение заворачиваем std::string json = bsoncxx::to_json(make_document(kvp("v", field.get_value())).view()); return json.substr(json.find(':') + 2, json.rfind('}') - json.find(':') - 3); } } } int main() { mongocxx::instance instance{}; const char* env = std::getenv("MONGO_URI"); mongocxx::client client{mongocxx::uri{env ? env : "mongodb://localhost:27017"}}; auto shop = client["shop"]; auto products = shop["products"]; auto orders = shop["orders"]; auto box = client["sandbox"]["products"];
package main import ( "context" "fmt" "log" "os" "sort" "strings" "unicode/utf8" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) // Product — поля товара, которые читает отчёт. type Product struct { ID string `bson:"_id"` Title string `bson:"title"` Category string `bson:"category"` Price int `bson:"price"` } // Order — заказ вместе с вложенными документами и массивом позиций. type Order struct { Number string `bson:"number"` Status string `bson:"status"` Total int `bson:"total"` Delivery struct { City string `bson:"city"` Type string `bson:"type"` Days int `bson:"days"` } `bson:"delivery"` Payment struct { Method string `bson:"method"` Paid bool `bson:"paid"` } `bson:"payment"` Items []struct { Title string `bson:"title"` Qty int `bson:"qty"` Price int `bson:"price"` } `bson:"items"` } // pad дополняет строку пробелами по числу символов: в кириллице байт больше, // чем букв, и обычное %-32s сдвигает столбцы. func pad(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return text + strings.Repeat(" ", width-n) } return text } func right(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return strings.Repeat(" ", width-n) + text } return text } func title(text string) { fmt.Println("\n" + text) fmt.Println(strings.Repeat("─", utf8.RuneCountInString(text))) } func main() { uri := os.Getenv("MONGO_URI") if uri == "" { uri = "mongodb://localhost:27017" } client, err := mongo.Connect(options.Client().ApplyURI(uri)) if err != nil { log.Fatal(err) } ctx := context.Background() defer client.Disconnect(ctx) shop := client.Database("shop") products := shop.Collection("products") orders := shop.Collection("orders") box := client.Database("sandbox").Collection("products")
require "mongo" Mongo::Logger.logger.level = Logger::WARN client = Mongo::Client.new(ENV.fetch("MONGO_URI", "mongodb://localhost:27017/")) shop = client.use("shop").database products = shop[:products] orders = shop[:orders] box = client.use("sandbox").database[:products] def title(text) puts "\n" + text puts "─" * text.length end def pad(text, width) # ljust по видимым символам, как в других языках text.to_s.ljust(width) end
Коллекции открыты заранее и названы по смыслу: products, orders, box — песочница. Дальше в скрипте нет ни одной строки с именем базы: все запросы идут через эти переменные.
§3Чтение: справка, карточка, таблицы
Первый раздел отчёта отвечает на вопрос «что вообще есть на сервере»: проверяет связь, перечисляет базы и коллекции и считает документы. Второй печатает один товар целиком — по полям, как они лежат в документе.
# ── 1. Что лежит на сервере ─────────────────────────────────────────────── title("1. Связь и база shop") client.admin.command("ping") # первое обращение к серверу print("базы на сервере:", ", ".join(sorted(client.list_database_names()))) print("коллекции shop:", ", ".join(sorted(shop.list_collection_names()))) print(f"товаров {products.count_documents({})}, " f"заказов {orders.count_documents({})}, " f"покупателей {shop['customers'].count_documents({})}") # ── 2. Карточка одного документа ────────────────────────────────────────── title("2. Один товар целиком") tovar = products.find_one({"_id": "p-001"}) for field, value in tovar.items(): print(f" {field:10} {value}")
// ── 1. Что лежит на сервере ─────────────────────────────────────── title("1. Связь и база shop"); client["admin"].run_command(make_document(kvp("ping", 1))); auto bases = client.list_database_names(); std::sort(bases.begin(), bases.end()); std::cout << "базы на сервере: "; for (size_t i = 0; i < bases.size(); ++i) std::cout << (i ? ", " : "") << bases[i]; std::cout << std::endl; auto names = shop.list_collection_names(); std::sort(names.begin(), names.end()); std::cout << "коллекции shop: "; for (size_t i = 0; i < names.size(); ++i) std::cout << (i ? ", " : "") << names[i]; std::cout << std::endl; auto vsego_tovarov = products.count_documents(make_document()); auto vsego_zakazov = orders.count_documents(make_document()); std::cout << "товаров " << vsego_tovarov << ", заказов " << vsego_zakazov << ", покупателей " << shop["customers"].count_documents(make_document()) << std::endl; // ── 2. Карточка одного документа ────────────────────────────────── title("2. Один товар целиком"); auto tovar = products.find_one(make_document(kvp("_id", "p-001"))); for (auto&& field : tovar->view()) std::cout << " " << pad(std::string(field.key()), 10) << " " << znachenie(field) << std::endl;
// ── 1. Что лежит на сервере ─────────────────────────────────────── title("1. Связь и база shop") if err := client.Ping(ctx, nil); err != nil { log.Fatal(err) } bases, _ := client.ListDatabaseNames(ctx, bson.D{}) sort.Strings(bases) fmt.Println("базы на сервере:", strings.Join(bases, ", ")) names, _ := shop.ListCollectionNames(ctx, bson.D{}) sort.Strings(names) fmt.Println("коллекции shop:", strings.Join(names, ", ")) vsegoTovarov, _ := products.CountDocuments(ctx, bson.D{}) vsegoZakazov, _ := orders.CountDocuments(ctx, bson.D{}) vsegoKlientov, _ := shop.Collection("customers").CountDocuments(ctx, bson.D{}) fmt.Printf("товаров %d, заказов %d, покупателей %d\n", vsegoTovarov, vsegoZakazov, vsegoKlientov) // ── 2. Карточка одного документа ────────────────────────────────── title("2. Один товар целиком") var tovar bson.D products.FindOne(ctx, bson.D{{Key: "_id", Value: "p-001"}}).Decode(&tovar) for _, field := range tovar { fmt.Printf(" %s %v\n", pad(field.Key, 10), field.Value) }
# ── 1. Что лежит на сервере ─────────────────────────────────────────────── title("1. Связь и база shop") client.use("admin").database.command(ping: 1) puts "базы на сервере: " + client.database_names.sort.join(", ") puts "коллекции shop: " + shop.collection_names.sort.join(", ") puts "товаров #{products.count_documents({})}, " \ "заказов #{orders.count_documents({})}, " \ "покупателей #{shop[:customers].count_documents({})}" # ── 2. Карточка одного документа ────────────────────────────────────────── title("2. Один товар целиком") tovar = products.find({ "_id" => "p-001" }).first tovar.each { |field, value| puts " #{pad(field, 10)} #{value}" }
В карточке видно всё, что разбиралось в главе 1.2: строки, число, дробное число, массив строк specs, массив документов stock и дата added. Драйвер отдаёт их значениями языка, поэтому печатать их можно как обычные переменные.
Третий раздел — таблица. Проекция оставляет три поля, сортировка ставит дорогие товары наверх, предел обрезает список до пяти строк. Заголовок таблицы и строки печатаются с одинаковыми ширинами столбцов, поэтому числа стоят в столбик.
# ── 3. Таблица: пять самых дорогих товаров ──────────────────────────────── title("3. Пять самых дорогих товаров") print(f" {'название':32} {'категория':14} {'цена':>8}") cursor = (products.find({}, {"_id": 0, "title": 1, "category": 1, "price": 1}) .sort("price", -1) .limit(5)) for doc in cursor: print(f" {doc['title']:32} {doc['category']:14} {doc['price']:>8}") # ── 4. Сортировка по двум полям ─────────────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым") katalog = (products.find({}, {"_id": 0, "category": 1, "title": 1, "price": 1}) .sort([("category", 1), ("price", -1)]) .limit(8)) for doc in katalog: print(f" {doc['category']:14} {doc['title']:32} {doc['price']:>8}")
// ── 3. Таблица: пять самых дорогих товаров ──────────────────────── title("3. Пять самых дорогих товаров"); std::cout << " " << pad("название", 32) << " " << pad("категория", 14) << " " << right("цена", 8) << std::endl; mongocxx::options::find options; options.projection(make_document(kvp("_id", 0), kvp("title", 1), kvp("category", 1), kvp("price", 1))); options.sort(make_document(kvp("price", -1))); options.limit(5); for (auto&& doc : products.find(make_document(), options)) { std::cout << " " << pad(std::string(doc["title"].get_string().value), 32) << " " << pad(std::string(doc["category"].get_string().value), 14) << " " << right(std::to_string(doc["price"].get_int32().value), 8) << std::endl; } // ── 4. Сортировка по двум полям ─────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым"); mongocxx::options::find katalog_options; katalog_options.projection(make_document(kvp("_id", 0), kvp("category", 1), kvp("title", 1), kvp("price", 1))); katalog_options.sort(make_document(kvp("category", 1), kvp("price", -1))); katalog_options.limit(8); for (auto&& doc : products.find(make_document(), katalog_options)) { std::cout << " " << pad(std::string(doc["category"].get_string().value), 14) << " " << pad(std::string(doc["title"].get_string().value), 32) << " " << right(std::to_string(doc["price"].get_int32().value), 8) << std::endl; }
// ── 3. Таблица: пять самых дорогих товаров ──────────────────────── title("3. Пять самых дорогих товаров") fmt.Printf(" %s %s %s\n", pad("название", 32), pad("категория", 14), right("цена", 8)) cursor, _ := products.Find(ctx, bson.D{}, options.Find(). SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "title", Value: 1}, {Key: "category", Value: 1}, {Key: "price", Value: 1}}). SetSort(bson.D{{Key: "price", Value: -1}}). SetLimit(5)) var dorogie []Product cursor.All(ctx, &dorogie) for _, p := range dorogie { fmt.Printf(" %s %s %s\n", pad(p.Title, 32), pad(p.Category, 14), right(fmt.Sprint(p.Price), 8)) } // ── 4. Сортировка по двум полям ─────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым") cursor, _ = products.Find(ctx, bson.D{}, options.Find(). SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "category", Value: 1}, {Key: "title", Value: 1}, {Key: "price", Value: 1}}). SetSort(bson.D{{Key: "category", Value: 1}, {Key: "price", Value: -1}}). SetLimit(8)) var katalog []Product cursor.All(ctx, &katalog) for _, p := range katalog { fmt.Printf(" %s %s %s\n", pad(p.Category, 14), pad(p.Title, 32), right(fmt.Sprint(p.Price), 8)) }
# ── 3. Таблица: пять самых дорогих товаров ──────────────────────────────── title("3. Пять самых дорогих товаров") puts " #{pad('название', 32)} #{pad('категория', 14)} #{'цена'.rjust(8)}" products.find({}, projection: { "_id" => 0, "title" => 1, "category" => 1, "price" => 1 }) .sort({ "price" => -1 }) .limit(5) .each do |doc| puts " #{pad(doc['title'], 32)} #{pad(doc['category'], 14)} #{doc['price'].to_s.rjust(8)}" end # ── 4. Сортировка по двум полям ─────────────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым") products.find({}, projection: { "_id" => 0, "category" => 1, "title" => 1, "price" => 1 }) .sort({ "category" => 1, "price" => -1 }) .limit(8) .each do |doc| puts " #{pad(doc['category'], 14)} #{pad(doc['title'], 32)} #{doc['price'].to_s.rjust(8)}" end
Четвёртый раздел сортирует по двум полям сразу: категории по алфавиту, внутри категории — от дорогих к дешёвым. Это тот же sort из главы 1.6, только с двумя ключами, и порядок ключей задаёт порядок сравнения.
§4Страницы и вложенные поля
Пятый раздел считает товары по категориям: сначала distinct даёт список категорий, затем на каждую идёт count_documents с фильтром. Шестой выводит заказы страницами по пять — skip отсчитывает страницу, limit ограничивает её размер.
# ── 5. Счётчики по категориям ───────────────────────────────────────────── title("5. Товары по категориям") for category in sorted(products.distinct("category")): print(f" {category:14} {products.count_documents({'category': category}):2} шт") # ── 6. Страницы заказов ─────────────────────────────────────────────────── title("6. Заказы страницами по 5") for page in (1, 2): print(f" страница {page}") stranica = (orders.find({"status": "доставлен"}, {"_id": 0, "number": 1, "total": 1}) .sort("number", 1) .skip((page - 1) * 5) .limit(5)) for doc in stranica: print(f" {doc['number']} {doc['total']:>7}")
// ── 5. Счётчики по категориям ───────────────────────────────────── title("5. Товары по категориям"); std::vector<std::string> kategorii; for (auto&& doc : products.distinct("category", make_document())) for (auto&& value : doc["values"].get_array().value) kategorii.push_back(std::string(value.get_string().value)); std::sort(kategorii.begin(), kategorii.end()); for (const auto& category : kategorii) { auto skolko = products.count_documents(make_document(kvp("category", category))); std::cout << " " << pad(category, 14) << " " << right(std::to_string(skolko), 2) << " шт" << std::endl; } // ── 6. Страницы заказов ─────────────────────────────────────────── title("6. Заказы страницами по 5"); for (int page = 1; page <= 2; ++page) { std::cout << " страница " << page << std::endl; mongocxx::options::find page_options; page_options.projection(make_document(kvp("_id", 0), kvp("number", 1), kvp("total", 1))); page_options.sort(make_document(kvp("number", 1))); page_options.skip((page - 1) * 5); page_options.limit(5); for (auto&& doc : orders.find(make_document(kvp("status", "доставлен")), page_options)) { std::cout << " " << std::string(doc["number"].get_string().value) << " " << right(std::to_string(doc["total"].get_int32().value), 7) << std::endl; } }
// ── 5. Счётчики по категориям ───────────────────────────────────── title("5. Товары по категориям") var spisok []string products.Distinct(ctx, "category", bson.D{}).Decode(&spisok) sort.Strings(spisok) for _, category := range spisok { skolko, _ := products.CountDocuments(ctx, bson.D{{Key: "category", Value: category}}) fmt.Printf(" %s %s шт\n", pad(category, 14), right(fmt.Sprint(skolko), 2)) } // ── 6. Страницы заказов ─────────────────────────────────────────── title("6. Заказы страницами по 5") for page := int64(1); page <= 2; page++ { fmt.Printf(" страница %d\n", page) cursor, _ = orders.Find(ctx, bson.D{{Key: "status", Value: "доставлен"}}, options.Find(). SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "number", Value: 1}, {Key: "total", Value: 1}}). SetSort(bson.D{{Key: "number", Value: 1}}). SetSkip((page - 1) * 5). SetLimit(5)) var stranica []Order cursor.All(ctx, &stranica) for _, o := range stranica { fmt.Printf(" %s %s\n", o.Number, right(fmt.Sprint(o.Total), 7)) } }
# ── 5. Счётчики по категориям ───────────────────────────────────────────── title("5. Товары по категориям") products.distinct("category").sort.each do |category| puts " #{pad(category, 14)} #{products.count_documents({ 'category' => category }).to_s.rjust(2)} шт" end # ── 6. Страницы заказов ─────────────────────────────────────────────────── title("6. Заказы страницами по 5") [1, 2].each do |page| puts " страница #{page}" orders.find({ "status" => "доставлен" }, projection: { "_id" => 0, "number" => 1, "total" => 1 }) .sort({ "number" => 1 }) .skip((page - 1) * 5) .limit(5) .each { |doc| puts " #{doc['number']} #{doc['total'].to_s.rjust(7)}" } end
Сортировка по номеру заказа здесь обязательна: без неё сервер вправе вернуть документы в любом порядке, и на второй странице мог бы оказаться тот же заказ, что на первой (глава 1.6).
Седьмой раздел печатает один заказ так, как его показывают в интерфейсе: номер и сумма, строка доставки из вложенного документа, способ оплаты и список позиций из массива документов.
# ── 7. Вложенные поля и массивы в выводе ────────────────────────────────── title("7. Заказ с доставкой и позициями") zakaz = orders.find_one({"_id": "o-0001"}) print(f" номер {zakaz['number']}, статус {zakaz['status']}, сумма {zakaz['total']}") print(f" доставка: {zakaz['delivery']['city']}, {zakaz['delivery']['type']}, " f"{zakaz['delivery']['days']} дн.") print(f" оплата: {zakaz['payment']['method']}, проведена: {zakaz['payment']['paid']}") for position in zakaz["items"]: print(f" {position['qty']} × {position['title']:34} {position['price']:>7}")
// ── 7. Вложенные поля и массивы в выводе ────────────────────────── title("7. Заказ с доставкой и позициями"); // Документ сохраняем в переменную: view указывает внутрь неё, и временное // значение из find_one исчезло бы сразу после строки. auto zakaz_doc = orders.find_one(make_document(kvp("_id", "o-0001"))); auto zakaz = zakaz_doc->view(); std::cout << " номер " << std::string(zakaz["number"].get_string().value) << ", статус " << std::string(zakaz["status"].get_string().value) << ", сумма " << zakaz["total"].get_int32().value << std::endl; auto dostavka = zakaz["delivery"].get_document().value; std::cout << " доставка: " << std::string(dostavka["city"].get_string().value) << ", " << std::string(dostavka["type"].get_string().value) << ", " << dostavka["days"].get_int32().value << " дн." << std::endl; auto oplata = zakaz["payment"].get_document().value; std::cout << " оплата: " << std::string(oplata["method"].get_string().value) << ", проведена: " << (oplata["paid"].get_bool().value ? "true" : "false") << std::endl; for (auto&& position : zakaz["items"].get_array().value) { auto item = position.get_document().value; std::cout << " " << item["qty"].get_int32().value << " × " << pad(std::string(item["title"].get_string().value), 34) << " " << right(std::to_string(item["price"].get_int32().value), 7) << std::endl; }
// ── 7. Вложенные поля и массивы в выводе ────────────────────────── title("7. Заказ с доставкой и позициями") var zakaz Order orders.FindOne(ctx, bson.D{{Key: "_id", Value: "o-0001"}}).Decode(&zakaz) fmt.Printf(" номер %s, статус %s, сумма %d\n", zakaz.Number, zakaz.Status, zakaz.Total) fmt.Printf(" доставка: %s, %s, %d дн.\n", zakaz.Delivery.City, zakaz.Delivery.Type, zakaz.Delivery.Days) fmt.Printf(" оплата: %s, проведена: %t\n", zakaz.Payment.Method, zakaz.Payment.Paid) for _, position := range zakaz.Items { fmt.Printf(" %d × %s %s\n", position.Qty, pad(position.Title, 34), right(fmt.Sprint(position.Price), 7)) }
# ── 7. Вложенные поля и массивы в выводе ────────────────────────────────── title("7. Заказ с доставкой и позициями") zakaz = orders.find({ "_id" => "o-0001" }).first puts " номер #{zakaz['number']}, статус #{zakaz['status']}, сумма #{zakaz['total']}" puts " доставка: #{zakaz['delivery']['city']}, #{zakaz['delivery']['type']}, " \ "#{zakaz['delivery']['days']} дн." puts " оплата: #{zakaz['payment']['method']}, проведена: #{zakaz['payment']['paid']}" zakaz["items"].each do |position| puts " #{position['qty']} × #{pad(position['title'], 34)} #{position['price'].to_s.rjust(7)}" end
К вложенному документу обращаются как к обычному значению: zakaz["delivery"]["city"]. Массив items перебирается циклом, и каждая позиция — тоже документ со своими полями.
§5Запись в песочницу
Восьмой раздел меняет данные, поэтому работает в песочнице — базе sandbox (глава 1.3). База shop остаётся нетронутой. Сначала он удаляет свои товары, если они остались от прошлого запуска: скрипт должен давать один и тот же результат при каждом запуске.
# ── 8. Запись: вставка, обновление, удаление ────────────────────────────── title("8. Песочница: вставка, обновление, удаление") for key in ("p-301", "p-302"): # чтобы скрипт можно было запускать повторно box.delete_one({"_id": key}) added = box.insert_many([ {"_id": "p-301", "title": "Док-станция USB-C", "category": "аксессуары", "price": 6490, "reviews": 0}, {"_id": "p-302", "title": "Кабель HDMI 2 м", "category": "аксессуары", "price": 890, "reviews": 0}, ]) print(" вставлено:", len(added.inserted_ids), "ключи:", ", ".join(added.inserted_ids)) changed = box.update_one({"_id": "p-301"}, {"$set": {"price": 5990}, "$inc": {"reviews": 1}}) print(f" обновление одного: найдено {changed.matched_count}, изменено {changed.modified_count}") many = box.update_many({"category": "аксессуары"}, {"$set": {"sale": True}}) print(f" пометка распродажи: найдено {many.matched_count}, изменено {many.modified_count}") after = box.find_one({"_id": "p-301"}, {"_id": 0, "title": 1, "price": 1, "reviews": 1, "sale": 1}) print(" что стало:", after) gone = box.delete_one({"_id": "p-302"}) print(" удалено:", gone.deleted_count, "· товаров в песочнице:", box.count_documents({}))
// ── 8. Запись: вставка, обновление, удаление ────────────────────── title("8. Песочница: вставка, обновление, удаление"); for (const std::string& key : {"p-301", "p-302"}) box.delete_one(make_document(kvp("_id", key))); std::vector<bsoncxx::document::value> novye; novye.push_back(make_document(kvp("_id", "p-301"), kvp("title", "Док-станция USB-C"), kvp("category", "аксессуары"), kvp("price", 6490), kvp("reviews", 0))); novye.push_back(make_document(kvp("_id", "p-302"), kvp("title", "Кабель HDMI 2 м"), kvp("category", "аксессуары"), kvp("price", 890), kvp("reviews", 0))); auto added = box.insert_many(novye); std::cout << " вставлено: " << added->inserted_count() << " ключи: "; bool first = true; for (auto&& pair : added->inserted_ids()) { std::cout << (first ? "" : ", ") << std::string(pair.second.get_string().value); first = false; } std::cout << std::endl; auto changed = box.update_one(make_document(kvp("_id", "p-301")), make_document(kvp("$set", make_document(kvp("price", 5990))), kvp("$inc", make_document(kvp("reviews", 1))))); std::cout << " обновление одного: найдено " << changed->matched_count() << ", изменено " << changed->modified_count() << std::endl; auto many = box.update_many(make_document(kvp("category", "аксессуары")), make_document(kvp("$set", make_document(kvp("sale", true))))); std::cout << " пометка распродажи: найдено " << many->matched_count() << ", изменено " << many->modified_count() << std::endl; mongocxx::options::find after_options; after_options.projection(make_document(kvp("_id", 0), kvp("title", 1), kvp("price", 1), kvp("reviews", 1), kvp("sale", 1))); auto after = box.find_one(make_document(kvp("_id", "p-301")), after_options); std::cout << " что стало: " << bsoncxx::to_json(after->view()) << std::endl; auto gone = box.delete_one(make_document(kvp("_id", "p-302"))); std::cout << " удалено: " << gone->deleted_count() << " · товаров в песочнице: " << box.count_documents(make_document()) << std::endl;
// ── 8. Запись: вставка, обновление, удаление ────────────────────── title("8. Песочница: вставка, обновление, удаление") for _, key := range []string{"p-301", "p-302"} { box.DeleteOne(ctx, bson.D{{Key: "_id", Value: key}}) } added, _ := box.InsertMany(ctx, []any{ bson.D{{Key: "_id", Value: "p-301"}, {Key: "title", Value: "Док-станция USB-C"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 6490}, {Key: "reviews", Value: 0}}, bson.D{{Key: "_id", Value: "p-302"}, {Key: "title", Value: "Кабель HDMI 2 м"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 890}, {Key: "reviews", Value: 0}}, }) kluchi := make([]string, 0, len(added.InsertedIDs)) for _, id := range added.InsertedIDs { kluchi = append(kluchi, fmt.Sprint(id)) } fmt.Printf(" вставлено: %d ключи: %s\n", len(added.InsertedIDs), strings.Join(kluchi, ", ")) changed, _ := box.UpdateOne(ctx, bson.D{{Key: "_id", Value: "p-301"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "price", Value: 5990}}}, {Key: "$inc", Value: bson.D{{Key: "reviews", Value: 1}}}}) fmt.Printf(" обновление одного: найдено %d, изменено %d\n", changed.MatchedCount, changed.ModifiedCount) many, _ := box.UpdateMany(ctx, bson.D{{Key: "category", Value: "аксессуары"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "sale", Value: true}}}}) fmt.Printf(" пометка распродажи: найдено %d, изменено %d\n", many.MatchedCount, many.ModifiedCount) var after bson.D box.FindOne(ctx, bson.D{{Key: "_id", Value: "p-301"}}, options.FindOne().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "title", Value: 1}, {Key: "price", Value: 1}, {Key: "reviews", Value: 1}, {Key: "sale", Value: 1}})).Decode(&after) out, _ := bson.MarshalExtJSON(after, false, false) fmt.Println(" что стало:", string(out)) gone, _ := box.DeleteOne(ctx, bson.D{{Key: "_id", Value: "p-302"}}) vPesochnice, _ := box.CountDocuments(ctx, bson.D{}) fmt.Printf(" удалено: %d · товаров в песочнице: %d\n", gone.DeletedCount, vPesochnice)
# ── 8. Запись: вставка, обновление, удаление ────────────────────────────── title("8. Песочница: вставка, обновление, удаление") ["p-301", "p-302"].each { |key| box.delete_one({ "_id" => key }) } added = box.insert_many([ { "_id" => "p-301", "title" => "Док-станция USB-C", "category" => "аксессуары", "price" => 6490, "reviews" => 0 }, { "_id" => "p-302", "title" => "Кабель HDMI 2 м", "category" => "аксессуары", "price" => 890, "reviews" => 0 }, ]) puts " вставлено: #{added.inserted_count} ключи: #{added.inserted_ids.join(', ')}" changed = box.update_one({ "_id" => "p-301" }, { "$set" => { "price" => 5990 }, "$inc" => { "reviews" => 1 } }) puts " обновление одного: найдено #{changed.matched_count}, изменено #{changed.modified_count}" many = box.update_many({ "category" => "аксессуары" }, { "$set" => { "sale" => true } }) puts " пометка распродажи: найдено #{many.matched_count}, изменено #{many.modified_count}" after = box.find({ "_id" => "p-301" }, projection: { "_id" => 0, "title" => 1, "price" => 1, "reviews" => 1, "sale" => 1 }).first puts " что стало: #{after.inspect}" gone = box.delete_one({ "_id" => "p-302" }) puts " удалено: #{gone.deleted_count} · товаров в песочнице: #{box.count_documents({})}"
Каждая операция записи возвращает результат, и скрипт его печатает. У вставки это список выданных ключей, у обновления — два числа: сколько документов подошло под фильтр и сколько изменилось (глава 1.7). У удаления — сколько документов удалено.
Обновление пачкой ставит пометку распродажи всем аксессуарам песочницы, включая три исходных, поэтому в выводе стоит пять. update_many меняет все документы, подошедшие под фильтр.
§6Скрипт целиком
Ниже — тот же файл без пропусков, а под ним вывод его запуска на стенде курса. Числа в выводе совпадут с вашими, если базы перед запуском в исходном состоянии.
"""Отчёт по магазину: что умеет MongoDB из модуля 1. Скрипт проходит все операции модуля 1 на базе shop и в песочнице sandbox и печатает результат так, как его показывают в консоли: счётчики, таблицы, карточку документа, страницы и итоги записи. cd scripts python3 shop_report.py docker compose run --rm python scripts/shop_report.py # в контейнере """ import os from pymongo import MongoClient client = MongoClient(os.environ.get("MONGO_URI", "mongodb://localhost:27017/")) shop = client["shop"] products = shop["products"] orders = shop["orders"] box = client["sandbox"]["products"] def title(text): """Заголовок раздела: отчёт читают глазами, разделы видно сразу.""" print("\n" + text) print("─" * len(text)) # ── 1. Что лежит на сервере ─────────────────────────────────────────────── title("1. Связь и база shop") client.admin.command("ping") # первое обращение к серверу print("базы на сервере:", ", ".join(sorted(client.list_database_names()))) print("коллекции shop:", ", ".join(sorted(shop.list_collection_names()))) print(f"товаров {products.count_documents({})}, " f"заказов {orders.count_documents({})}, " f"покупателей {shop['customers'].count_documents({})}") # ── 2. Карточка одного документа ────────────────────────────────────────── title("2. Один товар целиком") tovar = products.find_one({"_id": "p-001"}) for field, value in tovar.items(): print(f" {field:10} {value}") # ── 3. Таблица: пять самых дорогих товаров ──────────────────────────────── title("3. Пять самых дорогих товаров") print(f" {'название':32} {'категория':14} {'цена':>8}") cursor = (products.find({}, {"_id": 0, "title": 1, "category": 1, "price": 1}) .sort("price", -1) .limit(5)) for doc in cursor: print(f" {doc['title']:32} {doc['category']:14} {doc['price']:>8}") # ── 4. Сортировка по двум полям ─────────────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым") katalog = (products.find({}, {"_id": 0, "category": 1, "title": 1, "price": 1}) .sort([("category", 1), ("price", -1)]) .limit(8)) for doc in katalog: print(f" {doc['category']:14} {doc['title']:32} {doc['price']:>8}") # ── 5. Счётчики по категориям ───────────────────────────────────────────── title("5. Товары по категориям") for category in sorted(products.distinct("category")): print(f" {category:14} {products.count_documents({'category': category}):2} шт") # ── 6. Страницы заказов ─────────────────────────────────────────────────── title("6. Заказы страницами по 5") for page in (1, 2): print(f" страница {page}") stranica = (orders.find({"status": "доставлен"}, {"_id": 0, "number": 1, "total": 1}) .sort("number", 1) .skip((page - 1) * 5) .limit(5)) for doc in stranica: print(f" {doc['number']} {doc['total']:>7}") # ── 7. Вложенные поля и массивы в выводе ────────────────────────────────── title("7. Заказ с доставкой и позициями") zakaz = orders.find_one({"_id": "o-0001"}) print(f" номер {zakaz['number']}, статус {zakaz['status']}, сумма {zakaz['total']}") print(f" доставка: {zakaz['delivery']['city']}, {zakaz['delivery']['type']}, " f"{zakaz['delivery']['days']} дн.") print(f" оплата: {zakaz['payment']['method']}, проведена: {zakaz['payment']['paid']}") for position in zakaz["items"]: print(f" {position['qty']} × {position['title']:34} {position['price']:>7}") # ── 8. Запись: вставка, обновление, удаление ────────────────────────────── title("8. Песочница: вставка, обновление, удаление") for key in ("p-301", "p-302"): # чтобы скрипт можно было запускать повторно box.delete_one({"_id": key}) added = box.insert_many([ {"_id": "p-301", "title": "Док-станция USB-C", "category": "аксессуары", "price": 6490, "reviews": 0}, {"_id": "p-302", "title": "Кабель HDMI 2 м", "category": "аксессуары", "price": 890, "reviews": 0}, ]) print(" вставлено:", len(added.inserted_ids), "ключи:", ", ".join(added.inserted_ids)) changed = box.update_one({"_id": "p-301"}, {"$set": {"price": 5990}, "$inc": {"reviews": 1}}) print(f" обновление одного: найдено {changed.matched_count}, изменено {changed.modified_count}") many = box.update_many({"category": "аксессуары"}, {"$set": {"sale": True}}) print(f" пометка распродажи: найдено {many.matched_count}, изменено {many.modified_count}") after = box.find_one({"_id": "p-301"}, {"_id": 0, "title": 1, "price": 1, "reviews": 1, "sale": 1}) print(" что стало:", after) gone = box.delete_one({"_id": "p-302"}) print(" удалено:", gone.deleted_count, "· товаров в песочнице:", box.count_documents({})) # ── 9. Итог ─────────────────────────────────────────────────────────────── title("9. Итог") print(f" прочитано из shop: {products.count_documents({})} товаров и " f"{orders.count_documents({})} заказов") print(" изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление") client.close()
// Отчёт по магазину: что умеет MongoDB из модуля 1. // // Скрипт проходит все операции модуля 1 на базе shop и в песочнице sandbox // и печатает результат так, как его показывают в консоли: счётчики, таблицы, // карточку документа, страницы и итоги записи. // // cd scripts // g++ -std=c++17 shop_report.cpp -o shop_report $(pkg-config --cflags --libs libmongocxx1) && ./shop_report // // docker compose run --rm cpp scripts/shop_report.cpp # в контейнере #include <algorithm> #include <chrono> #include <cstdlib> #include <ctime> #include <iomanip> #include <iostream> #include <sstream> #include <string> #include <vector> #include <bsoncxx/builder/basic/array.hpp> #include <bsoncxx/builder/basic/document.hpp> #include <bsoncxx/builder/basic/kvp.hpp> #include <bsoncxx/json.hpp> #include <bsoncxx/types.hpp> #include <mongocxx/client.hpp> #include <mongocxx/instance.hpp> #include <mongocxx/options/find.hpp> #include <mongocxx/uri.hpp> using bsoncxx::builder::basic::kvp; using bsoncxx::builder::basic::make_document; // Ширина столбца считается по символам: в кириллице на букву приходится // два байта, и обычный setw сдвинул бы столбцы. int simvolov(const std::string& text) { int n = 0; for (unsigned char c : text) if ((c & 0xC0) != 0x80) ++n; return n; } std::string pad(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? text + std::string(add, ' ') : text; } std::string right(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? std::string(add, ' ') + text : text; } void title(const std::string& text) { std::cout << "\n" << text << "\n"; for (int i = 0; i < simvolov(text); ++i) std::cout << "─"; std::cout << std::endl; } // Значение любого типа BSON в строку — для карточки документа. std::string znachenie(const bsoncxx::document::element& field) { switch (field.type()) { case bsoncxx::type::k_string: return std::string(field.get_string().value); case bsoncxx::type::k_int32: return std::to_string(field.get_int32().value); case bsoncxx::type::k_int64: return std::to_string(field.get_int64().value); case bsoncxx::type::k_double: { std::ostringstream out; out << field.get_double().value; // без лишних нулей: 4.2, а не 4.200000 return out.str(); } case bsoncxx::type::k_bool: return field.get_bool().value ? "true" : "false"; case bsoncxx::type::k_date: { std::time_t sekundy = field.get_date().to_int64() / 1000; std::ostringstream out; out << std::put_time(std::gmtime(&sekundy), "%Y-%m-%d %H:%M:%S"); return out.str(); } case bsoncxx::type::k_document: return bsoncxx::to_json(field.get_document().value); case bsoncxx::type::k_array: return bsoncxx::to_json(field.get_array().value); default: { // to_json берёт документ или массив, поэтому значение заворачиваем std::string json = bsoncxx::to_json(make_document(kvp("v", field.get_value())).view()); return json.substr(json.find(':') + 2, json.rfind('}') - json.find(':') - 3); } } } int main() { mongocxx::instance instance{}; const char* env = std::getenv("MONGO_URI"); mongocxx::client client{mongocxx::uri{env ? env : "mongodb://localhost:27017"}}; auto shop = client["shop"]; auto products = shop["products"]; auto orders = shop["orders"]; auto box = client["sandbox"]["products"]; // ── 1. Что лежит на сервере ─────────────────────────────────────── title("1. Связь и база shop"); client["admin"].run_command(make_document(kvp("ping", 1))); auto bases = client.list_database_names(); std::sort(bases.begin(), bases.end()); std::cout << "базы на сервере: "; for (size_t i = 0; i < bases.size(); ++i) std::cout << (i ? ", " : "") << bases[i]; std::cout << std::endl; auto names = shop.list_collection_names(); std::sort(names.begin(), names.end()); std::cout << "коллекции shop: "; for (size_t i = 0; i < names.size(); ++i) std::cout << (i ? ", " : "") << names[i]; std::cout << std::endl; auto vsego_tovarov = products.count_documents(make_document()); auto vsego_zakazov = orders.count_documents(make_document()); std::cout << "товаров " << vsego_tovarov << ", заказов " << vsego_zakazov << ", покупателей " << shop["customers"].count_documents(make_document()) << std::endl; // ── 2. Карточка одного документа ────────────────────────────────── title("2. Один товар целиком"); auto tovar = products.find_one(make_document(kvp("_id", "p-001"))); for (auto&& field : tovar->view()) std::cout << " " << pad(std::string(field.key()), 10) << " " << znachenie(field) << std::endl; // ── 3. Таблица: пять самых дорогих товаров ──────────────────────── title("3. Пять самых дорогих товаров"); std::cout << " " << pad("название", 32) << " " << pad("категория", 14) << " " << right("цена", 8) << std::endl; mongocxx::options::find options; options.projection(make_document(kvp("_id", 0), kvp("title", 1), kvp("category", 1), kvp("price", 1))); options.sort(make_document(kvp("price", -1))); options.limit(5); for (auto&& doc : products.find(make_document(), options)) { std::cout << " " << pad(std::string(doc["title"].get_string().value), 32) << " " << pad(std::string(doc["category"].get_string().value), 14) << " " << right(std::to_string(doc["price"].get_int32().value), 8) << std::endl; } // ── 4. Сортировка по двум полям ─────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым"); mongocxx::options::find katalog_options; katalog_options.projection(make_document(kvp("_id", 0), kvp("category", 1), kvp("title", 1), kvp("price", 1))); katalog_options.sort(make_document(kvp("category", 1), kvp("price", -1))); katalog_options.limit(8); for (auto&& doc : products.find(make_document(), katalog_options)) { std::cout << " " << pad(std::string(doc["category"].get_string().value), 14) << " " << pad(std::string(doc["title"].get_string().value), 32) << " " << right(std::to_string(doc["price"].get_int32().value), 8) << std::endl; } // ── 5. Счётчики по категориям ───────────────────────────────────── title("5. Товары по категориям"); std::vector<std::string> kategorii; for (auto&& doc : products.distinct("category", make_document())) for (auto&& value : doc["values"].get_array().value) kategorii.push_back(std::string(value.get_string().value)); std::sort(kategorii.begin(), kategorii.end()); for (const auto& category : kategorii) { auto skolko = products.count_documents(make_document(kvp("category", category))); std::cout << " " << pad(category, 14) << " " << right(std::to_string(skolko), 2) << " шт" << std::endl; } // ── 6. Страницы заказов ─────────────────────────────────────────── title("6. Заказы страницами по 5"); for (int page = 1; page <= 2; ++page) { std::cout << " страница " << page << std::endl; mongocxx::options::find page_options; page_options.projection(make_document(kvp("_id", 0), kvp("number", 1), kvp("total", 1))); page_options.sort(make_document(kvp("number", 1))); page_options.skip((page - 1) * 5); page_options.limit(5); for (auto&& doc : orders.find(make_document(kvp("status", "доставлен")), page_options)) { std::cout << " " << std::string(doc["number"].get_string().value) << " " << right(std::to_string(doc["total"].get_int32().value), 7) << std::endl; } } // ── 7. Вложенные поля и массивы в выводе ────────────────────────── title("7. Заказ с доставкой и позициями"); // Документ сохраняем в переменную: view указывает внутрь неё, и временное // значение из find_one исчезло бы сразу после строки. auto zakaz_doc = orders.find_one(make_document(kvp("_id", "o-0001"))); auto zakaz = zakaz_doc->view(); std::cout << " номер " << std::string(zakaz["number"].get_string().value) << ", статус " << std::string(zakaz["status"].get_string().value) << ", сумма " << zakaz["total"].get_int32().value << std::endl; auto dostavka = zakaz["delivery"].get_document().value; std::cout << " доставка: " << std::string(dostavka["city"].get_string().value) << ", " << std::string(dostavka["type"].get_string().value) << ", " << dostavka["days"].get_int32().value << " дн." << std::endl; auto oplata = zakaz["payment"].get_document().value; std::cout << " оплата: " << std::string(oplata["method"].get_string().value) << ", проведена: " << (oplata["paid"].get_bool().value ? "true" : "false") << std::endl; for (auto&& position : zakaz["items"].get_array().value) { auto item = position.get_document().value; std::cout << " " << item["qty"].get_int32().value << " × " << pad(std::string(item["title"].get_string().value), 34) << " " << right(std::to_string(item["price"].get_int32().value), 7) << std::endl; } // ── 8. Запись: вставка, обновление, удаление ────────────────────── title("8. Песочница: вставка, обновление, удаление"); for (const std::string& key : {"p-301", "p-302"}) box.delete_one(make_document(kvp("_id", key))); std::vector<bsoncxx::document::value> novye; novye.push_back(make_document(kvp("_id", "p-301"), kvp("title", "Док-станция USB-C"), kvp("category", "аксессуары"), kvp("price", 6490), kvp("reviews", 0))); novye.push_back(make_document(kvp("_id", "p-302"), kvp("title", "Кабель HDMI 2 м"), kvp("category", "аксессуары"), kvp("price", 890), kvp("reviews", 0))); auto added = box.insert_many(novye); std::cout << " вставлено: " << added->inserted_count() << " ключи: "; bool first = true; for (auto&& pair : added->inserted_ids()) { std::cout << (first ? "" : ", ") << std::string(pair.second.get_string().value); first = false; } std::cout << std::endl; auto changed = box.update_one(make_document(kvp("_id", "p-301")), make_document(kvp("$set", make_document(kvp("price", 5990))), kvp("$inc", make_document(kvp("reviews", 1))))); std::cout << " обновление одного: найдено " << changed->matched_count() << ", изменено " << changed->modified_count() << std::endl; auto many = box.update_many(make_document(kvp("category", "аксессуары")), make_document(kvp("$set", make_document(kvp("sale", true))))); std::cout << " пометка распродажи: найдено " << many->matched_count() << ", изменено " << many->modified_count() << std::endl; mongocxx::options::find after_options; after_options.projection(make_document(kvp("_id", 0), kvp("title", 1), kvp("price", 1), kvp("reviews", 1), kvp("sale", 1))); auto after = box.find_one(make_document(kvp("_id", "p-301")), after_options); std::cout << " что стало: " << bsoncxx::to_json(after->view()) << std::endl; auto gone = box.delete_one(make_document(kvp("_id", "p-302"))); std::cout << " удалено: " << gone->deleted_count() << " · товаров в песочнице: " << box.count_documents(make_document()) << std::endl; // ── 9. Итог ─────────────────────────────────────────────────────── title("9. Итог"); std::cout << " прочитано из shop: " << vsego_tovarov << " товаров и " << vsego_zakazov << " заказов" << std::endl; std::cout << " изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление" << std::endl; }
// Отчёт по магазину: что умеет MongoDB из модуля 1. // // Скрипт проходит все операции модуля 1 на базе shop и в песочнице sandbox // и печатает результат так, как его показывают в консоли: счётчики, таблицы, // карточку документа, страницы и итоги записи. // // cd scripts // go run shop_report.go // // docker compose run --rm go scripts/shop_report.go # в контейнере package main import ( "context" "fmt" "log" "os" "sort" "strings" "unicode/utf8" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) // Product — поля товара, которые читает отчёт. type Product struct { ID string `bson:"_id"` Title string `bson:"title"` Category string `bson:"category"` Price int `bson:"price"` } // Order — заказ вместе с вложенными документами и массивом позиций. type Order struct { Number string `bson:"number"` Status string `bson:"status"` Total int `bson:"total"` Delivery struct { City string `bson:"city"` Type string `bson:"type"` Days int `bson:"days"` } `bson:"delivery"` Payment struct { Method string `bson:"method"` Paid bool `bson:"paid"` } `bson:"payment"` Items []struct { Title string `bson:"title"` Qty int `bson:"qty"` Price int `bson:"price"` } `bson:"items"` } // pad дополняет строку пробелами по числу символов: в кириллице байт больше, // чем букв, и обычное %-32s сдвигает столбцы. func pad(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return text + strings.Repeat(" ", width-n) } return text } func right(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return strings.Repeat(" ", width-n) + text } return text } func title(text string) { fmt.Println("\n" + text) fmt.Println(strings.Repeat("─", utf8.RuneCountInString(text))) } func main() { uri := os.Getenv("MONGO_URI") if uri == "" { uri = "mongodb://localhost:27017" } client, err := mongo.Connect(options.Client().ApplyURI(uri)) if err != nil { log.Fatal(err) } ctx := context.Background() defer client.Disconnect(ctx) shop := client.Database("shop") products := shop.Collection("products") orders := shop.Collection("orders") box := client.Database("sandbox").Collection("products") // ── 1. Что лежит на сервере ─────────────────────────────────────── title("1. Связь и база shop") if err := client.Ping(ctx, nil); err != nil { log.Fatal(err) } bases, _ := client.ListDatabaseNames(ctx, bson.D{}) sort.Strings(bases) fmt.Println("базы на сервере:", strings.Join(bases, ", ")) names, _ := shop.ListCollectionNames(ctx, bson.D{}) sort.Strings(names) fmt.Println("коллекции shop:", strings.Join(names, ", ")) vsegoTovarov, _ := products.CountDocuments(ctx, bson.D{}) vsegoZakazov, _ := orders.CountDocuments(ctx, bson.D{}) vsegoKlientov, _ := shop.Collection("customers").CountDocuments(ctx, bson.D{}) fmt.Printf("товаров %d, заказов %d, покупателей %d\n", vsegoTovarov, vsegoZakazov, vsegoKlientov) // ── 2. Карточка одного документа ────────────────────────────────── title("2. Один товар целиком") var tovar bson.D products.FindOne(ctx, bson.D{{Key: "_id", Value: "p-001"}}).Decode(&tovar) for _, field := range tovar { fmt.Printf(" %s %v\n", pad(field.Key, 10), field.Value) } // ── 3. Таблица: пять самых дорогих товаров ──────────────────────── title("3. Пять самых дорогих товаров") fmt.Printf(" %s %s %s\n", pad("название", 32), pad("категория", 14), right("цена", 8)) cursor, _ := products.Find(ctx, bson.D{}, options.Find(). SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "title", Value: 1}, {Key: "category", Value: 1}, {Key: "price", Value: 1}}). SetSort(bson.D{{Key: "price", Value: -1}}). SetLimit(5)) var dorogie []Product cursor.All(ctx, &dorogie) for _, p := range dorogie { fmt.Printf(" %s %s %s\n", pad(p.Title, 32), pad(p.Category, 14), right(fmt.Sprint(p.Price), 8)) } // ── 4. Сортировка по двум полям ─────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым") cursor, _ = products.Find(ctx, bson.D{}, options.Find(). SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "category", Value: 1}, {Key: "title", Value: 1}, {Key: "price", Value: 1}}). SetSort(bson.D{{Key: "category", Value: 1}, {Key: "price", Value: -1}}). SetLimit(8)) var katalog []Product cursor.All(ctx, &katalog) for _, p := range katalog { fmt.Printf(" %s %s %s\n", pad(p.Category, 14), pad(p.Title, 32), right(fmt.Sprint(p.Price), 8)) } // ── 5. Счётчики по категориям ───────────────────────────────────── title("5. Товары по категориям") var spisok []string products.Distinct(ctx, "category", bson.D{}).Decode(&spisok) sort.Strings(spisok) for _, category := range spisok { skolko, _ := products.CountDocuments(ctx, bson.D{{Key: "category", Value: category}}) fmt.Printf(" %s %s шт\n", pad(category, 14), right(fmt.Sprint(skolko), 2)) } // ── 6. Страницы заказов ─────────────────────────────────────────── title("6. Заказы страницами по 5") for page := int64(1); page <= 2; page++ { fmt.Printf(" страница %d\n", page) cursor, _ = orders.Find(ctx, bson.D{{Key: "status", Value: "доставлен"}}, options.Find(). SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "number", Value: 1}, {Key: "total", Value: 1}}). SetSort(bson.D{{Key: "number", Value: 1}}). SetSkip((page - 1) * 5). SetLimit(5)) var stranica []Order cursor.All(ctx, &stranica) for _, o := range stranica { fmt.Printf(" %s %s\n", o.Number, right(fmt.Sprint(o.Total), 7)) } } // ── 7. Вложенные поля и массивы в выводе ────────────────────────── title("7. Заказ с доставкой и позициями") var zakaz Order orders.FindOne(ctx, bson.D{{Key: "_id", Value: "o-0001"}}).Decode(&zakaz) fmt.Printf(" номер %s, статус %s, сумма %d\n", zakaz.Number, zakaz.Status, zakaz.Total) fmt.Printf(" доставка: %s, %s, %d дн.\n", zakaz.Delivery.City, zakaz.Delivery.Type, zakaz.Delivery.Days) fmt.Printf(" оплата: %s, проведена: %t\n", zakaz.Payment.Method, zakaz.Payment.Paid) for _, position := range zakaz.Items { fmt.Printf(" %d × %s %s\n", position.Qty, pad(position.Title, 34), right(fmt.Sprint(position.Price), 7)) } // ── 8. Запись: вставка, обновление, удаление ────────────────────── title("8. Песочница: вставка, обновление, удаление") for _, key := range []string{"p-301", "p-302"} { box.DeleteOne(ctx, bson.D{{Key: "_id", Value: key}}) } added, _ := box.InsertMany(ctx, []any{ bson.D{{Key: "_id", Value: "p-301"}, {Key: "title", Value: "Док-станция USB-C"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 6490}, {Key: "reviews", Value: 0}}, bson.D{{Key: "_id", Value: "p-302"}, {Key: "title", Value: "Кабель HDMI 2 м"}, {Key: "category", Value: "аксессуары"}, {Key: "price", Value: 890}, {Key: "reviews", Value: 0}}, }) kluchi := make([]string, 0, len(added.InsertedIDs)) for _, id := range added.InsertedIDs { kluchi = append(kluchi, fmt.Sprint(id)) } fmt.Printf(" вставлено: %d ключи: %s\n", len(added.InsertedIDs), strings.Join(kluchi, ", ")) changed, _ := box.UpdateOne(ctx, bson.D{{Key: "_id", Value: "p-301"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "price", Value: 5990}}}, {Key: "$inc", Value: bson.D{{Key: "reviews", Value: 1}}}}) fmt.Printf(" обновление одного: найдено %d, изменено %d\n", changed.MatchedCount, changed.ModifiedCount) many, _ := box.UpdateMany(ctx, bson.D{{Key: "category", Value: "аксессуары"}}, bson.D{{Key: "$set", Value: bson.D{{Key: "sale", Value: true}}}}) fmt.Printf(" пометка распродажи: найдено %d, изменено %d\n", many.MatchedCount, many.ModifiedCount) var after bson.D box.FindOne(ctx, bson.D{{Key: "_id", Value: "p-301"}}, options.FindOne().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "title", Value: 1}, {Key: "price", Value: 1}, {Key: "reviews", Value: 1}, {Key: "sale", Value: 1}})).Decode(&after) out, _ := bson.MarshalExtJSON(after, false, false) fmt.Println(" что стало:", string(out)) gone, _ := box.DeleteOne(ctx, bson.D{{Key: "_id", Value: "p-302"}}) vPesochnice, _ := box.CountDocuments(ctx, bson.D{}) fmt.Printf(" удалено: %d · товаров в песочнице: %d\n", gone.DeletedCount, vPesochnice) // ── 9. Итог ─────────────────────────────────────────────────────── title("9. Итог") fmt.Printf(" прочитано из shop: %d товаров и %d заказов\n", vsegoTovarov, vsegoZakazov) fmt.Println(" изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление") }
# Отчёт по магазину: что умеет MongoDB из модуля 1. # # Скрипт проходит все операции модуля 1 на базе shop и в песочнице sandbox # и печатает результат так, как его показывают в консоли: счётчики, таблицы, # карточку документа, страницы и итоги записи. # # cd scripts # ruby shop_report.rb # # docker compose run --rm ruby scripts/shop_report.rb # в контейнере require "mongo" Mongo::Logger.logger.level = Logger::WARN client = Mongo::Client.new(ENV.fetch("MONGO_URI", "mongodb://localhost:27017/")) shop = client.use("shop").database products = shop[:products] orders = shop[:orders] box = client.use("sandbox").database[:products] def title(text) puts "\n" + text puts "─" * text.length end def pad(text, width) # ljust по видимым символам, как в других языках text.to_s.ljust(width) end # ── 1. Что лежит на сервере ─────────────────────────────────────────────── title("1. Связь и база shop") client.use("admin").database.command(ping: 1) puts "базы на сервере: " + client.database_names.sort.join(", ") puts "коллекции shop: " + shop.collection_names.sort.join(", ") puts "товаров #{products.count_documents({})}, " \ "заказов #{orders.count_documents({})}, " \ "покупателей #{shop[:customers].count_documents({})}" # ── 2. Карточка одного документа ────────────────────────────────────────── title("2. Один товар целиком") tovar = products.find({ "_id" => "p-001" }).first tovar.each { |field, value| puts " #{pad(field, 10)} #{value}" } # ── 3. Таблица: пять самых дорогих товаров ──────────────────────────────── title("3. Пять самых дорогих товаров") puts " #{pad('название', 32)} #{pad('категория', 14)} #{'цена'.rjust(8)}" products.find({}, projection: { "_id" => 0, "title" => 1, "category" => 1, "price" => 1 }) .sort({ "price" => -1 }) .limit(5) .each do |doc| puts " #{pad(doc['title'], 32)} #{pad(doc['category'], 14)} #{doc['price'].to_s.rjust(8)}" end # ── 4. Сортировка по двум полям ─────────────────────────────────────────── title("4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым") products.find({}, projection: { "_id" => 0, "category" => 1, "title" => 1, "price" => 1 }) .sort({ "category" => 1, "price" => -1 }) .limit(8) .each do |doc| puts " #{pad(doc['category'], 14)} #{pad(doc['title'], 32)} #{doc['price'].to_s.rjust(8)}" end # ── 5. Счётчики по категориям ───────────────────────────────────────────── title("5. Товары по категориям") products.distinct("category").sort.each do |category| puts " #{pad(category, 14)} #{products.count_documents({ 'category' => category }).to_s.rjust(2)} шт" end # ── 6. Страницы заказов ─────────────────────────────────────────────────── title("6. Заказы страницами по 5") [1, 2].each do |page| puts " страница #{page}" orders.find({ "status" => "доставлен" }, projection: { "_id" => 0, "number" => 1, "total" => 1 }) .sort({ "number" => 1 }) .skip((page - 1) * 5) .limit(5) .each { |doc| puts " #{doc['number']} #{doc['total'].to_s.rjust(7)}" } end # ── 7. Вложенные поля и массивы в выводе ────────────────────────────────── title("7. Заказ с доставкой и позициями") zakaz = orders.find({ "_id" => "o-0001" }).first puts " номер #{zakaz['number']}, статус #{zakaz['status']}, сумма #{zakaz['total']}" puts " доставка: #{zakaz['delivery']['city']}, #{zakaz['delivery']['type']}, " \ "#{zakaz['delivery']['days']} дн." puts " оплата: #{zakaz['payment']['method']}, проведена: #{zakaz['payment']['paid']}" zakaz["items"].each do |position| puts " #{position['qty']} × #{pad(position['title'], 34)} #{position['price'].to_s.rjust(7)}" end # ── 8. Запись: вставка, обновление, удаление ────────────────────────────── title("8. Песочница: вставка, обновление, удаление") ["p-301", "p-302"].each { |key| box.delete_one({ "_id" => key }) } added = box.insert_many([ { "_id" => "p-301", "title" => "Док-станция USB-C", "category" => "аксессуары", "price" => 6490, "reviews" => 0 }, { "_id" => "p-302", "title" => "Кабель HDMI 2 м", "category" => "аксессуары", "price" => 890, "reviews" => 0 }, ]) puts " вставлено: #{added.inserted_count} ключи: #{added.inserted_ids.join(', ')}" changed = box.update_one({ "_id" => "p-301" }, { "$set" => { "price" => 5990 }, "$inc" => { "reviews" => 1 } }) puts " обновление одного: найдено #{changed.matched_count}, изменено #{changed.modified_count}" many = box.update_many({ "category" => "аксессуары" }, { "$set" => { "sale" => true } }) puts " пометка распродажи: найдено #{many.matched_count}, изменено #{many.modified_count}" after = box.find({ "_id" => "p-301" }, projection: { "_id" => 0, "title" => 1, "price" => 1, "reviews" => 1, "sale" => 1 }).first puts " что стало: #{after.inspect}" gone = box.delete_one({ "_id" => "p-302" }) puts " удалено: #{gone.deleted_count} · товаров в песочнице: #{box.count_documents({})}" # ── 9. Итог ─────────────────────────────────────────────────────────────── title("9. Итог") puts " прочитано из shop: #{products.count_documents({})} товаров и " \ "#{orders.count_documents({})} заказов" puts " изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление" client.close
1. Связь и база shop
────────────────────
базы на сервере: admin, config, hh, local, logs, org, sandbox, shop
коллекции shop: customers, orders, products
товаров 21, заказов 120, покупателей 20
2. Один товар целиком
─────────────────────
_id p-001
sku SKU-NB-001
title Ноутбук Lenovo IdeaPad 3
brand Lenovo
category ноутбуки
price 42990
specs ['15.6"', '8 ГБ', 'SSD 512 ГБ']
stock [{'warehouse': 'Санкт-Петербург', 'qty': 5}]
rating 4.2
reviews 57
added 2026-02-05 00:00:00
3. Пять самых дорогих товаров
─────────────────────────────
название категория цена
Ноутбук Apple MacBook Air 13 ноутбуки 114990
Ноутбук Acer Nitro V15 ноутбуки 89990
Смартфон Apple iPhone 15 смартфоны 79990
Ноутбук HP Pavilion 14 ноутбуки 67900
Ноутбук ASUS VivoBook 15 ноутбуки 51490
4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым
────────────────────────────────────────────────────────────────
аксессуары Powerbank Anker 20000 мА·ч 4290
аксессуары Рюкзак для ноутбука Xiaomi 2990
аксессуары Кабель USB-C 2 м 790
комплектующие Видеокарта NVIDIA RTX 4060 38990
комплектующие Блок питания be quiet! 650 Вт 8490
комплектующие SSD Samsung 980 1 ТБ 7890
комплектующие Оперативная память Kingston 16 ГБ 4590
ноутбуки Ноутбук Apple MacBook Air 13 114990
5. Товары по категориям
───────────────────────
аксессуары 3 шт
комплектующие 4 шт
ноутбуки 5 шт
периферия 5 шт
смартфоны 4 шт
6. Заказы страницами по 5
─────────────────────────
страница 1
2026-0001 5980
2026-0003 86880
2026-0004 106210
2026-0006 2370
2026-0007 75790
страница 2
2026-0008 285750
2026-0009 320740
2026-0010 120340
2026-0011 85980
2026-0012 222740
7. Заказ с доставкой и позициями
────────────────────────────────
номер 2026-0001, статус доставлен, сумма 5980
доставка: Ярославль, пункт выдачи, 1 дн.
оплата: карта, проведена: True
2 × Рюкзак для ноутбука Xiaomi 2990
8. Песочница: вставка, обновление, удаление
───────────────────────────────────────────
вставлено: 2 ключи: p-301, p-302
обновление одного: найдено 1, изменено 1
пометка распродажи: найдено 5, изменено 5
что стало: {'title': 'Док-станция USB-C', 'price': 5990, 'reviews': 1, 'sale': True}
удалено: 1 · товаров в песочнице: 22
9. Итог
───────
прочитано из shop: 21 товаров и 120 заказов
изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление
1. Связь и база shop
────────────────────
базы на сервере: admin, config, hh, local, logs, org, sandbox, shop
коллекции shop: customers, orders, products
товаров 21, заказов 120, покупателей 20
2. Один товар целиком
─────────────────────
_id p-001
sku SKU-NB-001
title Ноутбук Lenovo IdeaPad 3
brand Lenovo
category ноутбуки
price 42990
specs [ "15.6\"", "8 ГБ", "SSD 512 ГБ" ]
stock [ { "warehouse" : "Санкт-Петербург", "qty" : 5 } ]
rating 4.2
reviews 57
added 2026-02-05 00:00:00
3. Пять самых дорогих товаров
─────────────────────────────
название категория цена
Ноутбук Apple MacBook Air 13 ноутбуки 114990
Ноутбук Acer Nitro V15 ноутбуки 89990
Смартфон Apple iPhone 15 смартфоны 79990
Ноутбук HP Pavilion 14 ноутбуки 67900
Ноутбук ASUS VivoBook 15 ноутбуки 51490
4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым
────────────────────────────────────────────────────────────────
аксессуары Powerbank Anker 20000 мА·ч 4290
аксессуары Рюкзак для ноутбука Xiaomi 2990
аксессуары Кабель USB-C 2 м 790
комплектующие Видеокарта NVIDIA RTX 4060 38990
комплектующие Блок питания be quiet! 650 Вт 8490
комплектующие SSD Samsung 980 1 ТБ 7890
комплектующие Оперативная память Kingston 16 ГБ 4590
ноутбуки Ноутбук Apple MacBook Air 13 114990
5. Товары по категориям
───────────────────────
аксессуары 3 шт
комплектующие 4 шт
ноутбуки 5 шт
периферия 5 шт
смартфоны 4 шт
6. Заказы страницами по 5
─────────────────────────
страница 1
2026-0001 5980
2026-0003 86880
2026-0004 106210
2026-0006 2370
2026-0007 75790
страница 2
2026-0008 285750
2026-0009 320740
2026-0010 120340
2026-0011 85980
2026-0012 222740
7. Заказ с доставкой и позициями
────────────────────────────────
номер 2026-0001, статус доставлен, сумма 5980
доставка: Ярославль, пункт выдачи, 1 дн.
оплата: карта, проведена: true
2 × Рюкзак для ноутбука Xiaomi 2990
8. Песочница: вставка, обновление, удаление
───────────────────────────────────────────
вставлено: 2 ключи: p-301, p-302
обновление одного: найдено 1, изменено 1
пометка распродажи: найдено 5, изменено 5
что стало: { "title" : "Док-станция USB-C", "price" : 5990, "reviews" : 1, "sale" : true }
удалено: 1 · товаров в песочнице: 22
9. Итог
───────
прочитано из shop: 21 товаров и 120 заказов
изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление
1. Связь и база shop
────────────────────
базы на сервере: admin, config, hh, local, logs, org, sandbox, shop
коллекции shop: customers, orders, products
товаров 21, заказов 120, покупателей 20
2. Один товар целиком
─────────────────────
_id p-001
sku SKU-NB-001
title Ноутбук Lenovo IdeaPad 3
brand Lenovo
category ноутбуки
price 42990
specs [15.6" 8 ГБ SSD 512 ГБ]
stock [{"warehouse":"Санкт-Петербург","qty":{"$numberInt":"5"}}]
rating 4.2
reviews 57
added 1770249600000
3. Пять самых дорогих товаров
─────────────────────────────
название категория цена
Ноутбук Apple MacBook Air 13 ноутбуки 114990
Ноутбук Acer Nitro V15 ноутбуки 89990
Смартфон Apple iPhone 15 смартфоны 79990
Ноутбук HP Pavilion 14 ноутбуки 67900
Ноутбук ASUS VivoBook 15 ноутбуки 51490
4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым
────────────────────────────────────────────────────────────────
аксессуары Powerbank Anker 20000 мА·ч 4290
аксессуары Рюкзак для ноутбука Xiaomi 2990
аксессуары Кабель USB-C 2 м 790
комплектующие Видеокарта NVIDIA RTX 4060 38990
комплектующие Блок питания be quiet! 650 Вт 8490
комплектующие SSD Samsung 980 1 ТБ 7890
комплектующие Оперативная память Kingston 16 ГБ 4590
ноутбуки Ноутбук Apple MacBook Air 13 114990
5. Товары по категориям
───────────────────────
аксессуары 3 шт
комплектующие 4 шт
ноутбуки 5 шт
периферия 5 шт
смартфоны 4 шт
6. Заказы страницами по 5
─────────────────────────
страница 1
2026-0001 5980
2026-0003 86880
2026-0004 106210
2026-0006 2370
2026-0007 75790
страница 2
2026-0008 285750
2026-0009 320740
2026-0010 120340
2026-0011 85980
2026-0012 222740
7. Заказ с доставкой и позициями
────────────────────────────────
номер 2026-0001, статус доставлен, сумма 5980
доставка: Ярославль, пункт выдачи, 1 дн.
оплата: карта, проведена: true
2 × Рюкзак для ноутбука Xiaomi 2990
8. Песочница: вставка, обновление, удаление
───────────────────────────────────────────
вставлено: 2 ключи: p-301, p-302
обновление одного: найдено 1, изменено 1
пометка распродажи: найдено 5, изменено 5
что стало: {"title":"Док-станция USB-C","price":5990,"reviews":1,"sale":true}
удалено: 1 · товаров в песочнице: 22
9. Итог
───────
прочитано из shop: 21 товаров и 120 заказов
изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление
1. Связь и база shop
────────────────────
базы на сервере: admin, config, hh, local, logs, org, sandbox, shop
коллекции shop: customers, orders, products
товаров 21, заказов 120, покупателей 20
2. Один товар целиком
─────────────────────
_id p-001
sku SKU-NB-001
title Ноутбук Lenovo IdeaPad 3
brand Lenovo
category ноутбуки
price 42990
specs ["15.6\"", "8 ГБ", "SSD 512 ГБ"]
stock [{"warehouse"=>"Санкт-Петербург", "qty"=>5}]
rating 4.2
reviews 57
added 2026-02-05 00:00:00 UTC
3. Пять самых дорогих товаров
─────────────────────────────
название категория цена
Ноутбук Apple MacBook Air 13 ноутбуки 114990
Ноутбук Acer Nitro V15 ноутбуки 89990
Смартфон Apple iPhone 15 смартфоны 79990
Ноутбук HP Pavilion 14 ноутбуки 67900
Ноутбук ASUS VivoBook 15 ноутбуки 51490
4. Каталог: категории по алфавиту, внутри — от дорогих к дешёвым
────────────────────────────────────────────────────────────────
аксессуары Powerbank Anker 20000 мА·ч 4290
аксессуары Рюкзак для ноутбука Xiaomi 2990
аксессуары Кабель USB-C 2 м 790
комплектующие Видеокарта NVIDIA RTX 4060 38990
комплектующие Блок питания be quiet! 650 Вт 8490
комплектующие SSD Samsung 980 1 ТБ 7890
комплектующие Оперативная память Kingston 16 ГБ 4590
ноутбуки Ноутбук Apple MacBook Air 13 114990
5. Товары по категориям
───────────────────────
аксессуары 3 шт
комплектующие 4 шт
ноутбуки 5 шт
периферия 5 шт
смартфоны 4 шт
6. Заказы страницами по 5
─────────────────────────
страница 1
2026-0001 5980
2026-0003 86880
2026-0004 106210
2026-0006 2370
2026-0007 75790
страница 2
2026-0008 285750
2026-0009 320740
2026-0010 120340
2026-0011 85980
2026-0012 222740
7. Заказ с доставкой и позициями
────────────────────────────────
номер 2026-0001, статус доставлен, сумма 5980
доставка: Ярославль, пункт выдачи, 1 дн.
оплата: карта, проведена: true
2 × Рюкзак для ноутбука Xiaomi 2990
8. Песочница: вставка, обновление, удаление
───────────────────────────────────────────
вставлено: 2 ключи: p-301, p-302
обновление одного: найдено 1, изменено 1
пометка распродажи: найдено 5, изменено 5
что стало: {"title"=>"Док-станция USB-C", "price"=>5990, "reviews"=>1, "sale"=>true}
удалено: 1 · товаров в песочнице: 22
9. Итог
───────
прочитано из shop: 21 товаров и 120 заказов
изменено в песочнице: 1 вставка пачкой, 2 обновления, 1 удаление
§7Как запустить
Скрипт лежит в репозитории практик, в папке scripts, рядом с решениями работ. Перед запуском верните базы в исходное состояние — восьмой раздел пишет в песочницу.
# базы в исходное состояние docker compose run --rm reset # свой сервер: Python, Ruby, Go, C++ python3 scripts/shop_report.py ruby scripts/shop_report.rb go run scripts/shop_report.go g++ -std=c++17 scripts/shop_report.cpp -o report $(pkg-config --cflags --libs libmongocxx1) && ./report
docker compose run --rm python scripts/shop_report.py docker compose run --rm ruby scripts/shop_report.rb docker compose run --rm go scripts/shop_report.go docker compose run --rm cpp scripts/shop_report.cpp
Скрипт можно запускать сколько угодно раз подряд: он сам убирает за собой товары в песочнице. Пометка распродажи на исходных аксессуарах при этом останется — её снимает сброс баз.
§8Частые ошибки
| Что написано | Что происходит | Как правильно |
|---|---|---|
| Клиент создаётся внутри каждой функции отчёта | На каждый вызов поднимается новый пул соединений | Один MongoClient на программу, как в начале скрипта |
Запись идёт в shop | Данные курса разъезжаются, и числа в отчёте перестают совпадать с книгой | Писать в sandbox; вернуть данные — docker compose run --rm reset |
Страницы без sort | Порядок не гарантирован: документ может попасть на две страницы или пропасть | sort по уникальному полю перед skip |
print(cursor) | Печатается объект курсора, а не документы | Перебрать курсор циклом или превратить в список |
| Что написано | Что происходит | Как правильно |
|---|---|---|
auto doc = col.find_one(...)->view(); | Обращение к освобождённой памяти: временное значение исчезает после строки | Сохранить результат в переменную, потом брать view() |
std::setw для столбцов с кириллицей | Столбцы разъезжаются: в UTF-8 на букву приходится два байта | Считать ширину по символам — функция pad в скрипте |
Запись идёт в shop | Данные курса разъезжаются | Писать в sandbox; вернуть данные — docker compose run --rm reset |
mongocxx::instance создаётся дважды | Программа падает при старте | Один экземпляр на программу, первой строкой main |
| Что написано | Что происходит | Как правильно |
|---|---|---|
%-32s для столбцов с кириллицей | Столбцы разъезжаются: ширина считается в байтах | Считать ширину по символам — функция pad в скрипте |
Ошибка запроса присвоена _ | Пустой результат выглядит как «ничего не нашлось» | Проверять второе значение, которое возвращает драйвер |
Запись идёт в shop | Данные курса разъезжаются | Писать в sandbox; вернуть данные — docker compose run --rm reset |
defer client.Disconnect(ctx) забыт | Соединения остаются открытыми до конца процесса | Отложенное закрытие сразу после подключения |
| Что написано | Что происходит | Как правильно |
|---|---|---|
Результат find перебран дважды | Запрос выполняется заново при каждом обходе | Сохранить в массив: .to_a |
ljust по строке с кириллицей в таблице | Столбцы сходятся: Ruby считает символы, а не байты — ошибки нет | — |
Запись идёт в shop | Данные курса разъезжаются | Писать в sandbox; вернуть данные — docker compose run --rm reset |
Ключи документа — символы (:title) | Поля не находятся: в документах имена строковые | Писать ключи строками |
§9Проверьте себя
Почему отчёт читает данные из shop, а пишет в sandbox?
На данных shop построены примеры книги и ответы практик. Если скрипт изменит их, числа в отчёте перестанут совпадать с числами в главах. Песочница для того и заведена: её возвращают в исходное состояние одной командой.
Зачем в разделе со страницами задана сортировка, если нужны любые пять заказов?
Без сортировки порядок документов не определён. На второй странице может оказаться заказ с первой, а какой-то заказ не попадёт ни на одну страницу. Сортировка по номеру делает страницы устойчивыми.
Обновление пачкой напечатало «найдено 5, изменено 5», хотя скрипт вставил два товара. Почему?
Фильтр {"category": "аксессуары"} подошёл ко всем аксессуарам песочницы: трём исходным и двум новым. update_many меняет все документы, попавшие под фильтр.
Что напечатает скрипт, если запустить его дважды подряд?
То же самое. Перед вставкой он удаляет свои товары по ключам p-301 и p-302, поэтому повторный запуск не упирается в ошибку дубликата и не добавляет лишних документов.
Как получить тот же отчёт по другой базе, не меняя запросов?
Поменять в начале скрипта строку с именем базы: все запросы идут через переменные products, orders и box. Адрес сервера меняется переменной окружения MONGO_URI без правки файла.
§10Шпаргалка
клиент → база → коллекции
разделы отчёта по одной задаче
закрыть клиента в конце
MONGO_URI из окружения,
localhost — запасной вариант
одинаковые ширины столбцов
заголовок и строки одним форматом
сначала убрать свои документы,
потом вставлять
вставка — ключи
обновление — найдено и изменено
удаление — сколько удалено
docker compose run --rm reset
docker compose run --rm python \
scripts/shop_report.py
Итог главы
Скрипт собирает операции модуля 1 в одну программу: подключение, справку по базе, карточку документа, таблицы с проекцией и сортировкой, страницы, вложенные поля и полный цикл записи в песочнице. Каждый запрос печатает результат так, как его читает человек: счётчиком, строкой таблицы или числом изменённых документов. Данные читаются из баз курса, а меняются только в песочнице, поэтому скрипт можно запускать сколько угодно раз. Дальше — практическая работа ПР-01, где такую же программу нужно написать самому.