Multi-Tenancy Database Patterns met voorbeelden in Go

Volledige gids naar databasemodellen voor multi-tenancy

Inhoud

Multi-tenancy is een fundamenteel architectuurpatroon voor SaaS-toepassingen, dat meerdere klanten (verhuurders) toelaat om dezelfde toepassingsinfrastructuur te delen, terwijl data-isolatie wordt behouden.

Het kiezen van het juiste databaspatroon is cruciaal voor schaalbaarheid, beveiliging en operationele efficiëntie.

databases-scheme

Overzicht van Multi-Tenancy Patroon

Bij het ontwerpen van een multi-tenant toepassing, heb je drie primaire databasarchitectuurpatronen om te kiezen:

  1. Gedeelde Database, Gedeelde Schema (meest voorkomend)
  2. Gedeelde Database, Afzonderlijk Schema
  3. Afzonderlijke Database per Verhuurder

Elk patroon heeft verschillende kenmerken, afwegingen en toepassingen. Laten we elk in detail bespreken.

Patroon 1: Gedeelde Database, Gedeelde Schema

Dit is het meest voorkomende multi-tenancy patroon, waarbij alle verhuurders dezelfde database en schema delen, met een tenant_id kolom die gebruikt wordt om verhuurderdata te onderscheiden.

Architectuur

┌─────────────────────────────────────┐
│     Enkele Database                 │
│  ┌───────────────────────────────┐  │
│  │  Gedeeld Schema                │  │
│  │  - gebruikers (tenant_id, ...)     │  │
│  │  - bestellingen (tenant_id, ...)    │  │
│  │  - producten (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Implementatievoorbeeld

Bij het implementeren van multi-tenant patronen is het begrijpen van SQL-fundamenten cruciaal. Voor een uitgebreid overzicht van SQL-opdrachten en syntaxis, raadpleeg onze SQL Cheatsheet. Hier is hoe je het gedeelde schema patroon kunt instellen:

-- Gebruikers tabel met tenant_id
CREATE TABLE gebruikers (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    email VARCHAR(255) NOT NULL,
    naam VARCHAR(255),
    aangemaakt_op TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- Index op tenant_id voor prestaties
CREATE INDEX idx_gebruikers_tenant_id ON gebruikers(tenant_id);

-- Rijniveaubeveiliging (PostgreSQL voorbeeld)
ALTER TABLE gebruikers ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolatie OP gebruikers
    VOOR ALLES
    GEbruik (tenant_id = current_setting('app.current_tenant')::INTEGER);

Voor meer PostgreSQL-specifieke functies en opdrachten, inclusief RLS-beleid, schema-beheer en prestatieoptimalisatie, raadpleeg onze PostgreSQL Cheatsheet.

Toepassingsniveau-filtering

Bij het werken met Go-toepassingen kan het kiezen van het juiste ORM aanzienlijk invloed hebben op je multi-tenant implementatie. De voorbeelden hieronder gebruiken GORM, maar er zijn verschillende uitstekende opties beschikbaar. Voor een gedetailleerde vergelijking van Go-ORMs inclusief GORM, Ent, Bun en sqlc, zie onze uitgebreide gids over Go-ORMs voor PostgreSQL.

// Voorbeeld in Go met GORM
func GebruikerOphalenViaEmail(db *gorm.DB, tenantID uint, email string) (*Gebruiker, error) {
    var gebruiker Gebruiker
    err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&gebruiker).Error
    return &gebruiker, err
}

// Middleware om tenant-context in te stellen
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // Van subdomein, header of JWT
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Voordelen van Gedeeld Schema

  • Laagste kosten: Enkele databaseinstantie, minimale infrastructuur
  • Eenvoudigste operaties: Één database om te back-uppen, te monitoren en te onderhouden
  • Eenvoudige schema wijzigingen: Migraties worden tegelijkertijd toegepast op alle verhuurders
  • Beste voor hoge verhuurderaantallen: Efficiënte gebruik van resources
  • Cross-tenant analyse: Eenvoudig om data over verhuurders samen te vatten

Nadelen van Gedeeld Schema

  • Zwakkere isolatie: Risico op datalek als queries de tenant_id-filter vergeten
  • Noisy neighbor: Zware werkbelasting van één verhuurder kan andere beïnvloeden
  • Beperkte aanpassing: Alle verhuurders delen hetzelfde schema
  • Complianceproblemen: Moeilijker om strikte data-isolatievereisten te voldoen
  • Back-upcomplexiteit: Kan geen individuele verhuurderdata gemakkelijk herstellen

Beste voor Gedeeld Schema

  • SaaS-toepassingen met veel kleine- tot middelgrote verhuurders
  • Toepassingen waarbij verhuurders geen aangepaste schema’s nodig hebben
  • Kostengevoelige startups
  • Wanneer het aantal verhuurders hoog is (duizenden+)

Patroon 2: Gedeelde Database, Afzonderlijk Schema

Elke verhuurder krijgt hun eigen schema binnen dezelfde database, wat betere isolatie biedt terwijl infrastructuur wordt gedeeld.

Afzonderlijk Schema Architectuur

┌─────────────────────────────────────┐
│     Enkele Database                 │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Verhuurder1)│  │ (Verhuurder2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘

Afzonderlijk Schema Implementatie

PostgreSQL-schemata zijn een krachtige functie voor multi-tenancy. Voor gedetailleerde informatie over PostgreSQL-schema-beheer, connectiestrings en databasemanagementopdrachten, raadpleeg onze PostgreSQL Cheatsheet.

-- Schema aanmaken voor verhuurder
CREATE SCHEMA tenant_123;

-- Zoekpad instellen voor verhuurderbewerkingen
SET search_path TO tenant_123, public;

-- Tabellen aanmaken in verhuurderschema
CREATE TABLE tenant_123.gebruikers (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    naam VARCHAR(255),
    aangemaakt_op TIMESTAMP DEFAULT NOW()
);

Toepassingsverbindingbeheer

Efficiënt beheren van databaselinken is cruciaal voor multi-tenant toepassingen. De onderstaande linkbeheercode gebruikt GORM, maar je zou andere ORM-opties kunnen overwegen. Voor een grondige vergelijking van Go-ORMs inclusief verbindingspooling, prestatiekenmerken en gebruikscases, raadpleeg onze gids voor vergelijking van Go-ORMs.

// Verbindingsreeks met zoekpad
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}

// Of gebruik PostgreSQL-verbindingreeks
// postgresql://user:pass@host/db?search_path=tenant_123

Voordelen van Afzonderlijk Schema

  • Beter isolatie: Schema-niveau scheiding vermindert risico op datalek
  • Aanpassing: Elke verhuurder kan verschillende tabelstructuren hebben
  • Gemiddelde kosten: Nog steeds enkele databaseinstantie
  • Eenvoudigere back-ups per verhuurder: Kan individuele schema’s back-uppen
  • Beter voor compliance: Sterker dan het gedeelde schema patroon

Nadelen van Afzonderlijk Schema

  • Schema-beheercomplexiteit: Migraties moeten per verhuurder worden uitgevoerd
  • Verbindingsoverhead: Moet zoekpad per verbinding instellen
  • Beperkte schaalbaarheid: Schema-aantallen beperkingen (PostgreSQL ~10k schema’s)
  • Cross-tenant queries: Meer complex, vereist dynamische schema-referenties
  • Resourcebeperkingen: Nog steeds gedeelde databaseresources

Beste voor Afzonderlijk Schema

  • Middelgroot SaaS (tientallen tot honderden verhuurders)
  • Wanneer verhuurders schema-aanpassing nodig hebben
  • Toepassingen die betere isolatie nodig hebben dan gedeeld schema
  • Wanneer compliancevereisten gemiddeld zijn

Patroon 3: Afzonderlijke Database per Verhuurder

Elke verhuurder krijgt hun eigen volledige databaseinstantie, wat maximale isolatie biedt.

Afzonderlijke Database Architectuur

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

Afzonderlijke Database Implementatie

-- Database aanmaken voor verhuurder
CREATE DATABASE tenant_enterprise_corp;

-- Verbinden met verhuurderdatabase
\c tenant_enterprise_corp

-- Tabellen aanmaken (geen tenant_id nodig!)
CREATE TABLE gebruikers (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    naam VARCHAR(255),
    aangemaakt_op TIMESTAMP DEFAULT NOW()
);

Dynamisch Verbindingsbeheer

// Verbindingspooldbeheerder
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 na het verkrijgen van schrijflock
    if db, exists := m.pools[tenantID]; exists {
        return db, nil
    }

    // Nieuwe verbinding maken
    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
}

Voordelen van Afzonderlijke Database

  • Maximale isolatie: Volledige data-scheiding
  • Beste beveiliging: Geen risico op cross-tenant data-toegang
  • Volledige aanpassing: Elke verhuurder kan volledig verschillende schema’s hebben
  • Onafhankelijke schaalbaarheid: Schaal verhuurderdatabases individueel
  • Eenvoudige compliance: Voldoet aan de strikteste data-isolatievereisten
  • Per-tenant back-ups: Eenvoudig, onafhankelijke back-up/restore
  • Geen noisy neighbors: Verhuurderbelastingen beïnvloeden elkaar niet

Nadelen van Afzonderlijke Database

  • Hoogste kosten: Meerdere databaseinstanties vereisen meer resources
  • Operationele complexiteit: Beheren van veel databases (back-ups, monitoring, migraties)
  • Verbindingsbeperkingen: Elke databaseinstantie heeft verbindingsbeperkingen
  • Cross-tenant analyse: Vereist datafederatie of ETL
  • Migratiecomplexiteit: Moet migraties uitvoeren over alle databases
  • Resourceoverhead: Meer geheugen, CPU en opslag vereist

Beste voor Afzonderlijke Database

  • Enterprise SaaS met hoogwaardige klanten
  • Strikte compliancevereisten (HIPAA, GDPR, SOC 2)
  • Wanneer verhuurders aanzienlijke aanpassing nodig hebben
  • Laag tot gemiddeld aantal verhuurders (tientallen tot lage honderden)
  • Wanneer verhuurders zeer verschillende datamodellen hebben

Beveiligingsoverwegingen

Ongeacht het gekozen patroon is beveiliging van cruciaal belang:

1. Rijniveaubeveiliging (RLS)

PostgreSQL RLS filtert automatisch queries per verhuurder, wat een databaselagenbeveiliging biedt. Deze functie is vooral krachtig voor multi-tenant toepassingen. Voor meer informatie over PostgreSQL RLS, beveiligingsbeleid en andere geavanceerde PostgreSQL-functies, zie onze PostgreSQL Cheatsheet.

-- RLS inschakelen
ALTER TABLE bestellingen ENABLE ROW LEVEL SECURITY;

-- Beleid voor isolatie per verhuurder
CREATE POLICY tenant_isolatie OP bestellingen
    VOOR ALLES
    GEbruik (tenant_id = current_setting('app.current_tenant')::INTEGER);

-- Toepassing stelt verhuurdercontext in
SET app.current_tenant = '123';

2. Toepassingsniveau-filtering

Altijd filteren op tenant_id in toepassingscode. De onderstaande voorbeelden gebruiken GORM, maar verschillende ORMs hebben hun eigen aanpakken voor querybouwen. Voor richtlijnen over het kiezen van het juiste ORM voor je multi-tenant toepassing, zie onze vergelijking van Go-ORMs.

// ❌ Slecht - Missing tenant filter
db.Where("email = ?", email).First(&gebruiker)

// ✅ Goed - Always include tenant filter
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&gebruiker)

// ✅ Beter - Gebruik scopes of middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&gebruiker)

3. Verbindingspooling

Gebruik verbindingspoolers die ondersteuning bieden voor verhuurdercontext:

// PgBouncer met transactiepooling
// Of gebruik toepassingsniveau verbindingsrouting

4. Auditlogboek

Volg alle verhuurderdata-toegang:

type AuditLog struct {
    ID        uint
    TenantID  uint
    UserID    uint
    Actie     string
    Tabel     string
    RecordID  uint
    Tijd      time.Time
    IPAddress string
}

Prestatieoptimalisatie

Indexstrategie

Proper indexering is cruciaal voor multi-tenant databasprestaties. Het begrijpen van SQL-indexstrategieën, inclusief samengestelde indexen en gedeeltelijke indexen, is essentieel. Voor een uitgebreid overzicht van SQL-opdrachten inclusief CREATE INDEX en queryoptimalisatie, zie onze SQL Cheatsheet. Voor PostgreSQL-specifieke indexfuncties en prestatieoptimalisatie, raadpleeg onze PostgreSQL Cheatsheet.

-- Samengestelde indexen voor verhuurderqueries
CREATE INDEX idx_bestellingen_tenant_aangemaakt_op ON bestellingen(tenant_id, aangemaakt_op DESC);
CREATE INDEX idx_bestellingen_tenant_status ON bestellingen(tenant_id, status);

-- Gedeeltelijke indexen voor veelvoorkomende verhuurder-specifieke queries
CREATE INDEX idx_bestellingen_actief_tenant ON bestellingen(tenant_id, aangemaakt_op)
WHERE status = 'actief';

Queryoptimalisatie

// Gebruik voorbereide statements voor verhuurderqueries
stmt := db.Prepare("SELECT * FROM gebruikers WHERE tenant_id = $1 AND email = $2")

// Batchbewerkingen per verhuurder
db.Where("tenant_id = ?", tenantID).Find(&gebruikers)

// Gebruik verbindingspooling per verhuurder (voor afzonderlijk databasepatroon)

Monitoring

Effectieve databasbeheertools zijn essentieel voor het monitoren van multi-tenant toepassingen. Je moet queryprestaties, resourcegebruik en databasgezondheid volgen over alle verhuurders. Voor het vergelijken van databasbeheertools die hierbij kunnen helpen, zie onze DBeaver vs Beekeeper vergelijking. Beide tools bieden uitstekende functies voor het beheren en monitoren van PostgreSQL-databases in multi-tenantomgevingen.

Monitor per-verhuurder metrieken:

  • Queryprestaties per verhuurder
  • Resourcegebruik per verhuurder
  • Verbindingsaantallen per verhuurder
  • Databagrootte per verhuurder

Migratiestrategie

Gedeeld Schema Patroon

Bij het implementeren van databasmigraties beïnvloedt je keuze van ORM hoe je schema wijzigingen aanpakt. De onderstaande voorbeelden gebruiken GORM’s AutoMigrate-functie, maar verschillende ORMs hebben verschillende migratiestrategieën. Voor gedetailleerde informatie over hoe verschillende Go-ORMs migraties en schema-beheer aanpakken, zie onze Go-ORMs vergelijking.

// Migraties worden automatisch toegepast op alle verhuurders
func Migreren(db *gorm.DB) error {
    return db.AutoMigrate(&Gebruiker{}, &Bestelling{}, &Product{})
}

Afzonderlijk Schema/Database Patroon

// Migraties moeten per verhuurder worden uitgevoerd
func MigrerenAlleVerhuurders(tenantIDs []uint) error {
    for _, tenantID := range tenantIDs {
        db := GetTenantDB(tenantID)
        if err := db.AutoMigrate(&Gebruiker{}, &Bestelling{}); err != nil {
            return fmt.Errorf("verhuurder %d: %w", tenantID, err)
        }
    }
    return nil
}

Beslissingsmatrix

Factor Gedeeld Schema Afzonderlijk Schema Afzonderlijke DB
Isolatie Laag Gemiddeld Hoog
Kosten Laag Gemiddeld Hoog
Schaalbaarheid Hoog Gemiddeld Laag-Middel
Aanpassing Geen Gemiddeld Hoog
Operationele complexiteit Laag Gemiddeld Hoog
Compliance Beperkt Goed Uitstekend
Beste verhuurderaantal 1000+ 10-1000 1-100

Hybride aanpak

Je kunt patronen combineren voor verschillende verhuurderlagen:

// Kleine verhuurders: Gedeeld schema
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}

// Enterprise verhuurders: Afzonderlijke database
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}

Best Practices

  1. Altijd filteren op verhuurder: Vertrouw nooit alleen op toepassingscode; gebruik RLS wanneer mogelijk. Het begrijpen van SQL-fundamenten helpt om correcte queryconstructie te waarborgen—zie onze SQL Cheatsheet voor querybest practices.
  2. Monitor verhuurderresourcegebruik: Identificeer en beperk noisy neighbors. Gebruik databasbeheertools zoals die in onze DBeaver vs Beekeeper gids worden vergeleken om prestatie-metrieken te volgen.
  3. Implementeer verhuurdercontextmiddleware: Centraliseer verhuurderextrahering en validatie. Je keuze van ORM beïnvloedt hoe je dit implementeert—zie onze Go-ORMs vergelijking voor verschillende aanpakken.
  4. Gebruik verbindingspooling: Efficient beheren van databaselinken. PostgreSQL-specifieke verbindingspoolstrategieën worden behandeld in onze PostgreSQL Cheatsheet.
  5. Plan voor verhuurdermigratie: Mogelijkheid om verhuurders te verplaatsen tussen patronen
  6. Implementeer soft delete: Gebruik deleted_at in plaats van harde deleten voor verhuurderdata
  7. Audit alles: Log alle verhuurderdata-toegang voor compliance
  8. Test isolatie: Regelmatige beveiligingsaudits om cross-tenant datalekken te voorkomen

Conclusie

Het kiezen van het juiste multi-tenancy databaspatroon hangt af van je specifieke vereisten voor isolatie, kosten, schaalbaarheid en operationele complexiteit. Het Gedeelde Database, Gedeeld Schema patroon werkt goed voor de meeste SaaS-toepassingen, terwijl Afzonderlijke Database per Verhuurder nodig is voor enterpriseklanten met strikte compliancevereisten.

Begin met het eenvoudigste patroon dat je vereisten voldoet, en plan voor migratie naar een meer geïsoleerd patroon als je behoeften evolueren. Prioriteer altijd beveiliging en data-isolatie, ongeacht het gekozen patroon.