Multi-Tenancy Database Patterns met voorbeelden in Go
Volledige gids naar databasemodellen voor multi-tenancy
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.

Overzicht van Multi-Tenancy Patroon
Bij het ontwerpen van een multi-tenant toepassing, heb je drie primaire databasarchitectuurpatronen om te kiezen:
- Gedeelde Database, Gedeelde Schema (meest voorkomend)
- Gedeelde Database, Afzonderlijk Schema
- 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
- 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.
- Monitor verhuurderresourcegebruik: Identificeer en beperk noisy neighbors. Gebruik databasbeheertools zoals die in onze DBeaver vs Beekeeper gids worden vergeleken om prestatie-metrieken te volgen.
- Implementeer verhuurdercontextmiddleware: Centraliseer verhuurderextrahering en validatie. Je keuze van ORM beïnvloedt hoe je dit implementeert—zie onze Go-ORMs vergelijking voor verschillende aanpakken.
- Gebruik verbindingspooling: Efficient beheren van databaselinken. PostgreSQL-specifieke verbindingspoolstrategieën worden behandeld in onze PostgreSQL Cheatsheet.
- Plan voor verhuurdermigratie: Mogelijkheid om verhuurders te verplaatsen tussen patronen
- Implementeer soft delete: Gebruik deleted_at in plaats van harde deleten voor verhuurderdata
- Audit alles: Log alle verhuurderdata-toegang voor compliance
- 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.
Nuttige Links
- PostgreSQL Row-Level Security Documentatie
- Multi-Tenant SaaS Databasarchitectuur
- Designing Multi-Tenant Databases
- Vergelijking van Go-ORMs voor PostgreSQL: GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet: Een ontwikkelaars snelle verwijzing
- DBeaver vs Beekeeper - SQL Databasbeheertools
- SQL Cheatsheet - meest nuttige SQL-opdrachten