Pengenalan

Dalam aplikasi Go Fiber yang berhadapan dengan data kritikal—misalnya update stok, saldo, atau log aktivitas finansial—kontrol transaksi dan locking merupakan kebutuhan utama. GORM menyediakan API yang lengkap untuk mengelola transaksi database, termasuk nested transaction melalui savepoint dan locking baris dengan FOR UPDATE. Artikel ini membahas cara menggabungkan seluruh teknik tersebut dalam konteks Fiber, sehingga transaksi aman, konsisten, dan mudah di-debug.

Menyiapkan GORM dan Fiber

Pertama, pastikan koneksi database dibuat dengan mode yang mendukung transaksi (misalnya PostgreSQL atau MySQL dengan engine InnoDB). Struktur umum koneksi:

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
    log.Fatalf("gagal koneksi database: %v", err)
}

Dalam Fiber, sering kali kita ingin menyuntikkan objek *gorm.DB ke dalam setiap permintaan. Cara yang bersih adalah dengan middleware yang menyimpan instance ini dalam ctx.Locals. Jika perlu transaksi per request, kita bisa simpan transaction handle di sana juga.

Transaksi GORM: commit dan rollback

Dasar penggunaan transaksi di GORM:

tx := db.Begin()
if err := tx.Error; err != nil {
    return err
}
if err := tx.Create(&model).Error; err != nil {
    tx.Rollback()
    return err
}
return tx.Commit().Error

Namun dalam Fiber, Anda tidak ingin menulis boilerplate di setiap handler. Solusi praktis adalah middleware yang menyediakan helper, misalnya:

func TransactionMiddleware(db *gorm.DB) fiber.Handler {
    return func(ctx *fiber.Ctx) error {
        tx := db.Begin()
        if tx.Error != nil {
            return ctx.Status(fiber.StatusInternalServerError).SendString("gagal memulai transaksi")
        }
        ctx.Locals("tx", tx)
        err := ctx.Next()
        if err != nil {
            tx.Rollback()
            return err
        }
        if err := tx.Commit().Error; err != nil {
            return ctx.Status(fiber.StatusInternalServerError).SendString("gagal commit transaksi")
        }
        return nil
    }
}

Dengan pendekatan ini, setiap handler dapat mengekstrak transaksi dari ctx.Locals dan fokus pada logika bisnis. Jangan lupa menambahkan rollback eksplisit jika terjadi error sebelum ctx.Next() selesai.

Common mistake: lupa rollback

Transaksi yang tidak di-*rollback* akan memblokir kunci database (deadlock) dan menurunkan performa. Selalu panggil tx.Rollback() pada jalur error, bahkan sebelum mengembalikan response error.

Integrasi transaksi tersembunyi dalam handler Fiber

Pada handler, kita bisa memanggil helper seperti berikut:

func UpdateStockHandler(ctx *fiber.Ctx) error {
    tx := ctx.Locals("tx").(*gorm.DB)
    var req UpdateStockRequest
    if err := ctx.BodyParser(&req); err != nil {
        return err
    }
    if err := tx.Model(&Product{}).Where("id = ?", req.ProductID).Updates(map[string]interface{}{"stock": gorm.Expr("stock - ?", req.Quantity)}).Error; err != nil {
        return ctx.Status(fiber.StatusInternalServerError).SendString("gagal update stok")
    }
    return ctx.SendStatus(fiber.StatusOK)
}

Jika handler perlu mengembalikan error, middleware akan mengembalikan ctx.Next() dengan error tersebut dan kemudian menjalankan rollback. Ini menghindari duplikasi rollback di setiap handler.

Nested transaction dengan SavePoint

Beberapa alur bisnis memerlukan nested operation: misalnya, menyimpan log audit di dalam transaksi utama. GORM mendukung savepoint:

func useSavePoint(tx *gorm.DB) error {
    if err := tx.SavePoint("sp_audit").Error; err != nil {
        return err
    }
    if err := tx.Create(&audit).Error; err != nil {
        tx.RollbackTo("sp_audit")
        return err
    }
    return nil
}

Dengan SavePoint/RollbackTo, Anda bisa menggagalkan satu bagian tanpa membatalkan keseluruhan transaksi. Ini sangat berguna saat ingin memastikan data utama committed tetapi log atau cache dapat di-_rollback_ jika gagal.

Tip debug: Perhatikan pesan kesalahan dari perintah RollbackTo karena tidak semua DB mendukung savepoint; GORM tidak secara otomatis memverifikasi kesiapan DB. Pastikan Anda menggunakan driver yang kompatibel.

Locking baris dengan FOR UPDATE

Ketika update stok atau saldo dilakukan di bawah lalu lintas tinggi, kunci baris menjadi penting. Jangan memakai teknik read-modify-write tanpa kunci karena akan menyebabkan race condition.

Dalam GORM, Anda bisa menambahkan locking clause:

var product Product
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", req.ProductID).First(&product).Error; err != nil {
    return err
}
if product.Stock < req.Quantity {
    return errors.New("stok tidak cukup")
}
product.Stock -= req.Quantity
if err := tx.Save(&product).Error; err != nil {
    return err
}

Clause ini menghasilkan SELECT ... FOR UPDATE, memastikan baris dikunci sampai transaksi selesai. Jika dua request mencoba mengupdate stok yang sama, request kedua akan menunggu hingga transaksi pertama selesai, mencegah over-selling.

Perhatikan: Mengunci terlalu banyak baris atau tabel dapat menyebabkan penurunan performa/kemacetan. Gunakan kunci pada seleksi baris terbatas (misalnya menurut id) dan pastikan transaksi sesingkat mungkin.

Kesalahan umum dan cara menghindarinya

  • Lupa memanggil Commit sehingga transaksi tetap terbuka. Selalu tangani jalur sukses dan error.
  • Rollback tanpa memeriksa nil (misalnya transaksi sudah di-*rollback* sebelumnya). Periksa pesan kesalahan dari Rollback untuk logging.
  • Terlalu banyak nesting. Gunakan SavePoint hanya jika perlu—menambah terlalu banyak savepoint memperbesar overhead.
  • Locking di luar kebutuhan. Jika update tidak mengubah data kritis, hindari FOR UPDATE karena menambah contention.

Debugging tip: Aktifkan logging SQL GORM (db.Debug()) saat menguji transaksi. Pastikan query BEGIN, COMMIT, serta SELECT ... FOR UPDATE muncul sesuai urutan. Itu membantu mendeteksi missing commit akibat error halus.

Kesimpulan

Mengelola transaksi dan locking di GORM untuk aplikasi Fiber bukan hanya soal memanggil Begin/Commit, tapi juga menyediakan struktur yang aman untuk middleware, menyusun nested operation dengan SavePoint, dan melindungi data dengan FOR UPDATE. Kombinasi middleware Fiber, penggunaan konteks per-request, dan SQL locking akan menjaga konsistensi data di bawah beban produksi tinggi. Selalu uji jalur error dan gunakan logging SQL untuk memastikan transaksi berjalan seperti yang diharapkan.