Modèles de base de données multi-locataires avec des exemples en Go
Guide complet sur les modèles de bases de données multi-locataires
Multi-tenancy est un modèle architectural fondamental pour les applications SaaS, permettant à plusieurs clients (locataires) de partager la même infrastructure d’application tout en maintenant une isolation des données.
Le choix du bon modèle de base de données est crucial pour l’évolutivité, la sécurité et l’efficacité opérationnelle.

Aperçu des modèles de multi-tenancy
Lors de la conception d’une application multi-locataire, vous avez trois modèles principaux d’architecture de base de données à choisir :
- Base de données partagée, schéma partagé (le plus courant)
- Base de données partagée, schéma séparé
- Base de données séparée par locataire
Chaque modèle a des caractéristiques distinctes, des compromis et des cas d’utilisation. Explorons-les en détail.
Modèle 1 : Base de données partagée, schéma partagé
C’est le modèle le plus courant de multi-tenancy, où tous les locataires partagent la même base de données et le même schéma, avec une colonne tenant_id utilisée pour distinguer les données des locataires.
Architecture
┌─────────────────────────────────────┐
│ Single Database │
│ ┌───────────────────────────────┐ │
│ │ Shared Schema │ │
│ │ - users (tenant_id, ...) │ │
│ │ - orders (tenant_id, ...) │ │
│ │ - products (tenant_id, ...) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
Exemple d’implémentation
Lors de l’implémentation des modèles de multi-tenancy, comprendre les fondamentaux de SQL est crucial. Pour une référence complète sur les commandes et la syntaxe SQL, consultez notre SQL Cheatsheet. Voici comment configurer le modèle de schéma partagé :
-- Table des utilisateurs avec 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)
);
-- Index sur tenant_id pour les performances
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- Sécurité au niveau des lignes (exemple 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);
Pour plus de fonctionnalités et de commandes spécifiques à PostgreSQL, y compris les politiques RLS, la gestion des schémas et l’optimisation des performances, consultez notre PostgreSQL Cheatsheet.
Filtrage au niveau de l’application
Lorsque vous travaillez avec des applications Go, le choix de l’ORM approprié peut avoir un impact significatif sur votre implémentation de multi-tenancy. Les exemples ci-dessous utilisent GORM, mais il existe plusieurs excellents choix disponibles. Pour une comparaison détaillée des ORMs Go, y compris GORM, Ent, Bun et sqlc, consultez notre guide complet des ORMs Go pour PostgreSQL.
// Exemple en Go avec 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 pour définir le contexte du locataire
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := extractTenantID(r) // Du sous-domaine, d'en-tête ou de JWT
ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Avantages du schéma partagé
- Coût le plus bas : Une seule instance de base de données, infrastructure minimale
- Opérations les plus simples : Une seule base de données à sauvegarder, surveiller et maintenir
- Changements de schéma simples : Les migrations s’appliquent à tous les locataires en même temps
- Meilleur pour un grand nombre de locataires : Utilisation efficace des ressources
- Analyse croisée des locataires : Facile à agréger les données à travers les locataires
Inconvénients du schéma partagé
- Isolation plus faible : Risque de fuite de données si les requêtes oublient le filtre tenant_id
- Locataire bruyant : La charge de travail lourde d’un locataire peut affecter les autres
- Personnalisation limitée : Tous les locataires partagent le même schéma
- Défis de conformité : Plus difficile à respecter les exigences strictes d’isolation des données
- Complexité des sauvegardes : Impossible de restaurer facilement les données d’un seul locataire
Meilleur pour le schéma partagé
- Applications SaaS avec de nombreux locataires de petite à moyenne taille
- Applications où les locataires n’ont pas besoin de schémas personnalisés
- Startups sensibles au coût
- Lorsque le nombre de locataires est élevé (plus de mille)
Modèle 2 : Base de données partagée, schéma séparé
Chaque locataire obtient son propre schéma au sein de la même base de données, offrant une meilleure isolation tout en partageant l’infrastructure.
Architecture du schéma séparé
┌─────────────────────────────────────┐
│ Single Database │
│ ┌──────────┐ ┌──────────┐ │
│ │ Schema A │ │ Schema B │ ... │
│ │ (Tenant1)│ │ (Tenant2)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────┘
Implémentation du schéma séparé
Les schémas PostgreSQL sont une fonction puissante pour le multi-tenancy. Pour des informations détaillées sur la gestion des schémas PostgreSQL, les chaînes de connexion et les commandes d’administration de base de données, consultez notre PostgreSQL Cheatsheet.
-- Créer un schéma pour le locataire
CREATE SCHEMA tenant_123;
-- Définir le chemin de recherche pour les opérations du locataire
SET search_path TO tenant_123, public;
-- Créer des tables dans le schéma du locataire
CREATE TABLE tenant_123.users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gestion des connexions de base de données
La gestion efficace des connexions de base de données est cruciale pour les applications multi-locataires. Le code de gestion des connexions ci-dessous utilise GORM, mais vous pourriez vouloir explorer d’autres options ORM. Pour une comparaison approfondie des ORMs Go, y compris la gestion de la mise en pool des connexions, les caractéristiques de performance et les cas d’utilisation, consultez notre guide de comparaison des ORMs Go.
// Chaîne de connexion avec le chemin de recherche du schéma
func GetTenantDB(tenantID uint) *gorm.DB {
db := initializeDB()
db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
return db
}
// Ou utiliser la chaîne de connexion PostgreSQL
// postgresql://user:pass@host/db?search_path=tenant_123
Avantages du schéma séparé
- Meilleure isolation : La séparation au niveau du schéma réduit le risque de fuite de données
- Personnalisation : Chaque locataire peut avoir des structures de table différentes
- Coût modéré : Toujours une seule instance de base de données
- Sauvegardes par locataire plus simples : Peut sauvegarder individuellement les schémas
- Meilleure pour la conformité : Plus forte que le modèle de schéma partagé
Inconvénients du schéma séparé
- Complexité de gestion des schémas : Les migrations doivent s’exécuter par locataire
- Surcoût de connexion : Il faut définir search_path par connexion
- Échelle limitée : Le nombre de schémas est limité (PostgreSQL ~10k schémas)
- Requêtes croisées entre locataires : Plus complexes, nécessitent des références dynamiques aux schémas
- Limites de ressources : Les ressources de base de données restent partagées
Meilleur pour le schéma séparé
- SaaS à moyenne échelle (dizaines à centaines de locataires)
- Lorsque les locataires ont besoin de personnalisation de schéma
- Applications nécessitant une meilleure isolation que le schéma partagé
- Lorsque les exigences de conformité sont modérées
Modèle 3 : Base de données séparée par locataire
Chaque locataire obtient son propre instance complète de base de données, offrant une isolation maximale.
Architecture de base de données séparée
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Database 1 │ │ Database 2 │ │ Database 3 │
│ (Tenant A) │ │ (Tenant B) │ │ (Tenant C) │
└──────────────┘ └──────────────┘ └──────────────┘
Implémentation de base de données séparée
-- Créer une base de données pour le locataire
CREATE DATABASE tenant_enterprise_corp;
-- Se connecter à la base de données du locataire
\c tenant_enterprise_corp
-- Créer des tables (aucun tenant_id nécessaire !)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
Gestion dynamique des connexions
// Gestionnaire de pool de connexions
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()
// Vérification double après l'acquisition du verrou d'écriture
if db, exists := m.pools[tenantID]; exists {
return db, nil
}
// Créer une nouvelle connexion
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
}
Avantages de la base de données séparée
- Isolation maximale : Séparation complète des données
- Meilleure sécurité : Aucun risque d’accès croisé aux données des locataires
- Personnalisation complète : Chaque locataire peut avoir des schémas complètement différents
- Échelle indépendante : Échelle individuelle des bases de données des locataires
- Conformité facile : Respecte les exigences de séparation des données les plus strictes
- Sauvegardes par locataire : Sauvegarde/Restauration simple et indépendante
- Aucun locataire bruyant : Les charges de travail des locataires ne s’entretuent pas
Inconvénients de la base de données séparée
- Coût le plus élevé : Plusieurs instances de base de données nécessitent plus de ressources
- Complexité opérationnelle : Gérer de nombreuses bases de données (sauvegardes, surveillance, migrations)
- Limites de connexion : Chaque instance de base de données a des limites de connexion
- Analyse croisée entre locataires : Nécessite une fédération de données ou un ETL
- Complexité des migrations : Doit exécuter les migrations sur toutes les bases de données
- Surcoût de ressources : Plus de mémoire, de CPU et d’espace de stockage requis
Meilleur pour la base de données séparée
- SaaS d’entreprise avec des clients de haute valeur
- Exigences de conformité strictes (HIPAA, GDPR, SOC 2)
- Lorsque les locataires ont besoin de personnalisation significative
- Nombre de locataires faible à modéré (dizaines à quelques centaines)
- Lorsque les locataires ont des modèles de données très différents
Considérations de sécurité
Quel que soit le modèle choisi, la sécurité est primordiale :
1. Sécurité au niveau des lignes (RLS)
La RLS de PostgreSQL filtre automatiquement les requêtes par locataire, offrant une couche de sécurité au niveau de la base de données. Cette fonction est particulièrement puissante pour les applications multi-locataires. Pour plus de détails sur la RLS de PostgreSQL, les politiques de sécurité et d’autres fonctionnalités avancées de PostgreSQL, consultez notre PostgreSQL Cheatsheet.
-- Activer la RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Politique pour isoler par locataire
CREATE POLICY tenant_isolation ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- Application définit le contexte du locataire
SET app.current_tenant = '123';
2. Filtrage au niveau de l’application
Toujours filtrer par tenant_id dans le code de l’application. Les exemples ci-dessous utilisent GORM, mais différents ORMs ont leurs propres approches pour la construction des requêtes. Pour des conseils sur le choix de l’ORM approprié pour votre application multi-locataire, consultez notre comparaison des ORMs Go.
// ❌ Mauvais - Filtre manquant
db.Where("email = ?", email).First(&user)
// ✅ Bon - Inclure toujours le filtre tenant
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ Meilleur - Utiliser des scopes ou des middlewares
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. Mise en pool des connexions
Utilisez des poolers de connexions qui prennent en charge le contexte du locataire :
// PgBouncer avec mise en pool des transactions
// Ou utiliser la routage des connexions au niveau de l'application
4. Journalisation d’audit
Suivez toutes les accès aux données des locataires :
type AuditLog struct {
ID uint
TenantID uint
UserID uint
Action string
Table string
RecordID uint
Timestamp time.Time
IPAddress string
}
Optimisation des performances
Stratégie d’indexation
Une indexation correcte est cruciale pour les performances des bases de données multi-locataires. Comprendre les stratégies d’indexation SQL, y compris les index composés et les index partiels, est essentiel. Pour une référence complète sur les commandes SQL, y compris CREATE INDEX et l’optimisation des requêtes, consultez notre SQL Cheatsheet. Pour les fonctionnalités spécifiques à PostgreSQL d’indexation et d’optimisation des performances, consultez notre PostgreSQL Cheatsheet.
-- Index composés pour les requêtes de locataire
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- Index partiels pour les requêtes courantes spécifiques aux locataires
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
Optimisation des requêtes
// Utiliser des requêtes préparées pour les requêtes de locataire
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// Opérations par lot par locataire
db.Where("tenant_id = ?", tenantID).Find(&users)
// Utiliser le mise en pool des connexions par locataire (pour le modèle de base de données séparée)
Surveillance
Les outils de gestion de base de données efficaces sont essentiels pour surveiller les applications multi-locataires. Vous devrez suivre les performances des requêtes, l’utilisation des ressources et la santé de la base de données pour tous les locataires. Pour comparer les outils de gestion de base de données qui peuvent vous aider à cela, consultez notre comparaison DBeaver vs Beekeeper. Les deux outils offrent d’excellentes fonctionnalités pour gérer et surveiller les bases de données PostgreSQL dans des environnements multi-locataires.
Surveillez les métriques par locataire :
- Performance des requêtes par locataire
- Utilisation des ressources par locataire
- Nombre de connexions par locataire
- Taille de la base de données par locataire
Stratégie de migration
Modèle de schéma partagé
Lors de l’implémentation des migrations de base de données, votre choix d’ORM affecte la manière dont vous gérez les changements de schéma. Les exemples ci-dessous utilisent la fonction AutoMigrate de GORM, mais différents ORMs ont différentes stratégies de migration. Pour des informations détaillées sur la manière dont différents ORMs Go gèrent les migrations et la gestion des schémas, consultez notre comparaison des ORMs Go.
// Les migrations s'appliquent automatiquement à tous les locataires
func Migrate(db *gorm.DB) error {
return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
Modèle de schéma/base de données séparée
// Les migrations doivent s'exécuter par locataire
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 de décision
| Facteur | Schéma partagé | Schéma séparé | Base de données séparée |
|---|---|---|---|
| Isolation | Faible | Moyenne | Élevée |
| Coût | Faible | Moyen | Élevé |
| Évolutivité | Élevée | Moyenne | Faible-Moyenne |
| Personnalisation | Aucune | Moyenne | Élevée |
| Complexité opérationnelle | Faible | Moyenne | Élevée |
| Conformité | Limitée | Bonne | Excellente |
| Meilleur nombre de locataires | 1000+ | 10-1000 | 1-100 |
Approche hybride
Vous pouvez combiner des modèles pour différentes catégories de locataires :
// Petits locataires : Schéma partagé
if tenant.Tier == "standard" {
return GetSharedDB(tenant.ID)
}
// Locataires d'entreprise : Base de données séparée
if tenant.Tier == "enterprise" {
return GetTenantDB(tenant.ID)
}
Bonnes pratiques
- Toujours filtrer par locataire : Ne faites jamais confiance au code de l’application seul ; utilisez RLS si possible. Comprendre les fondamentaux de SQL aide à garantir une construction correcte des requêtes – consultez notre SQL Cheatsheet pour les meilleures pratiques de requêtes.
- Surveiller l’utilisation des ressources par locataire : Identifier et limiter les locataires bruyants. Utilisez des outils de gestion de base de données comme ceux comparés dans notre guide DBeaver vs Beekeeper pour suivre les métriques de performance.
- Implémenter un middleware de contexte de locataire : Centraliser l’extraction et la validation du locataire. Votre choix d’ORM affecte la manière dont vous implémentez cela – consultez notre comparaison des ORMs Go pour différentes approches.
- Utiliser le mise en pool des connexions : Gérer efficacement les connexions de base de données. Les stratégies spécifiques à PostgreSQL de mise en pool des connexions sont couvertes dans notre PostgreSQL Cheatsheet.
- Planifier la migration des locataires : Capacité à déplacer les locataires entre modèles
- Implémenter une suppression douce : Utiliser deleted_at à la place des suppressions dures pour les données des locataires
- Auditer tout : Journaliser toutes les accès aux données des locataires pour la conformité
- Tester l’isolation : Audit de sécurité régulier pour prévenir la fuite de données entre locataires
Conclusion
Le choix du bon modèle de base de données de multi-tenancy dépend de vos exigences spécifiques en matière d’isolation, de coût, d’évolutivité et de complexité opérationnelle. Le modèle de base de données partagée, schéma partagé fonctionne bien pour la plupart des applications SaaS, tandis que la base de données séparée par locataire est nécessaire pour les clients d’entreprise avec des exigences de conformité strictes.
Commencez par le modèle le plus simple qui répond à vos besoins, et planifiez la migration vers un modèle plus isolé à mesure que vos besoins évoluent. Priorisez toujours la sécurité et l’isolation des données, quel que soit le modèle choisi.
Liens utiles
- Documentation PostgreSQL sur la sécurité au niveau des lignes
- Architecture de base de données SaaS multi-locataire
- Conception de bases de données multi-locataires
- Comparaison des ORMs Go pour PostgreSQL : GORM vs Ent vs Bun vs sqlc
- PostgreSQL Cheatsheet : Un guide rapide pour les développeurs
- DBeaver vs Beekeeper - Outils de gestion de base de données SQL
- SQL Cheatsheet - les commandes SQL les plus utiles