mongodb (native driver) / mongoose → go.mongodb.org/mongo-driver
await collection.insertOne(doc) → col.InsertOne(ctx, doc)
await collection.find(filter).toArray() → cursor.All(ctx, &results)
Schema + Model (Mongoose) → структуры с тегами bson
aggregate([...]) → col.Aggregate(ctx, pipeline)
$lookup, $group, $match — точно такие же стадии
bson:"fieldname,omitempty" — аналог @Prop()$skip/$limit и cursor-basedmkdir go-mongodb && cd go-mongodb
go mod init go-mongodb
# Устанавливаем драйвер MongoDB
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/bson
go get github.com/google/uuid
go mod tidy
# Запускаем MongoDB
docker run -d --name mongo-lesson15 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=secret \
-p 27017:27017 \
mongo:7
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ 1. МОДЕЛИ ДАННЫХ (теги bson!) ║
// ╚══════════════════════════════════════════════════════════╝
// Product — товар в каталоге.
// Теги bson работают как json-теги, но для MongoDB.
// omitempty — не сохранять поле, если оно пустое.
type Product struct {
// primitive.ObjectID — стандартный тип для _id в MongoDB
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Name string `bson:"name" json:"name"`
Description string `bson:"description,omitempty" json:"description,omitempty"`
Price float64 `bson:"price" json:"price"` // В реальности — int64 (центы)!
Category string `bson:"category" json:"category"`
Tags []string `bson:"tags,omitempty" json:"tags,omitempty"`
InStock bool `bson:"in_stock" json:"in_stock"`
StockCount int `bson:"stock_count" json:"stock_count"`
Rating *float64 `bson:"rating,omitempty" json:"rating,omitempty"` // Указатель для NULL
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
// Order — заказ (с вложенным документом)
type Order struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
CustomerID string `bson:"customer_id" json:"customer_id"`
Items []OrderItem `bson:"items" json:"items"` // Вложенный массив документов
TotalPrice float64 `bson:"total_price" json:"total_price"`
Status string `bson:"status" json:"status"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
}
// OrderItem — элемент заказа (вложенный документ)
type OrderItem struct {
ProductID primitive.ObjectID `bson:"product_id" json:"product_id"`
Name string `bson:"name" json:"name"`
Quantity int `bson:"quantity" json:"quantity"`
Price float64 `bson:"price" json:"price"`
}
// ProductRepository — репозиторий для работы с товарами
type ProductRepository struct {
col *mongo.Collection
}
// OrderRepository — репозиторий для работы с заказами
type OrderRepository struct {
col *mongo.Collection
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 2. ПОДКЛЮЧЕНИЕ К MONGODB ║
// ╚══════════════════════════════════════════════════════════╝
func connectMongo(ctx context.Context, uri, dbName string) (*mongo.Database, error) {
// Настройка клиента
clientOpts := options.Client().
ApplyURI(uri).
SetMaxPoolSize(100). // Максимальный размер пула соединений
SetMinPoolSize(10). // Минимальный размер
SetMaxConnIdleTime(5 * time.Minute).
SetConnectTimeout(10 * time.Second)
// Подключение
client, err := mongo.Connect(ctx, clientOpts)
if err != nil {
return nil, fmt.Errorf("mongo connect: %w", err)
}
// Проверка соединения (ping)
if err := client.Ping(ctx, readpref.Primary()); err != nil {
return nil, fmt.Errorf("mongo ping: %w", err)
}
log.Println("✅ Подключено к MongoDB")
return client.Database(dbName), nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 3. СОЗДАНИЕ ИНДЕКСОВ ║
// ╚══════════════════════════════════════════════════════════╝
func createIndexes(ctx context.Context, db *mongo.Database) error {
productCol := db.Collection("products")
orderCol := db.Collection("orders")
// Индексы для продуктов
productIndexes := []mongo.IndexModel{
{
// Обычный индекс по категории
Keys: bson.D{{Key: "category", Value: 1}},
},
{
// Составной индекс: категория + цена (для сортировки)
Keys: bson.D{
{Key: "category", Value: 1},
{Key: "price", Value: -1}, // -1 = по убыванию
},
},
{
// Уникальный индекс по имени (не может быть двух товаров с одинаковым именем)
Keys: bson.D{{Key: "name", Value: 1}},
Options: options.Index().SetUnique(true),
},
{
// Текстовый индекс для полнотекстового поиска
Keys: bson.D{
{Key: "name", Value: "text"},
{Key: "description", Value: "text"},
},
Options: options.Index().SetName("text_search_idx"),
},
{
// TTL-индекс: автоматически удаляет документы через 90 дней
Keys: bson.D{{Key: "created_at", Value: 1}},
Options: options.Index().SetExpireAfterSeconds(90 * 24 * 3600),
},
}
if _, err := productCol.Indexes().CreateMany(ctx, productIndexes); err != nil {
return fmt.Errorf("create product indexes: %w", err)
}
// Индексы для заказов
orderIndexes := []mongo.IndexModel{
{
Keys: bson.D{{Key: "customer_id", Value: 1}},
},
{
Keys: bson.D{{Key: "status", Value: 1}},
},
{
Keys: bson.D{{Key: "items.product_id", Value: 1}},
Options: options.Index().SetName("items_product_idx"),
},
}
if _, err := orderCol.Indexes().CreateMany(ctx, orderIndexes); err != nil {
return fmt.Errorf("create order indexes: %w", err)
}
log.Println(" ✓ Индексы созданы")
return nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 4. CRUD ОПЕРАЦИИ ║
// ╚══════════════════════════════════════════════════════════╝
// NewProductRepository — конструктор
func NewProductRepository(db *mongo.Database) *ProductRepository {
return &ProductRepository{col: db.Collection("products")}
}
// Create — создаёт товар
func (r *ProductRepository) Create(ctx context.Context, product *Product) error {
product.ID = primitive.NewObjectID()
product.CreatedAt = time.Now()
product.UpdatedAt = time.Now()
result, err := r.col.InsertOne(ctx, product)
if err != nil {
return fmt.Errorf("insert product: %w", err)
}
product.ID = result.InsertedID.(primitive.ObjectID)
return nil
}
// GetByID — находит товар по ID
func (r *ProductRepository) GetByID(ctx context.Context, id primitive.ObjectID) (*Product, error) {
var product Product
err := r.col.FindOne(ctx, bson.M{"_id": id}).Decode(&product)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return nil, fmt.Errorf("product not found")
}
return nil, fmt.Errorf("find product: %w", err)
}
return &product, nil
}
// List — список товаров с фильтрацией и пагинацией
func (r *ProductRepository) List(ctx context.Context, filter bson.M, limit, offset int64) ([]Product, error) {
opts := options.Find().
SetLimit(limit).
SetSkip(offset).
SetSort(bson.D{{Key: "created_at", Value: -1}}) // Сначала новые
cursor, err := r.col.Find(ctx, filter, opts)
if err != nil {
return nil, fmt.Errorf("find products: %w", err)
}
defer cursor.Close(ctx)
// cursor.All — декодирует все документы в слайс
var products []Product
if err := cursor.All(ctx, &products); err != nil {
return nil, fmt.Errorf("decode products: %w", err)
}
if products == nil {
products = []Product{}
}
return products, nil
}
// Update — обновляет товар
func (r *ProductRepository) Update(ctx context.Context, id primitive.ObjectID, update bson.M) error {
update["updated_at"] = time.Now()
result, err := r.col.UpdateOne(
ctx,
bson.M{"_id": id},
bson.M{"$set": update},
)
if err != nil {
return fmt.Errorf("update product: %w", err)
}
if result.MatchedCount == 0 {
return fmt.Errorf("product not found")
}
return nil
}
// Delete — удаляет товар
func (r *ProductRepository) Delete(ctx context.Context, id primitive.ObjectID) error {
result, err := r.col.DeleteOne(ctx, bson.M{"_id": id})
if err != nil {
return fmt.Errorf("delete product: %w", err)
}
if result.DeletedCount == 0 {
return fmt.Errorf("product not found")
}
return nil
}
// Search — текстовый поиск
func (r *ProductRepository) Search(ctx context.Context, query string) ([]Product, error) {
filter := bson.M{
"$text": bson.M{"$search": query},
}
// Сортировка по релевантности (текстовый счёт)
opts := options.Find().SetSort(bson.D{{Key: "score", Value: bson.M{"$meta": "textScore"}}})
cursor, err := r.col.Find(ctx, filter, opts)
if err != nil {
return nil, fmt.Errorf("search products: %w", err)
}
defer cursor.Close(ctx)
var products []Product
if err := cursor.All(ctx, &products); err != nil {
return nil, fmt.Errorf("decode search: %w", err)
}
return products, nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ 5. АГРЕГАЦИИ ║
// ╚══════════════════════════════════════════════════════════╝
// AggregationResult — результат агрегации
type AggregationResult struct {
Category string `bson:"_id" json:"category"`
Count int `bson:"count" json:"count"`
AvgPrice float64 `bson:"avg_price" json:"avg_price"`
TotalStock int `bson:"total_stock" json:"total_stock"`
}
// AggregateByCategory — агрегация: группировка по категориям
func (r *ProductRepository) AggregateByCategory(ctx context.Context) ([]AggregationResult, error) {
pipeline := mongo.Pipeline{
// Стадия 1: только товары в наличии
{{Key: "$match", Value: bson.M{"in_stock": true}}},
// Стадия 2: группировка
{{Key: "$group", Value: bson.M{
"_id": "$category",
"count": bson.M{"$sum": 1},
"avg_price": bson.M{"$avg": "$price"},
"total_stock": bson.M{"$sum": "$stock_count"},
}}},
// Стадия 3: сортировка по количеству
{{Key: "$sort", Value: bson.M{"count": -1}}},
}
cursor, err := r.col.Aggregate(ctx, pipeline)
if err != nil {
return nil, fmt.Errorf("aggregate: %w", err)
}
defer cursor.Close(ctx)
var results []AggregationResult
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("decode aggregate: %w", err)
}
return results, nil
}
// NewOrderRepository — конструктор
func NewOrderRepository(db *mongo.Database) *OrderRepository {
return &OrderRepository{col: db.Collection("orders")}
}
// CreateOrder — создаёт заказ
func (r *OrderRepository) CreateOrder(ctx context.Context, order *Order) error {
order.ID = primitive.NewObjectID()
order.CreatedAt = time.Now()
order.Status = "pending"
_, err := r.col.InsertOne(ctx, order)
return err
}
// GetOrdersWithProducts — агрегация с $lookup (заказы + товары)
func (r *OrderRepository) GetOrdersWithProducts(ctx context.Context, customerID string) ([]bson.M, error) {
pipeline := mongo.Pipeline{
// Фильтр по клиенту
{{Key: "$match", Value: bson.M{"customer_id": customerID}}},
// Разворачиваем массив items
{{Key: "$unwind", Value: "$items"}},
// JOIN с коллекцией products
{{Key: "$lookup", Value: bson.M{
"from": "products",
"localField": "items.product_id",
"foreignField": "_id",
"as": "product_details",
}}},
// Разворачиваем product_details
{{Key: "$unwind", Value: bson.M{
"path": "$product_details",
"preserveNullAndEmptyArrays": true,
}}},
// Группируем обратно в заказы
{{Key: "$group", Value: bson.M{
"_id": "$_id",
"customer_id": bson.M{"$first": "$customer_id"},
"status": bson.M{"$first": "$status"},
"total_price": bson.M{"$first": "$total_price"},
"created_at": bson.M{"$first": "$created_at"},
"items": bson.M{"$push": bson.M{
"name": "$items.name",
"quantity": "$items.quantity",
"price": "$items.price",
"product": "$product_details",
}},
}}},
{{Key: "$sort", Value: bson.M{"created_at": -1}}},
}
cursor, err := r.col.Aggregate(ctx, pipeline)
if err != nil {
return nil, fmt.Errorf("aggregate orders: %w", err)
}
defer cursor.Close(ctx)
var results []bson.M
if err := cursor.All(ctx, &results); err != nil {
return nil, fmt.Errorf("decode orders: %w", err)
}
return results, nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ MAIN ║
// ╚══════════════════════════════════════════════════════════╝
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🍃 MongoDB CRUD + индексы + агрегации")
ctx := context.Background()
// Подключение
mongoURI := "mongodb://admin:secret@localhost:27017"
db, err := connectMongo(ctx, mongoURI, "shopdb")
if err != nil {
log.Fatalf("Подключение: %v", err)
}
// Создание индексов
if err := createIndexes(ctx, db); err != nil {
log.Fatalf("Индексы: %v", err)
}
productRepo := NewProductRepository(db)
orderRepo := NewOrderRepository(db)
// ==========================================
// 1. CREATE — добавляем товары
// ==========================================
log.Println("\n── CREATE ──")
products := []Product{
{Name: "MacBook Pro", Description: "Ноутбук Apple", Price: 1999.99, Category: "electronics", Tags: []string{"apple", "laptop"}, InStock: true, StockCount: 15},
{Name: "iPhone 15", Description: "Смартфон Apple", Price: 999.99, Category: "electronics", Tags: []string{"apple", "phone"}, InStock: true, StockCount: 25},
{Name: "AirPods Pro", Description: "Наушники", Price: 249.99, Category: "electronics", Tags: []string{"apple", "audio"}, InStock: true, StockCount: 50},
{Name: "Книга «Go для профи»", Description: "Учебник", Price: 49.99, Category: "books", Tags: []string{"go", "programming"}, InStock: true, StockCount: 100},
{Name: "Кофемашина", Description: "Для офиса", Price: 599.99, Category: "appliances", Tags: []string{"coffee", "office"}, InStock: false, StockCount: 0},
}
for _, p := range products {
p := p // Копия для безопасной передачи указателя
if err := productRepo.Create(ctx, &p); err != nil {
log.Printf(" Ошибка создания %s: %v", p.Name, err)
} else {
log.Printf(" ✓ Создан: %s ($%.2f) [%s]", p.Name, p.Price, p.ID.Hex())
}
}
// ==========================================
// 2. READ — поиск и фильтрация
// ==========================================
log.Println("\n── FIND ──")
// Все товары в категории electronics
electronics, err := productRepo.List(ctx, bson.M{"category": "electronics"}, 10, 0)
if err != nil {
log.Printf(" Ошибка: %v", err)
} else {
log.Printf(" Товары в electronics (%d):", len(electronics))
for _, p := range electronics {
log.Printf(" - %s: $%.2f (в наличии: %d)", p.Name, p.Price, p.StockCount)
}
}
// Товары дороже $500
expensive, _ := productRepo.List(ctx, bson.M{"price": bson.M{"$gte": 500}}, 10, 0)
log.Printf(" Дорогие товары (≥$500): %d", len(expensive))
// Текстовый поиск
results, _ := productRepo.Search(ctx, "apple")
log.Printf(" Поиск 'apple': %d результатов", len(results))
// ==========================================
// 3. UPDATE
// ==========================================
log.Println("\n── UPDATE ──")
if len(electronics) > 0 {
err := productRepo.Update(ctx, electronics[0].ID, bson.M{
"price": 1899.99,
"stock_count": 10,
})
if err != nil {
log.Printf(" Ошибка обновления: %v", err)
} else {
log.Printf(" ✓ Обновлён: %s", electronics[0].Name)
}
}
// ==========================================
// 4. АГРЕГАЦИИ
// ==========================================
log.Println("\n── АГРЕГАЦИИ ──")
aggResults, err := productRepo.AggregateByCategory(ctx)
if err != nil {
log.Printf(" Ошибка агрегации: %v", err)
} else {
log.Println(" Группировка по категориям:")
for _, r := range aggResults {
log.Printf(" %s: %d товаров, средняя цена $%.2f, всего на складе %d",
r.Category, r.Count, r.AvgPrice, r.TotalStock)
}
}
// ==========================================
// 5. СОЗДАНИЕ ЗАКАЗА
// ==========================================
log.Println("\n── ЗАКАЗЫ ──")
if len(electronics) >= 2 {
order := &Order{
CustomerID: "user-42",
Items: []OrderItem{
{ProductID: electronics[0].ID, Name: electronics[0].Name, Quantity: 1, Price: electronics[0].Price},
{ProductID: electronics[1].ID, Name: electronics[1].Name, Quantity: 2, Price: electronics[1].Price},
},
TotalPrice: electronics[0].Price + 2*electronics[1].Price,
}
if err := orderRepo.CreateOrder(ctx, order); err != nil {
log.Printf(" Ошибка создания заказа: %v", err)
} else {
log.Printf(" ✓ Заказ создан: %s (сумма $%.2f)", order.ID.Hex(), order.TotalPrice)
}
}
log.Println("\n✅ Демонстрация завершена!")
}
# Запускаем MongoDB
docker run -d --name mongo-lesson15 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=secret \
-p 27017:27017 \
mongo:7
# Запускаем программу
go run main.go
# Подключаемся к MongoDB для проверки
docker exec -it mongo-lesson15 mongosh -u admin -p secret
# В mongosh:
use shopdb
db.products.find().pretty()
db.products.getIndexes()
db.orders.find().pretty()
| Тип | Синтаксис | Когда использовать |
|---|---|---|
bson.D | bson.D{{Key: "name", Value: "Alice"}} | Когда важен порядок полей (индексы, агрегации) |
bson.M | bson.M{"name": "Alice"} | Фильтры, обновления (удобнее как map) |
bson.A | bson.A{"apple", "banana"} | Массивы (в агрегациях) |
go run main.go
# Сборка
go build -o mongo-demo main.go
./mongo-demo
bson:"...", не json:"...".
bson:"_id,omitempty" — обязательно, иначе создаётся нулевой ObjectID.💡 Best practices от сеньоров:
*float64 для рейтинга, которого может не быть.bson:"fieldname" — имя поля в документе.
💡 Для Node.js разработчика:
bson.M — как обычный объект {} в JS, но с типизацией.primitive.ObjectID — как new mongoose.Types.ObjectId().$match, $group, $lookup.