Pola Basis Data Multi-Tenancy dengan Contoh dalam Go

Panduan lengkap pola basis data multi-tenan

Konten Halaman

Multi-tenancy adalah pola arsitektur dasar untuk aplikasi SaaS, memungkinkan beberapa pelanggan (tenant) untuk berbagi infrastruktur aplikasi yang sama sambil mempertahankan isolasi data.

Memilih pola basis data yang tepat sangat penting untuk skalabilitas, keamanan, dan efisiensi operasional.

databases-scheme

Ringkasan Pola Multi-Tenancy

Ketika merancang aplikasi multi-tenant, Anda memiliki tiga pola arsitektur basis data utama untuk dipilih:

  1. Shared Database, Shared Schema (paling umum)
  2. Shared Database, Separate Schema
  3. Separate Database per Tenant

Setiap pola memiliki karakteristik, kompromisi, dan kasus penggunaan yang berbeda. Mari kita eksplorasi masing-masing secara detail.

Pola 1: Shared Database, Shared Schema

Ini adalah pola multi-tenancy yang paling umum, di mana semua tenant berbagi database dan skema yang sama, dengan kolom tenant_id digunakan untuk membedakan data tenant.

Arsitektur

┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌───────────────────────────────┐  │
│  │  Shared Schema                │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Contoh Implementasi

Ketika menerapkan pola multi-tenant, memahami dasar-dasar SQL sangat penting. Untuk referensi menyeluruh tentang perintah dan sintaks SQL, lihat SQL Cheatsheet. Berikut cara mengatur pola shared schema:

-- Tabel users dengan tenant_id
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- Indeks pada tenant_id untuk kinerja
CREATE INDEX idx_users_tenant_id ON users(tenant_id);

-- Row-Level Security (contoh PostgreSQL)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON users
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

Untuk fitur dan perintah PostgreSQL spesifik, termasuk kebijakan RLS, manajemen skema, dan penyetelan kinerja, lihat PostgreSQL Cheatsheet.

Filter pada Tingkat Aplikasi

Ketika bekerja dengan aplikasi Go, memilih ORM yang tepat dapat secara signifikan memengaruhi implementasi multi-tenant Anda. Contoh di bawah ini menggunakan GORM, tetapi tersedia beberapa pilihan yang sangat baik. Untuk perbandingan rinci tentang ORM Go termasuk GORM, Ent, Bun, dan sqlc, lihat panduan menyeluruh tentang ORM Go untuk PostgreSQL.

// Contoh dalam Go dengan GORM
func GetUserByEmail(db *gorm.DB, tenantID uint, email string) (*User, error) {
    var user User
    err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user).Error
    return &user, err
}

// Middleware untuk menetapkan konteks tenant
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // Dari subdomain, header, atau JWT
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Kelebihan Shared Schema

  • Biaya terendah: Satu instance database, infrastruktur minimal
  • Operasi paling mudah: Satu database untuk dibackup, dimonitor, dan dipelihara
  • Perubahan skema sederhana: Migrasi berlaku untuk semua tenant sekaligus
  • Terbaik untuk jumlah tenant tinggi: Penggunaan sumber daya yang efisien
  • Analisis lintas tenant: Mudah menggabungkan data lintas tenant

Kekurangan Shared Schema

  • Isolasi yang lebih lemah: Risiko kebocoran data jika query lupa menyaring tenant_id
  • Neighbour yang bising: Beban kerja berat satu tenant dapat memengaruhi yang lain
  • Kurangnya penyesuaian: Semua tenant berbagi skema yang sama
  • Tantangan kepatuhan: Lebih sulit memenuhi persyaratan isolasi data yang ketat
  • Kompleksitas backup: Sulit memulihkan data tenant individu

Shared Schema Terbaik Untuk

  • Aplikasi SaaS dengan banyak tenant kecil-kecilan hingga menengah
  • Aplikasi di mana tenant tidak membutuhkan skema khusus
  • Startup yang sensitif terhadap biaya
  • Ketika jumlah tenant tinggi (ribuan+)

Pola 2: Shared Database, Separate Schema

Setiap tenant mendapatkan skema mereka sendiri dalam database yang sama, memberikan isolasi yang lebih baik sambil berbagi infrastruktur.

Arsitektur Separate Schema

┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Tenant1)│  │ (Tenant2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

Implementasi Separate Schema

Schema PostgreSQL adalah fitur yang kuat untuk multi-tenancy. Untuk informasi rinci tentang manajemen schema PostgreSQL, string koneksi, dan perintah administrasi database, konsultasikan PostgreSQL Cheatsheet.

-- Membuat schema untuk tenant
CREATE SCHEMA tenant_123;

-- Menetapkan search path untuk operasi tenant
SET search_path TO tenant_123, public;

-- Membuat tabel dalam schema tenant
CREATE TABLE tenant_123.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Manajemen Koneksi Aplikasi

Manajemen koneksi database yang efisien sangat penting untuk aplikasi multi-tenant. Kode manajemen koneksi di bawah ini menggunakan GORM, tetapi Anda mungkin ingin mengeksplorasi opsi ORM lainnya. Untuk perbandingan menyeluruh tentang ORM Go termasuk pooling koneksi, karakteristik kinerja, dan kasus penggunaan, lihat panduan perbandingan ORM Go.

// String koneksi dengan search path
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}

// Atau gunakan string koneksi PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123

Kelebihan Separate Schema

  • Isolasi yang lebih baik: Pemisahan pada tingkat schema mengurangi risiko kebocoran data
  • Penyesuaian: Setiap tenant dapat memiliki struktur tabel yang berbeda
  • Biaya sedang: Masih satu instance database
  • Backup per tenant lebih mudah: Dapat backup schema individu
  • Lebih baik untuk kepatuhan: Lebih baik daripada pola shared schema

Kekurangan Separate Schema

  • Kompleksitas manajemen schema: Migrasi harus dijalankan per tenant
  • Overhead koneksi: Perlu menetapkan search_path per koneksi
  • Keterbatasan skalabilitas: Jumlah schema terbatas (PostgreSQL sekitar 10k schema)
  • Query lintas tenant: Lebih kompleks, memerlukan referensi schema dinamis
  • Keterbatasan sumber daya: Masih berbagi sumber daya database

Separate Schema Terbaik Untuk

  • SaaS skala menengah (puluhan hingga ratusan tenant)
  • Ketika tenant membutuhkan penyesuaian schema
  • Aplikasi yang membutuhkan isolasi lebih baik daripada shared schema
  • Ketika persyaratan kepatuhan sedang

Pola 3: Separate Database per Tenant

Setiap tenant mendapatkan instance database lengkap mereka sendiri, memberikan isolasi maksimal.

Arsitektur Separate Database

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Database 1  │  │  Database 2  │  │  Database 3  │
│  (Tenant A)  │  │  (Tenant B)  │  │  (Tenant C)  │
└──────────────┘  └──────────────┘  └──────────────┘

Implementasi Separate Database

-- Membuat database untuk tenant
CREATE DATABASE tenant_enterprise_corp;

-- Menghubungkan ke database tenant
\c tenant_enterprise_corp

-- Membuat tabel (tidak perlu tenant_id!)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);

Manajemen Koneksi Dinamis

// Manajer pool koneksi
type TenantDBManager struct {
    pools map[uint]*gorm.DB
    mu    sync.RWMutex
}

func (m *TenantDBManager) GetDB(tenantID uint) (*gorm.DB, error) {
    m.mu.RLock()
    if db, exists := m.pools[tenantID]; exists {
        m.mu.RUnlock()
        return db, nil
    }
    m.mu.RUnlock()

    m.mu.Lock()
    defer m.mu.Unlock()

    // Double-check after acquiring write lock
    if db, exists := m.pools[tenantID]; exists {
        return db, nil
    }

    // Membuat koneksi baru
    db, err := gorm.Open(postgres.Open(fmt.Sprintf(
        "host=localhost user=dbuser password=dbpass dbname=tenant_%d sslmode=disable",
        tenantID,
    )), &gorm.Config{})
    
    if err != nil {
        return nil, err
    }

    m.pools[tenantID] = db
    return db, nil
}

Kelebihan Separate Database

  • Isolasi maksimal: Pemisahan data lengkap
  • Keamanan terbaik: Tidak ada risiko akses data lintas tenant
  • Penyesuaian penuh: Setiap tenant dapat memiliki skema yang sepenuhnya berbeda
  • Pemecahan skala independen: Skala database tenant secara individual
  • Kepatuhan mudah: Memenuhi persyaratan isolasi data paling ketat
  • Backup per tenant: Backup/restore sederhana, independen
  • Tidak ada neighbor bising: Beban kerja tenant tidak memengaruhi satu sama lain

Kekurangan Separate Database

  • Biaya tertinggi: Banyak instance database memerlukan lebih banyak sumber daya
  • Kompleksitas operasional: Mengelola banyak database (backup, pemantauan, migrasi)
  • Keterbatasan koneksi: Setiap instance database memiliki batas koneksi
  • Analisis lintas tenant: Memerlukan federasi data atau ETL
  • Kompleksitas migrasi: Harus menjalankan migrasi di semua database
  • Overhead sumber daya: Lebih banyak memori, CPU, dan penyimpanan yang diperlukan

Separate Database Terbaik Untuk

  • SaaS enterprise dengan pelanggan bernilai tinggi
  • Persyaratan kepatuhan ketat (HIPAA, GDPR, SOC 2)
  • Ketika tenant membutuhkan penyesuaian signifikan
  • Jumlah tenant rendah hingga menengah (puluhan hingga ratusan rendah)
  • Ketika tenant memiliki model data yang sangat berbeda

Pertimbangan Keamanan

Tidak peduli pola yang dipilih, keamanan sangat penting:

1. Row-Level Security (RLS)

RLS PostgreSQL secara otomatis menyaring query berdasarkan tenant, memberikan lapisan keamanan pada tingkat database. Fitur ini sangat kuat untuk aplikasi multi-tenant. Untuk informasi lebih lanjut tentang RLS PostgreSQL, kebijakan keamanan, dan fitur PostgreSQL lainnya, lihat PostgreSQL Cheatsheet.

-- Aktifkan RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Kebijakan untuk isolasi berdasarkan tenant
CREATE POLICY tenant_isolation ON orders
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);

-- Aplikasi menetapkan konteks tenant
SET app.current_tenant = '123';

2. Filtering pada Tingkat Aplikasi

Selalu menyaring berdasarkan tenant_id dalam kode aplikasi. Contoh di bawah ini menggunakan GORM, tetapi ORM berbeda memiliki pendekatan sendiri untuk membangun query. Untuk panduan memilih ORM yang tepat untuk aplikasi multi-tenant Anda, lihat perbandingan ORM Go.

// ❌ BAD - Tidak ada filter tenant
db.Where("email = ?", email).First(&user)

// ✅ GOOD - Selalu sertakan filter tenant
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)

// ✅ BETTER - Gunakan scope atau middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)

3. Pooling Koneksi

Gunakan pooler koneksi yang mendukung konteks tenant:

// PgBouncer dengan pooling transaksi
// Atau gunakan routing koneksi pada tingkat aplikasi

4. Logging Audit

Lacak akses data tenant:

type AuditLog struct {
    ID        uint
    TenantID  uint
    UserID    uint
    Action    string
    Table     string
    RecordID  uint
    Timestamp time.Time
    IPAddress string
}

Optimisasi Kinerja

Strategi Indeks

Indeks yang tepat sangat penting untuk kinerja database multi-tenant. Memahami strategi indeks SQL, termasuk indeks komposit dan indeks parsial, sangat penting. Untuk referensi menyeluruh tentang perintah SQL termasuk CREATE INDEX dan optimisasi query, lihat SQL Cheatsheet. Untuk fitur indeks PostgreSQL spesifik dan penyetelan kinerja, lihat PostgreSQL Cheatsheet.

-- Indeks komposit untuk query tenant
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);

-- Indeks parsial untuk query tenant spesifik yang umum
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';

Optimisasi Query

// Gunakan pernyataan yang disiapkan untuk query tenant
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")

// Operasi batch per tenant
db.Where("tenant_id = ?", tenantID).Find(&users)

// Gunakan pooling koneksi per tenant (untuk pola database terpisah)

Pemantauan

Alat manajemen database yang efektif sangat penting untuk memantau aplikasi multi-tenant. Anda perlu melacak kinerja query, penggunaan sumber daya, dan kesehatan database di semua tenant. Untuk membandingkan alat manajemen database yang dapat membantu hal ini, lihat perbandingan DBeaver vs Beekeeper. Kedua alat ini menawarkan fitur yang sangat baik untuk mengelola dan memantau database PostgreSQL dalam lingkungan multi-tenant.

Pantau metrik per tenant:

  • Kinerja query per tenant
  • Penggunaan sumber daya per tenant
  • Jumlah koneksi per tenant
  • Ukuran database per tenant

Strategi Migrasi

Pola Shared Schema

Ketika menerapkan migrasi database, pilihan ORM Anda memengaruhi cara Anda menangani perubahan skema. Contoh di bawah ini menggunakan fitur AutoMigrate GORM, tetapi ORM berbeda memiliki strategi migrasi yang berbeda. Untuk informasi rinci tentang bagaimana berbagai ORM Go menangani migrasi dan manajemen skema, lihat perbandingan ORM Go.

// Migrasi berlaku untuk semua tenant secara otomatis
func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Order{}, &Product{})
}

Pola Separate Schema/Database

// Migrasi harus dijalankan per tenant
func MigrateAllTenants(tenantIDs []uint) error {
    for _, tenantID := range tenantIDs {
        db := GetTenantDB(tenantID)
        if err := db.AutoMigrate(&User{}, &Order{}); err != nil {
            return fmt.Errorf("tenant %d: %w", tenantID, err)
        }
    }
    return nil
}

Matriks Keputusan

Faktor Shared Schema Separate Schema Separate DB
Isolasi Rendah Sedang Tinggi
Biaya Rendah Sedang Tinggi
Skalabilitas Tinggi Sedang Rendah-Sedang
Penyesuaian Tidak ada Sedang Tinggi
Kompleksitas Operasional Rendah Sedang Tinggi
Kepatuhan Terbatas Baik Sangat Baik
Jumlah Tenant Terbaik 1000+ 10-1000 1-100

Pendekatan Hybrid

Anda dapat menggabungkan pola untuk tingkat tenant yang berbeda:

// Tenant kecil: Shared schema
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}

// Tenant enterprise: Separate database
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}

Praktik Terbaik

  1. Selalu menyaring berdasarkan tenant: Jangan pernah mempercayai kode aplikasi sendiri; gunakan RLS jika mungkin. Memahami dasar-dasar SQL membantu memastikan konstruksi query yang tepat—lihat SQL Cheatsheet untuk praktik terbaik query.
  2. Pantau penggunaan sumber daya tenant: Identifikasi dan batasi neighbor bising. Gunakan alat manajemen database seperti yang dibandingkan dalam panduan DBeaver vs Beekeeper untuk melacak metrik kinerja.
  3. Implementasikan middleware konteks tenant: Pusatkan ekstraksi dan validasi tenant. Pilihan ORM Anda memengaruhi cara Anda mengimplementasikannya—lihat perbandingan ORM Go untuk pendekatan berbeda.
  4. Gunakan pooling koneksi: Kelola koneksi database secara efisien. Strategi pooling koneksi PostgreSQL spesifik dibahas dalam PostgreSQL Cheatsheet.
  5. Rencanakan migrasi tenant: Kemampuan untuk memindahkan tenant antar pola
  6. Implementasikan soft delete: Gunakan deleted_at alih-alih penghapusan keras untuk data tenant
  7. Audit semuanya: Catat akses data tenant untuk kepatuhan
  8. Uji isolasi: Audit keamanan berkala untuk mencegah kebocoran data lintas tenant

Kesimpulan

Memilih pola basis data multi-tenancy yang tepat bergantung pada persyaratan spesifik Anda untuk isolasi, biaya, skalabilitas, dan kompleksitas operasional. Pola Shared Database, Shared Schema bekerja baik untuk sebagian besar aplikasi SaaS, sementara Separate Database per Tenant diperlukan untuk pelanggan enterprise dengan persyaratan kepatuhan yang ketat.

Mulailah dengan pola paling sederhana yang memenuhi kebutuhan Anda, dan rencanakan migrasi ke pola yang lebih terisolasi seiring berkembangnya kebutuhan Anda. Selalu prioritaskan keamanan dan isolasi data, terlepas dari pola yang dipilih.

Tautan Berguna