Модуль 2 · Язык фильтров

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

1.9

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, функции — выше main
Запускcd 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.0, §10.

§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 и limit1.6
7. Заказ с доставкой и позициямивложенные документы и массивы в выводе1.2
8. Песочницаinsert_many, update_one, update_many, delete_one1.3, 1.7, 1.8
9. Итогчисла, которые попадают в отчёт—

Скрипт лежит в репозитории практик, в папке scripts, и запускается одной командой (§7). Главу читают вместе с открытым файлом.

§2Подключение и разделы отчёта

Начало скрипта — то же, что в главе 1.1: клиент, база, коллекции. Адрес сервера берётся из переменной окружения MONGO_URI, а если её нет — подставляется localhost. Так один и тот же файл работает и на компьютере, и в контейнере.

Рядом объявлена функция title: она печатает заголовок раздела и подчёркивает его. Отчёт читает человек, и разделы должны быть видны с первого взгляда.

Python · PyMongoподключение и заголовки
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))
C++ · mongocxxподключение и заголовки
#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"];
Go · mongo-driverподключение и заголовки
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")
Ruby · mongoподключение и заголовки
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Чтение: справка, карточка, таблицы

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

Python · PyMongoсправка и карточка товара
# ── 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}")
C++ · mongocxxсправка и карточка товара
    // ── 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;
Go · mongo-driverсправка и карточка товара
	// ── 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)
	}
Ruby · mongoсправка и карточка товара
# ── 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. Драйвер отдаёт их значениями языка, поэтому печатать их можно как обычные переменные.

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

Python · PyMongoтаблица и сортировка по двум полям
# ── 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}")
C++ · mongocxxтаблица и сортировка по двум полям
    // ── 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;
    }
Go · mongo-driverтаблица и сортировка по двум полям
	// ── 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))
	}
Ruby · mongoтаблица и сортировка по двум полям
# ── 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 ограничивает её размер.

Python · PyMongoсчётчики и страницы
# ── 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}")
C++ · mongocxxсчётчики и страницы
    // ── 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;
        }
    }
Go · mongo-driverсчётчики и страницы
	// ── 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))
		}
	}
Ruby · mongoсчётчики и страницы
# ── 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).

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

Python · PyMongoвложенные документы и массив позиций
# ── 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}")
C++ · mongocxxвложенные документы и массив позиций
    // ── 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;
    }
Go · mongo-driverвложенные документы и массив позиций
	// ── 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))
	}
Ruby · mongoвложенные документы и массив позиций
# ── 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 остаётся нетронутой. Сначала он удаляет свои товары, если они остались от прошлого запуска: скрипт должен давать один и тот же результат при каждом запуске.

Python · PyMongoвставка, обновление, удаление
# ── 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({}))
C++ · mongocxxвставка, обновление, удаление
    // ── 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;
Go · mongo-driverвставка, обновление, удаление
	// ── 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)
Ruby · mongoвставка, обновление, удаление
# ── 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Скрипт целиком

Ниже — тот же файл без пропусков, а под ним вывод его запуска на стенде курса. Числа в выводе совпадут с вашими, если базы перед запуском в исходном состоянии.

Python · PyMongoscripts/shop_report
"""Отчёт по магазину: что умеет 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()
C++ · mongocxxscripts/shop_report
// Отчёт по магазину: что умеет 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;
}
Go · mongo-driverscripts/shop_report
// Отчёт по магазину: что умеет 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 удаление")
}
Ruby · mongoscripts/shop_report
# Отчёт по магазину: что умеет 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, где такую же программу нужно написать самому.

Глава 1.9 · Справочник по MongoDB Автор: Макаров Максим Николаевич