MongoDB #

MongoDB adalah database NoSQL berbasis dokumen yang menyimpan data dalam format BSON (Binary JSON). Alih-alih tabel dan baris seperti SQL, MongoDB menggunakan koleksi (collection) dan dokumen (document) — setiap dokumen adalah struktur JSON yang fleksibel tanpa skema yang kaku. Go mendukung MongoDB melalui driver resmi go.mongodb.org/mongo-driver yang dikembangkan oleh MongoDB Inc. sendiri.

Instalasi #

go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/bson
go get go.mongodb.org/mongo-driver/mongo/options

Koneksi ke MongoDB #

import (
    "context"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)

func connectMongo(uri string) (*mongo.Client, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Buat client dengan opsi
    opts := options.Client().
        ApplyURI(uri).
        SetMaxPoolSize(25).
        SetMinPoolSize(5).
        SetMaxConnIdleTime(1 * time.Minute).
        SetServerSelectionTimeout(5 * time.Second)

    client, err := mongo.Connect(ctx, opts)
    if err != nil {
        return nil, fmt.Errorf("connect: %w", err)
    }

    // Ping untuk verifikasi koneksi
    if err := client.Ping(ctx, readpref.Primary()); err != nil {
        return nil, fmt.Errorf("ping: %w", err)
    }

    return client, nil
}

func main() {
    // Format URI: mongodb://user:pass@host:port/dbname
    client, err := connectMongo("mongodb://localhost:27017")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Disconnect(context.Background())

    // Akses database dan koleksi
    db := client.Database("tokoonline")
    products := db.Collection("products")
    _ = products
}

BSON — Format Data MongoDB #

BSON adalah cara MongoDB menyimpan dan menerima data. Driver Go menyediakan beberapa cara untuk bekerja dengan BSON:

import (
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
)

// bson.D — slice of key-value pairs, urutan terjaga (untuk filter, sort, dll)
filter := bson.D{
    {Key: "category", Value: "elektronik"},
    {Key: "price", Value: bson.D{{Key: "$lte", Value: 5_000_000}}},
}

// bson.M — map (tidak berurutan, lebih ringkas untuk dokumen sederhana)
filter2 := bson.M{
    "category": "elektronik",
    "price":    bson.M{"$lte": 5_000_000},
}

// bson.A — array (untuk $in, $and, $or, dll)
filter3 := bson.M{
    "category": bson.M{"$in": bson.A{"elektronik", "gaming"}},
}

// primitive.ObjectID — tipe ID dokumen MongoDB
id, _ := primitive.ObjectIDFromHex("507f1f77bcf86cd799439011")
filter4 := bson.M{"_id": id}

// Buat ObjectID baru
newID := primitive.NewObjectID()
fmt.Println(newID.Hex())  // "507f1f77bcf86cd799439011"

Mendefinisikan Struct dengan BSON Tags #

type Product struct {
    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"`
    Stock       int                `bson:"stock" json:"stock"`
    Category    string             `bson:"category" json:"category"`
    Tags        []string           `bson:"tags,omitempty" json:"tags,omitempty"`
    Specs       map[string]string  `bson:"specs,omitempty" json:"specs,omitempty"`
    IsActive    bool               `bson:"is_active" json:"is_active"`
    CreatedAt   time.Time          `bson:"created_at" json:"created_at"`
    UpdatedAt   time.Time          `bson:"updated_at" json:"updated_at"`
}

type Review struct {
    ID        primitive.ObjectID `bson:"_id,omitempty"`
    ProductID primitive.ObjectID `bson:"product_id"`
    UserID    string             `bson:"user_id"`
    Rating    int                `bson:"rating"`   // 1-5
    Comment   string             `bson:"comment,omitempty"`
    CreatedAt time.Time          `bson:"created_at"`
}

Create — Menyimpan Dokumen #

func createProduct(ctx context.Context, col *mongo.Collection, p *Product) error {
    p.ID = primitive.NewObjectID()
    p.CreatedAt = time.Now()
    p.UpdatedAt = time.Now()

    result, err := col.InsertOne(ctx, p)
    if err != nil {
        return fmt.Errorf("insert: %w", err)
    }
    fmt.Println("ID baru:", result.InsertedID)
    return nil
}

// Insert banyak dokumen sekaligus
func createMany(ctx context.Context, col *mongo.Collection, products []Product) error {
    docs := make([]interface{}, len(products))
    for i := range products {
        products[i].ID = primitive.NewObjectID()
        products[i].CreatedAt = time.Now()
        products[i].UpdatedAt = time.Now()
        docs[i] = products[i]
    }

    opts := options.InsertMany().SetOrdered(false) // lanjutkan meski ada yang gagal
    result, err := col.InsertMany(ctx, docs, opts)
    if err != nil {
        return fmt.Errorf("insert many: %w", err)
    }
    fmt.Printf("Berhasil insert %d dokumen\n", len(result.InsertedIDs))
    return nil
}

Read — Membaca Dokumen #

// FindOne — satu dokumen
func getProduct(ctx context.Context, col *mongo.Collection, id string) (*Product, error) {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return nil, fmt.Errorf("ID tidak valid: %w", err)
    }

    var product Product
    err = col.FindOne(ctx, bson.M{"_id": oid}).Decode(&product)
    if err != nil {
        if errors.Is(err, mongo.ErrNoDocuments) {
            return nil, ErrNotFound
        }
        return nil, fmt.Errorf("find: %w", err)
    }
    return &product, nil
}

// Find — banyak dokumen
func listProducts(ctx context.Context, col *mongo.Collection, category string) ([]*Product, error) {
    filter := bson.M{"category": category, "is_active": true}

    // Opsi: sort, limit, skip, projection
    opts := options.Find().
        SetSort(bson.D{{Key: "price", Value: 1}}).   // sort ascending by price
        SetLimit(50).
        SetProjection(bson.M{                          // hanya ambil field tertentu
            "name": 1, "price": 1, "stock": 1, "category": 1,
        })

    cursor, err := col.Find(ctx, filter, opts)
    if err != nil {
        return nil, fmt.Errorf("find: %w", err)
    }
    defer cursor.Close(ctx)

    var products []*Product
    if err := cursor.All(ctx, &products); err != nil {
        return nil, fmt.Errorf("decode: %w", err)
    }
    return products, nil
}

// Count
func countProducts(ctx context.Context, col *mongo.Collection, filter bson.M) (int64, error) {
    return col.CountDocuments(ctx, filter)
}

// Dengan pagination
func listPaged(ctx context.Context, col *mongo.Collection, page, perPage int) ([]*Product, error) {
    skip := int64((page - 1) * perPage)
    opts := options.Find().
        SetSkip(skip).
        SetLimit(int64(perPage)).
        SetSort(bson.D{{Key: "created_at", Value: -1}})

    cursor, err := col.Find(ctx, bson.M{"is_active": true}, opts)
    if err != nil {
        return nil, err
    }
    defer cursor.Close(ctx)

    var products []*Product
    return products, cursor.All(ctx, &products)
}

// Text search (butuh text index)
func searchProducts(ctx context.Context, col *mongo.Collection, keyword string) ([]*Product, error) {
    filter := bson.M{"$text": bson.M{"$search": keyword}}
    opts := options.Find().
        SetProjection(bson.M{"score": bson.M{"$meta": "textScore"}}).
        SetSort(bson.M{"score": bson.M{"$meta": "textScore"}})

    cursor, err := col.Find(ctx, filter, opts)
    if err != nil {
        return nil, err
    }
    defer cursor.Close(ctx)

    var products []*Product
    return products, cursor.All(ctx, &products)
}

Update — Memperbarui Dokumen #

// UpdateOne — update satu dokumen
func updateProduct(ctx context.Context, col *mongo.Collection, id string, updates bson.M) error {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return fmt.Errorf("ID tidak valid: %w", err)
    }

    result, err := col.UpdateOne(ctx,
        bson.M{"_id": oid},
        bson.M{
            "$set": updates,
            "$currentDate": bson.M{"updated_at": true},
        },
    )
    if err != nil {
        return fmt.Errorf("update: %w", err)
    }
    if result.MatchedCount == 0 {
        return ErrNotFound
    }
    return nil
}

// UpdateMany — update banyak dokumen
func deactivateCategory(ctx context.Context, col *mongo.Collection, category string) (int64, error) {
    result, err := col.UpdateMany(ctx,
        bson.M{"category": category},
        bson.M{"$set": bson.M{"is_active": false}},
    )
    if err != nil {
        return 0, err
    }
    return result.ModifiedCount, nil
}

// FindOneAndUpdate — atomik find + update, kembalikan dokumen setelah update
func decrementStock(ctx context.Context, col *mongo.Collection, id string, qty int) (*Product, error) {
    oid, _ := primitive.ObjectIDFromHex(id)

    opts := options.FindOneAndUpdate().
        SetReturnDocument(options.After)  // kembalikan dokumen setelah update

    var updated Product
    err := col.FindOneAndUpdate(ctx,
        bson.M{"_id": oid, "stock": bson.M{"$gte": qty}},  // pastikan stok cukup
        bson.M{
            "$inc": bson.M{"stock": -qty},
            "$currentDate": bson.M{"updated_at": true},
        },
        opts,
    ).Decode(&updated)

    if errors.Is(err, mongo.ErrNoDocuments) {
        return nil, errors.New("stok tidak mencukupi atau produk tidak ditemukan")
    }
    return &updated, err
}

// Upsert — insert jika tidak ada, update jika ada
func upsertProduct(ctx context.Context, col *mongo.Collection, p *Product) error {
    opts := options.Update().SetUpsert(true)
    _, err := col.UpdateOne(ctx,
        bson.M{"name": p.Name},
        bson.M{
            "$set": p,
            "$setOnInsert": bson.M{
                "_id":        primitive.NewObjectID(),
                "created_at": time.Now(),
            },
        },
        opts,
    )
    return err
}

Delete #

func deleteProduct(ctx context.Context, col *mongo.Collection, id string) error {
    oid, _ := primitive.ObjectIDFromHex(id)
    result, err := col.DeleteOne(ctx, bson.M{"_id": oid})
    if err != nil {
        return err
    }
    if result.DeletedCount == 0 {
        return ErrNotFound
    }
    return nil
}

// Soft delete — set is_active = false
func softDelete(ctx context.Context, col *mongo.Collection, id string) error {
    oid, _ := primitive.ObjectIDFromHex(id)
    return col.FindOneAndUpdate(ctx,
        bson.M{"_id": oid},
        bson.M{"$set": bson.M{
            "is_active":  false,
            "deleted_at": time.Now(),
        }},
        options.FindOneAndUpdate(),
    ).Err()
}

Aggregation Pipeline #

Aggregation pipeline adalah cara paling powerful untuk query kompleks di MongoDB:

func productStats(ctx context.Context, col *mongo.Collection) error {
    pipeline := mongo.Pipeline{
        // Stage 1: filter hanya produk aktif
        {{Key: "$match", Value: bson.M{"is_active": true}}},

        // Stage 2: group by category
        {{Key: "$group", Value: bson.D{
            {Key: "_id", Value: "$category"},
            {Key: "count", Value: bson.M{"$sum": 1}},
            {Key: "avg_price", Value: bson.M{"$avg": "$price"}},
            {Key: "total_stock", Value: bson.M{"$sum": "$stock"}},
            {Key: "max_price", Value: bson.M{"$max": "$price"}},
            {Key: "min_price", Value: bson.M{"$min": "$price"}},
        }}},

        // Stage 3: sort by count desc
        {{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},

        // Stage 4: format output
        {{Key: "$project", Value: bson.D{
            {Key: "category", Value: "$_id"},
            {Key: "count", Value: 1},
            {Key: "avg_price", Value: bson.M{"$round": bson.A{"$avg_price", 0}}},
            {Key: "total_stock", Value: 1},
            {Key: "_id", Value: 0},
        }}},
    }

    cursor, err := col.Aggregate(ctx, pipeline)
    if err != nil {
        return fmt.Errorf("aggregate: %w", err)
    }
    defer cursor.Close(ctx)

    var results []bson.M
    if err := cursor.All(ctx, &results); err != nil {
        return err
    }

    for _, r := range results {
        fmt.Printf("  %-15s: %v produk, avg Rp%.0f, stok %v\n",
            r["category"], r["count"], r["avg_price"], r["total_stock"])
    }
    return nil
}

Indexing #

Index sangat penting untuk performa query MongoDB:

func createIndexes(ctx context.Context, col *mongo.Collection) error {
    indexes := []mongo.IndexModel{
        // Index untuk filter yang sering dipakai
        {
            Keys: bson.D{{Key: "category", Value: 1}, {Key: "is_active", Value: 1}},
        },
        // Index untuk sort by price
        {
            Keys: bson.D{{Key: "price", Value: 1}},
        },
        // Unique index
        {
            Keys:    bson.D{{Key: "sku", Value: 1}},
            Options: options.Index().SetUnique(true).SetSparse(true),
        },
        // Text index untuk full-text search
        {
            Keys: bson.D{
                {Key: "name", Value: "text"},
                {Key: "description", Value: "text"},
            },
            Options: options.Index().SetName("text_search"),
        },
        // TTL index — dokumen otomatis dihapus setelah durasi tertentu
        {
            Keys:    bson.D{{Key: "expires_at", Value: 1}},
            Options: options.Index().SetExpireAfterSeconds(0),
        },
    }

    _, err := col.Indexes().CreateMany(ctx, indexes)
    return err
}

Transaksi Multi-Dokumen #

MongoDB 4.0+ mendukung transaksi ACID multi-dokumen (hanya pada replica set atau sharded cluster):

func placeOrder(ctx context.Context, client *mongo.Client, order Order) error {
    products := client.Database("tokoonline").Collection("products")
    orders := client.Database("tokoonline").Collection("orders")

    session, err := client.StartSession()
    if err != nil {
        return fmt.Errorf("start session: %w", err)
    }
    defer session.EndSession(ctx)

    _, err = session.WithTransaction(ctx, func(sc mongo.SessionContext) (interface{}, error) {
        // Kurangi stok setiap item
        for _, item := range order.Items {
            result, err := products.UpdateOne(sc,
                bson.M{
                    "_id":   item.ProductID,
                    "stock": bson.M{"$gte": item.Qty},
                },
                bson.M{"$inc": bson.M{"stock": -item.Qty}},
            )
            if err != nil {
                return nil, err
            }
            if result.MatchedCount == 0 {
                return nil, fmt.Errorf("stok produk tidak mencukupi")
            }
        }

        // Simpan order
        order.ID = primitive.NewObjectID()
        order.CreatedAt = time.Now()
        if _, err := orders.InsertOne(sc, order); err != nil {
            return nil, err
        }

        return nil, nil
    })
    return err
}

Contoh Program Lengkap — Repository Pattern #

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"
)

var ErrNotFound = errors.New("dokumen tidak ditemukan")

type Product struct {
    ID        primitive.ObjectID `bson:"_id,omitempty" json:"id"`
    Name      string             `bson:"name" json:"name"`
    Price     float64            `bson:"price" json:"price"`
    Stock     int                `bson:"stock" json:"stock"`
    Category  string             `bson:"category" json:"category"`
    Tags      []string           `bson:"tags,omitempty" json:"tags,omitempty"`
    IsActive  bool               `bson:"is_active" json:"is_active"`
    CreatedAt time.Time          `bson:"created_at" json:"created_at"`
    UpdatedAt time.Time          `bson:"updated_at" json:"updated_at"`
}

type ProductRepo struct {
    col *mongo.Collection
}

func NewProductRepo(db *mongo.Database) *ProductRepo {
    return &ProductRepo{col: db.Collection("products")}
}

func (r *ProductRepo) Create(ctx context.Context, p *Product) error {
    p.ID = primitive.NewObjectID()
    p.IsActive = true
    p.CreatedAt = time.Now()
    p.UpdatedAt = time.Now()
    _, err := r.col.InsertOne(ctx, p)
    return err
}

func (r *ProductRepo) FindByID(ctx context.Context, id string) (*Product, error) {
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return nil, fmt.Errorf("ID tidak valid: %w", err)
    }
    var p Product
    err = r.col.FindOne(ctx, bson.M{"_id": oid}).Decode(&p)
    if errors.Is(err, mongo.ErrNoDocuments) {
        return nil, ErrNotFound
    }
    return &p, err
}

func (r *ProductRepo) FindByCategory(ctx context.Context, category string) ([]*Product, error) {
    cursor, err := r.col.Find(ctx,
        bson.M{"category": category, "is_active": true},
        options.Find().SetSort(bson.D{{Key: "name", Value: 1}}),
    )
    if err != nil {
        return nil, err
    }
    defer cursor.Close(ctx)

    var products []*Product
    return products, cursor.All(ctx, &products)
}

func (r *ProductRepo) Update(ctx context.Context, id string, updates bson.M) error {
    oid, _ := primitive.ObjectIDFromHex(id)
    updates["updated_at"] = time.Now()
    res, err := r.col.UpdateOne(ctx,
        bson.M{"_id": oid},
        bson.M{"$set": updates},
    )
    if err != nil {
        return err
    }
    if res.MatchedCount == 0 {
        return ErrNotFound
    }
    return nil
}

func (r *ProductRepo) Delete(ctx context.Context, id string) error {
    oid, _ := primitive.ObjectIDFromHex(id)
    res, err := r.col.DeleteOne(ctx, bson.M{"_id": oid})
    if err != nil {
        return err
    }
    if res.DeletedCount == 0 {
        return ErrNotFound
    }
    return nil
}

func (r *ProductRepo) CategoryStats(ctx context.Context) error {
    pipeline := mongo.Pipeline{
        {{Key: "$match", Value: bson.M{"is_active": true}}},
        {{Key: "$group", Value: bson.D{
            {Key: "_id", Value: "$category"},
            {Key: "count", Value: bson.M{"$sum": 1}},
            {Key: "avg_price", Value: bson.M{"$avg": "$price"}},
        }}},
        {{Key: "$sort", Value: bson.D{{Key: "count", Value: -1}}}},
    }
    cursor, err := r.col.Aggregate(ctx, pipeline)
    if err != nil {
        return err
    }
    defer cursor.Close(ctx)
    var results []bson.M
    if err := cursor.All(ctx, &results); err != nil {
        return err
    }
    for _, r := range results {
        fmt.Printf("  %-15s: %v produk, avg Rp%.0f\n",
            r["_id"], r["count"], r["avg_price"])
    }
    return nil
}

func main() {
    ctx := context.Background()

    client, err := mongo.Connect(ctx,
        options.Client().ApplyURI("mongodb://localhost:27017"))
    if err != nil {
        log.Fatal(err)
    }
    defer client.Disconnect(ctx)

    if err := client.Ping(ctx, readpref.Primary()); err != nil {
        log.Fatal("Ping:", err)
    }
    fmt.Println("✓ Terhubung ke MongoDB")

    db := client.Database("tokoonline_demo")
    // Bersihkan koleksi untuk demo
    db.Collection("products").Drop(ctx)

    repo := NewProductRepo(db)

    // Create
    fmt.Println("\n=== Insert Produk ===")
    products := []*Product{
        {Name: "Laptop Pro 14", Price: 15_000_000, Stock: 10,
            Category: "elektronik", Tags: []string{"laptop", "gaming"}},
        {Name: "Mouse Wireless", Price: 350_000, Stock: 50,
            Category: "elektronik", Tags: []string{"mouse", "wireless"}},
        {Name: "Keyboard Mech", Price: 1_500_000, Stock: 25,
            Category: "elektronik", Tags: []string{"keyboard", "mechanical"}},
        {Name: "Kaos Polos", Price: 85_000, Stock: 100,
            Category: "fashion", Tags: []string{"kaos", "basic"}},
        {Name: "Celana Chino", Price: 250_000, Stock: 60,
            Category: "fashion", Tags: []string{"celana"}},
    }
    for _, p := range products {
        if err := repo.Create(ctx, p); err != nil {
            log.Printf("Gagal insert %s: %v", p.Name, err)
        } else {
            fmt.Printf("  [%s] %s\n", p.ID.Hex()[:8]+"...", p.Name)
        }
    }

    // FindByCategory
    fmt.Println("\n=== Produk Elektronik ===")
    elekt, _ := repo.FindByCategory(ctx, "elektronik")
    for _, p := range elekt {
        fmt.Printf("  %-20s Rp%10.0f  stok=%d\n", p.Name, p.Price, p.Stock)
    }

    // FindByID
    if len(products) > 0 {
        fmt.Println("\n=== FindByID ===")
        p, err := repo.FindByID(ctx, products[0].ID.Hex())
        if err != nil {
            log.Println(err)
        } else {
            fmt.Printf("  Ditemukan: %s (tags: %v)\n", p.Name, p.Tags)
        }

        // Update
        fmt.Println("\n=== Update ===")
        err = repo.Update(ctx, products[0].ID.Hex(), bson.M{
            "price": 14_500_000,
            "stock": 8,
        })
        fmt.Printf("  Update: %v\n", err)
    }

    // Aggregation stats
    fmt.Println("\n=== Statistik per Kategori ===")
    repo.CategoryStats(ctx)

    // Delete
    if len(products) > 0 {
        fmt.Println("\n=== Delete ===")
        err := repo.Delete(ctx, products[len(products)-1].ID.Hex())
        fmt.Printf("  Delete: %v\n", err)
    }

    var total int64
    total, _ = db.Collection("products").CountDocuments(ctx, bson.M{})
    fmt.Printf("\nTotal dokumen tersisa: %d\n", total)
}

Ringkasan #

  • mongo.Connect tidak langsung membuka koneksi — selalu Ping untuk memverifikasi.
  • defer client.Disconnect(ctx) dan defer cursor.Close(ctx) wajib untuk resource cleanup.
  • bson.M untuk filter/update sederhana; bson.D untuk query yang butuh urutan (sort, aggregation stage).
  • mongo.ErrNoDocuments dari FindOne berarti dokumen tidak ditemukan — cek dengan errors.Is.
  • primitive.ObjectID untuk ID dokumen — konversi dari/ke string dengan ObjectIDFromHex dan .Hex().
  • $set untuk update field tertentu; $inc untuk increment; $currentDate untuk set timestamp.
  • FindOneAndUpdate untuk operasi atomik — ambil dan update sekaligus tanpa race condition.
  • Aggregation pipeline untuk query kompleks — lebih powerful dari SQL GROUP BY biasa.
  • Buat index sebelum production — tanpa index, MongoDB melakukan full collection scan.
  • Transaksi butuh replica set/sharded cluster — tidak tersedia di standalone MongoDB.

← Sebelumnya: GORM   Berikutnya: Elasticsearch →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact