Pattern di database per multi-tenancy con esempi in Go
Guida completa ai modelli di database multi-tenant
Multi-tenancy è un modello architetturale fondamentale per le applicazioni SaaS, che consente a diversi clienti (inquilini) di condividere la stessa infrastruttura applicativa mantenendo l’isolamento dei dati.
La scelta del modello di database giusto è cruciale per la scalabilità, la sicurezza e l’efficienza operativa.

Panoramica dei Modelli di Multi-Tenancy
Quando si progetta un’applicazione multi-inquilino, si hanno tre modelli principali di architettura del database da considerare:
- Database condiviso, schema condiviso (più comune)
- Database condiviso, schema separato
- Database separato per ogni inquilino
Ogni modello ha caratteristiche distinte, compromessi e casi d’uso. Esploriamo ciascuno nel dettaglio.
Modello 1: Database condiviso, schema condiviso
Questo è il modello più comune per la multi-tenancy, dove tutti gli inquilini condividono lo stesso database e schema, con una colonna tenant_id utilizzata per distinguere i dati degli inquilini.
Architettura
┌─────────────────────────────────────┐
│ Singolo Database │
│ ┌───────────────────────────────┐ │
│ │ Schema Condiviso │ │
│ │ - utenti (tenant_id, ...) │ │
│ │ - ordini (tenant_id, ...) │ │
│ │ - prodotti (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Esempio di Implementazione
Quando si implementano i modelli multi-inquilino, comprendere i fondamenti SQL è cruciale. Per un riferimento completo sui comandi e sulla sintassi SQL, consulta il nostro SQL Cheatsheet. Ecco come impostare il modello di schema condiviso:
-- Tabella utenti con 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)
);
-- Indice su tenant_id per prestazioni
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Sicurezza a livello di riga (esempio 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);
Per ulteriori funzionalità e comandi specifici di PostgreSQL, tra cui le politiche RLS, la gestione degli schemi e l’ottimizzazione delle prestazioni, consulta il nostro PostgreSQL Cheatsheet.
Filtraggio a livello applicativo
Quando si lavora con applicazioni Go, la scelta del giusto ORM può influenzare significativamente l’implementazione multi-inquilino. Gli esempi seguenti utilizzano GORM, ma ci sono diverse ottime opzioni disponibili. Per un confronto dettagliato degli ORM Go, tra cui GORM, Ent, Bun e sqlc, vedi la nostra guida completa agli ORM Go per PostgreSQL.
// Esempio in Go con 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 per impostare il contesto dell'inquilino
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // Dal sottodominio, header o JWT
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Vantaggi dello schema condiviso
- Costo più basso: Un’istanza di database, infrastruttura minima
- Operazioni più semplici: Un solo database da backup, monitorare e mantenere
- Cambiamenti dello schema semplici: Le migrazioni si applicano a tutti gli inquilini contemporaneamente
- Migliore per un alto numero di inquilini: Utilizzo efficiente delle risorse
- Analisi tra inquilini: Facile aggregare dati tra inquilini
Svantaggi dello schema condiviso
- Isolamento più debole: Rischio di perdita di dati se le query dimenticano il filtro tenant_id
- Inquilino rumoroso: Il carico di lavoro pesante di un inquilino può influenzare gli altri
- Personalizzazione limitata: Tutti gli inquilini condividono lo stesso schema
- Sfide di conformità: Più difficile soddisfare i requisiti di isolamento dei dati rigorosi
- Complessità del backup: Non è facile ripristinare facilmente i dati di un singolo inquilino
Migliore per lo schema condiviso
- Applicazioni SaaS con molti inquilini piccoli o medi
- Applicazioni dove gli inquilini non necessitano di schemi personalizzati
- Start-up sensibili ai costi
- Quando il numero di inquilini è alto (migliaia+)
Modello 2: Database condiviso, schema separato
Ogni inquilino riceve il proprio schema all’interno dello stesso database, fornendo un isolamento migliore mentre condivide l’infrastruttura.
Architettura schema separato
┌─────────────────────────────────────┐
│ Singolo Database │
│ ┌──────────┐ ┌──────────┐ │
│ │ Schema A │ │ Schema B │ ... │
│ │ (Inquilino1)│ │ (Inquilino2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Implementazione schema separato
Le schemi di PostgreSQL sono una funzionalità potente per la multi-tenancy. Per informazioni dettagliate sulla gestione degli schemi di PostgreSQL, le stringhe di connessione e i comandi di amministrazione del database, consulta il nostro PostgreSQL Cheatsheet.
-- Creare schema per inquilino
CREATE SCHEMA tenant_123;
-- Impostare il percorso di ricerca per le operazioni dell'inquilino
SET search_path TO tenant_123, public;
-- Creare tabelle nello schema dell'inquilino
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gestione delle connessioni del database
La gestione efficiente delle connessioni al database è cruciale per le applicazioni multi-inquilino. Il codice di gestione delle connessioni seguente utilizza GORM, ma potresti voler esplorare altre opzioni ORM. Per un confronto dettagliato degli ORM Go, tra cui la gestione del pooling delle connessioni, le caratteristiche di prestazione e i casi d’uso, consulta la nostra guida al confronto degli ORM Go.
// Stringa di connessione con percorso di ricerca dello schema
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// Oppure utilizzare la stringa di connessione PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Vantaggi dello schema separato
- Migliore isolamento: L’isolamento a livello di schema riduce il rischio di perdita di dati
- Personalizzazione: Ogni inquilino può avere strutture di tabelle diverse
- Costo moderato: Ancora un’istanza di database singola
- Backup per inquilino più semplice: È possibile backuppare gli schemi individuali
- Migliore per la conformità: Più forte del modello di schema condiviso
Svantaggi dello schema separato
- Complessità della gestione degli schemi: Le migrazioni devono essere eseguite per inquilino
- Overhead delle connessioni: È necessario impostare search_path per ogni connessione
- Scalabilità limitata: Il numero di schemi è limitato (PostgreSQL ~10k schemi)
- Query tra inquilini: Più complesse, richiedono riferimenti dinamici agli schemi
- Limiti delle risorse: Risorse del database condivise
Migliore per lo schema separato
- SaaS di media scala (decine a centinaia di inquilini)
- Quando gli inquilini necessitano di personalizzazione degli schemi
- Applicazioni che necessitano di un isolamento migliore rispetto allo schema condiviso
- Quando i requisiti di conformità sono moderati
Modello 3: Database separato per ogni inquilino
Ogni inquilino riceve la propria istanza completa del database, fornendo l’isolamento massimo.
Architettura database separato
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database 1 │ │ Database 2 │ │ Database 3 │
│ (Inquilino A) │ │ (Inquilino B) │ │ (Inquilino C) │
└──────────────┘ └──────────────┘ └──────────────┘
Implementazione database separato
-- Creare database per inquilino
CREATE DATABASE tenant_enterprise_corp;
-- Connettersi al database dell'inquilino
\c tenant_enterprise_corp
-- Creare tabelle (non necessario tenant_id!)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gestione dinamica delle connessioni
// Gestore del pool di connessioni
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()
// Verifica doppia dopo aver acquisito il blocco di scrittura
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Creare una nuova connessione
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
}
Vantaggi del database separato
- Isolamento massimo: Separazione completa dei dati
- Migliore sicurezza: Nessun rischio di accesso ai dati tra inquilini
- Personalizzazione completa: Ogni inquilino può avere schemi completamente diversi
- Scalabilità indipendente: Scalare individualmente i database degli inquilini
- Conformità facile: Rispetta i requisiti di isolamento dei dati più rigorosi
- Backup per inquilino: Backup e ripristino semplice e indipendente
- Nessun inquilino rumoroso: I carichi di lavoro degli inquilini non influenzano gli altri
Svantaggi del database separato
- Costo più alto: Molte istanze di database richiedono più risorse
- Complessità operativa: Gestire molti database (backup, monitoraggio, migrazioni)
- Limiti delle connessioni: Ogni istanza di database ha limiti di connessione
- Analisi tra inquilini: Richiede federazione dei dati o ETL
- Complessità delle migrazioni: Deve eseguire le migrazioni su tutti i database
- Overhead delle risorse: Maggiore utilizzo di memoria, CPU e storage
Migliore per il database separato
- SaaS enterprise con clienti ad alto valore
- Requisiti di conformità rigorosi (HIPAA, GDPR, SOC 2)
- Quando gli inquilini necessitano di personalizzazione significativa
- Numero di inquilini basso a medio (decine a centinaia)
- Quando gli inquilini hanno modelli di dati molto diversi
Considerazioni sulla Sicurezza
Indipendentemente dal modello scelto, la sicurezza è fondamentale:
1. Sicurezza a livello di riga (RLS)
La RLS di PostgreSQL filtra automaticamente le query per inquilino, fornendo uno strato di sicurezza a livello di database. Questa funzione è particolarmente potente per le applicazioni multi-inquilino. Per ulteriori dettagli sulla RLS di PostgreSQL, le politiche di sicurezza e altre funzionalità avanzate di PostgreSQL, vedi il nostro PostgreSQL Cheatsheet.
-- Abilitare RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Politica per isolare per inquilino
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- Applicazione imposta contesto inquilino
SET app.current_tenant = '123';
2. Filtraggio a livello applicativo
Filtra sempre per tenant_id nel codice dell’applicazione. Gli esempi seguenti utilizzano GORM, ma diversi ORM hanno approcci diversi per la costruzione delle query. Per linee guida sulla scelta del giusto ORM per la tua applicazione multi-inquilino, consulta la nostra confronto degli ORM Go.
// ❌ Cattivo - Mancante filtro inquilino
db.Where("email = ?", email).First(&user)
// ✅ Buono - Includere sempre il filtro inquilino
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ Migliore - Utilizzare scope o middleware
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Pooling delle connessioni
Utilizzare pooler di connessioni che supportano il contesto dell’inquilino:
// PgBouncer con pooling delle transazioni
// Oppure utilizzare routing delle connessioni a livello applicativo
4. Log di audit
Tracciare l’accesso a tutti i dati degli inquilini:
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
Ottimizzazione delle Prestazioni
Strategia di Indicizzazione
L’indicizzazione corretta è cruciale per le prestazioni del database multi-inquilino. Comprendere le strategie di indicizzazione SQL, tra cui gli indici composti e gli indici parziali, è essenziale. Per un riferimento completo sui comandi SQL, tra cui CREATE INDEX e ottimizzazione delle query, vedi il nostro SQL Cheatsheet. Per le funzionalità specifiche di indicizzazione e ottimizzazione delle prestazioni di PostgreSQL, consulta il nostro PostgreSQL Cheatsheet.
-- Indici composti per query degli inquilini
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- Indici parziali per query comuni specifiche degli inquilini
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
Ottimizzazione delle Query
// Utilizzare istruzioni preparate per le query degli inquilini
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// Operazioni in batch per inquilino
db.Where("tenant_id = ?", tenantID).Find(&users)
// Utilizzare il pooling delle connessioni per inquilino (per il modello database separato)
Monitoraggio
Strumenti essenziali per la gestione del database sono necessari per monitorare le applicazioni multi-inquilino. Dovrai tracciare le prestazioni delle query, l’utilizzo delle risorse e la salute del database per tutti gli inquilini. Per confrontare gli strumenti di gestione del database che possono aiutarti in questo, consulta la nostra confronto tra DBeaver e Beekeeper. Entrambi gli strumenti offrono funzionalità eccellenti per la gestione e il monitoraggio dei database PostgreSQL in ambienti multi-inquilino.
Monitorare le metriche per inquilino:
- Prestazioni delle query per inquilino
- Utilizzo delle risorse per inquilino
- Conteggio delle connessioni per inquilino
- Dimensione del database per inquilino
Strategia di Migrazione
Modello schema condiviso
Quando si implementano le migrazioni del database, la scelta dell’ORM influisce su come si gestiscono i cambiamenti nello schema. Gli esempi seguenti utilizzano la funzionalità AutoMigrate di GORM, ma diversi ORM hanno diverse strategie di migrazione. Per informazioni dettagliate su come diversi ORM Go gestiscono le migrazioni e la gestione dello schema, vedi la nostra confronto degli ORM Go.
// Le migrazioni si applicano automaticamente a tutti gli inquilini
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
Modello schema/database separato
// Le migrazioni devono essere eseguite per inquilino
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
}
Matrice di Decisione
| Fattore | Schema condiviso | Schema separato | Database separato |
|---|---|---|---|
| Isolamento | Basso | Medio | Alto |
| Costo | Basso | Medio | Alto |
| Scalabilità | Alta | Media | Bassa-Media |
| Personalizzazione | Nessuna | Media | Alta |
| Complessità operativa | Basso | Media | Alta |
| Conformità | Limitata | Buona | Eccellente |
| Migliore numero di inquilini | 1000+ | 10-1000 | 1-100 |
Approccio ibrido
Puoi combinare i modelli per diversi livelli di inquilino:
// Inquilini piccoli: Schema condiviso
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Inquilini enterprise: Database separato
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Linee guida per la migliore pratica
- Filtra sempre per inquilino: Mai fidarsi del codice dell’applicazione da solo; utilizza RLS quando possibile. Comprendere i fondamenti SQL aiuta a garantire la costruzione corretta delle query—consulta il nostro SQL Cheatsheet per le migliori pratiche di query.
- Monitora l’utilizzo delle risorse degli inquilini: Identifica e limita gli inquilini rumorosi. Utilizza strumenti di gestione del database come quelli confrontati nella nostra guida DBeaver vs Beekeeper per tracciare le metriche di prestazione.
- Implementa middleware per il contesto degli inquilini: Centralizza l’estrazione e la validazione degli inquilini. La tua scelta dell’ORM influisce su come implementi questo—vedi la nostra guida al confronto degli ORM Go per diversi approcci.
- Utilizza il pooling delle connessioni: Gestisci in modo efficiente le connessioni al database. Le strategie specifiche di pooling delle connessioni di PostgreSQL sono coperte nel nostro PostgreSQL Cheatsheet.
- Pianifica la migrazione degli inquilini: La capacità di spostare gli inquilini tra modelli
- Implementa il soft delete: Utilizza deleted_at invece di cancellazioni dure per i dati degli inquilini
- Audit tutto: Registra l’accesso a tutti i dati degli inquilini per la conformità
- Testa l’isolamento: Audit di sicurezza regolari per prevenire la perdita di dati tra inquilini
Conclusione
La scelta del modello giusto per la multi-tenancy dipende dai requisiti specifici per l’isolamento, il costo, la scalabilità e la complessità operativa. Il modello Database condiviso, Schema condiviso funziona bene per la maggior parte delle applicazioni SaaS, mentre il modello Database separato per ogni inquilino è necessario per i clienti enterprise con requisiti di conformità rigorosi.
Inizia con il modello più semplice che soddisfa i tuoi requisiti e pianifica la migrazione a un modello più isolato man mano che i tuoi bisogni evolvono. Priorizza sempre la sicurezza e l’isolamento dei dati, indipendentemente dal modello scelto.
Link utili
- Documentazione PostgreSQL Row-Level Security
- Architettura del database per SaaS multi-inquilino
- Progettare database multi-inquilino
- Confronto degli ORM Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet: Un riferimento rapido per gli sviluppatori
- DBeaver vs Beekeeper - Strumenti di Gestione Database SQL
- SQL Cheatsheet - comandi SQL più utili