Django est un framework populaire que vous pouvez sélectionner pour développer une application pour votre entreprise. Mais que se passe-t-il si vous souhaitez créer une application SaaS que plusieurs clients utiliseront ? Quelle architecture choisir ? Voyons comment cette tâche peut être abordée.
L'approche la plus simple consiste à créer une instance distincte pour chaque client dont vous disposez. Disons que nous avons une application Django et une base de données. Ensuite, pour chaque client, nous devons exécuter sa propre base de données et sa propre instance d'application. Cela signifie que chaque instance d'application n'a qu'un seul locataire.
Cette approche est simple à mettre en œuvre : il vous suffit de démarrer une nouvelle instance de chaque service dont vous disposez. Mais en même temps, cela peut poser un problème : chaque client va augmenter considérablement le coût de l'infrastructure. Ce n’est peut-être pas grave si vous envisagez de n’avoir que quelques clients ou si chaque instance est petite.
Cependant, supposons que nous construisions une grande entreprise qui fournit un messager d'entreprise à 100 000 organisations. Imaginez à quel point il peut être coûteux de dupliquer toute l’infrastructure pour chaque nouveau client ! Et, lorsque nous devons mettre à jour la version de l'application, nous devons la déployer pour chaque client, donc le déploiement sera également ralenti .
Il existe une autre approche qui peut s'avérer utile dans un scénario où nous avons beaucoup de clients pour l'application : une architecture multi-tenant. Cela signifie que nous avons plusieurs clients, que nous appelons locataires , mais qu'ils n'utilisent tous qu'une seule instance de l'application.
Si cette architecture résout le problème du coût élevé des instances dédiées pour chaque client, elle introduit un nouveau problème : comment être sûr que les données du client sont isolées de manière sécurisée des autres clients ?
Nous discuterons des approches suivantes :
Utilisation d'une base de données partagée et d'un schéma de base de données partagée : nous pouvons identifier quel locataire possède les données grâce à la clé étrangère que nous devons ajouter à chaque table de base de données.
Utiliser une base de données partagée, mais des schémas de base de données séparés : de cette façon, nous n'aurons pas besoin de maintenir plusieurs instances de base de données mais obtiendrons un bon niveau d'isolation des données des locataires.
Utilisation de bases de données distinctes : cela ressemble à l'exemple à locataire unique, mais ce ne sera pas le même, car nous utiliserons toujours une instance d'application partagée et sélectionnerons la base de données à utiliser en vérifiant le locataire.
Approfondissons ces idées et voyons comment les intégrer à l'application Django.
Cette option est peut-être la première qui vient à l'esprit : ajouter une ForeignKey aux tables et l'utiliser pour sélectionner les données appropriées pour chaque locataire. Cependant, il présente un énorme inconvénient : les données des locataires ne sont pas du tout isolées, donc une petite erreur de programmation peut suffire à divulguer les données du locataire vers le mauvais client.
Prenons un exemple de structure de base de données issu de la documentation Django :
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Nous devrons identifier quels enregistrements appartiennent à quel locataire. Nous devons donc ajouter une table Tenant
et une clé étrangère dans chaque table existante :
class Tenant(models.Model): name = models.CharField(max_length=200) class Question(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Pour simplifier un peu le code, nous pouvons créer un modèle de base abstrait qui sera réutilisé dans chaque autre modèle que nous créons.
class Tenant(models.Model): name = models.CharField(max_length=200) class BaseModel(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) class Meta: abstract = True class Question(BaseModel): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(BaseModel): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Comme vous pouvez le constater, il existe ici au moins deux risques majeurs : un développeur peut oublier d'ajouter un champ locataire au nouveau modèle, ou un développeur peut oublier d'utiliser ce champ lors du filtrage des données.
Le code source de cet exemple peut être trouvé sur GitHub : https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema .
En gardant à l'esprit les risques du schéma partagé, envisageons une autre option : la base de données sera toujours partagée, mais nous créerons un schéma dédié pour chaque locataire. Pour l'implémentation, nous pouvons consulter une bibliothèque populaire django-tenants ( documentation ).
Ajoutons django-tenants
à notre petit projet (les étapes d'installation officielles peuvent être trouvées ici ).
La première étape est l'installation de la bibliothèque via pip
:
pip install django-tenants
Changez les modèles : le modèle Tenant
sera désormais dans une application distincte. Les modèles Question
et Choice
n'auront plus de connexion avec le locataire. Comme les données des différents locataires se trouveront dans des schémas distincts, nous n'aurons plus besoin de lier les enregistrements individuels aux lignes du locataire.
Le fichier tenants/models.py
from django.db import models from django_tenants.models import TenantMixin, DomainMixin class Tenant(TenantMixin): name = models.CharField(max_length=200) # default true, schema will be automatically created and synced when it is saved auto_create_schema = True class Domain(DomainMixin): # a required table for django-tenants too ...
Le fichier sondages/models.py
from django.db import models class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0)
Notez que Question et Choice n'ont plus de clé étrangère pour Tenant !
L'autre chose qui a été modifiée est que le locataire est maintenant dans une application distincte : ce n'est pas seulement pour séparer les domaines mais c'est aussi important car nous devrons stocker la table tenants
dans le schéma partagé, et des tables polls
seront créées pour chaque locataire. schéma.
Apportez des modifications au fichier settings.py
pour prendre en charge plusieurs schémas et locataires :
DATABASES = { 'default': { 'ENGINE': 'django_tenants.postgresql_backend', # .. } } DATABASE_ROUTERS = ( 'django_tenants.routers.TenantSyncRouter', ) MIDDLEWARE = ( 'django_tenants.middleware.main.TenantMainMiddleware', #... ) TEMPLATES = [ { #... 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.request', #... ], }, }, ] SHARED_APPS = ( 'django_tenants', # mandatory 'tenants', # you must list the app where your tenant model resides in 'django.contrib.contenttypes', # everything below here is optional 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.admin', ) TENANT_APPS = ( # your tenant-specific apps 'polls', ) INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS] TENANT_MODEL = "tenants.Tenant" TENANT_DOMAIN_MODEL = "tenants.Domain"
Ensuite, créons et appliquons les migrations :
python manage.py makemigrations
python manage.py migrate_schemas --shared
De ce fait, nous verrons que le schéma public sera créé et ne contiendra que des tables partagées.
Nous devrons créer un locataire par défaut pour le schéma public
:
python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant
Définissez is_primary
sur True
si demandé.
Et puis, nous pouvons commencer à créer les véritables locataires du service :
python manage.py create_tenant --domain-domain=tenant1.com --schema_name=tenant1 --name=tenant_1 python manage.py create_tenant --domain-domain=tenant2.com --schema_name=tenant2 --name=tenant_2
Notez qu'il y a maintenant 2 schémas supplémentaires dans la base de données qui contiennent des tables polls
:
Désormais, vous obtiendrez les questions et les choix de différents schémas lorsque vous appellerez des API sur les domaines que vous avez configurés pour les locataires – tout est fait !
Bien que la configuration semble plus compliquée et peut-être encore plus difficile si vous migrez l'application existante, l'approche elle-même présente encore de nombreux avantages tels que la sécurité des données.
Le code de l'exemple peut être trouvé ici .
La dernière approche dont nous discuterons aujourd'hui va encore plus loin et dispose de bases de données distinctes pour les locataires.
Cette fois, nous aurons quelques bases de données :
Nous stockerons les données partagées telles que le mappage du locataire avec les noms des bases de données dans default_db
et créerons une base de données distincte pour chaque locataire.
Ensuite, nous devrons définir la configuration des bases de données dans le fichier settings.py :
DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, }
Et maintenant, nous allons pouvoir obtenir les données de chaque locataire en appelant using
de la méthode QuerySet :
Questions.objects.using('tenant_1')…
L'inconvénient de la méthode est que vous devrez appliquer toutes les migrations sur chaque base de données en utilisant :
python manage.py migrate --database=tenant_1
Il peut également être moins pratique de créer une nouvelle base de données pour chaque locataire, par rapport à l'utilisation des django-tenants
ou simplement à l'aide d'une clé étrangère comme dans l'approche de schéma partagé.
En revanche, l'isolation des données du locataire est vraiment bonne : les bases de données peuvent être physiquement séparées. Un autre avantage est que nous ne serons pas limités en utilisant uniquement Postgresql comme l'exigent les django-tenants
, nous pouvons sélectionner n'importe quel moteur qui répondra à nos besoins.
Plus d'informations sur le sujet des bases de données multiples peuvent être trouvées dans la documentation Django .
| Locataire unique | MT avec schéma partagé | MT avec schéma séparé | MT avec bases de données séparées |
---|---|---|---|---|
Isolement des données | ✅Élevé | ❌Le plus bas | ✅Élevé | ✅Élevé |
Risque de fuite accidentelle de données | ✅Faible | ❌Élevé | ✅Faible | ✅Faible |
Coût des infrastructures | ❌Plus élevé avec chaque locataire | ✅Inférieur | ✅Inférieur | ✅❌ Inférieur à celui d'un locataire unique |
Vitesse de déploiement | ❌Baisse avec chaque locataire | ✅ | ✅❌ Les migrations seront plus lentes car elles doivent être exécutées pour chaque schéma | ✅❌ Les migrations seront plus lentes car elles devront être exécutées pour chaque base de données |
Facile à mettre en œuvre | ✅ | ❌ Nécessite de nombreux changements si le service a déjà été implémenté en tant qu'application à locataire unique | ✅ | ✅ |
Pour résumer tout ce qui précède, il semble qu'il n'y ait pas de solution miracle au problème, chaque approche a ses avantages et ses inconvénients, c'est donc aux développeurs de décider quel compromis ils peuvent faire.
Les bases de données séparées offrent la meilleure isolation pour les données du locataire et sont simples à mettre en œuvre, cependant, cela vous coûte plus cher en maintenance : n base de données à mettre à jour, les nombres de connexions aux bases de données sont plus élevés.
Une base de données partagée avec un schéma distinct peu complexe à implémenter et peut rencontrer des problèmes de migration.
Le locataire unique est le plus simple à mettre en œuvre, mais cela vous coûte cher en surconsommation de ressources puisque vous disposez d'une copie complète de votre service par locataire.