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.Connecttidak langsung membuka koneksi — selaluPinguntuk memverifikasi.defer client.Disconnect(ctx)dandefer cursor.Close(ctx)wajib untuk resource cleanup.bson.Muntuk filter/update sederhana;bson.Duntuk query yang butuh urutan (sort, aggregation stage).mongo.ErrNoDocumentsdariFindOneberarti dokumen tidak ditemukan — cek denganerrors.Is.primitive.ObjectIDuntuk ID dokumen — konversi dari/ke string denganObjectIDFromHexdan.Hex().$setuntuk update field tertentu;$incuntuk increment;$currentDateuntuk set timestamp.FindOneAndUpdateuntuk 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.