Django é uma estrutura popular que você pode selecionar para desenvolver um aplicativo para sua empresa. Mas e se você quiser criar um aplicativo SaaS que vários clientes usarão? Que arquitetura você deve escolher? Vamos ver como essa tarefa pode ser abordada. Arquitetura de locatário único A abordagem mais direta é criar uma instância separada para cada cliente que você possui. Digamos que temos uma aplicação Django e um banco de dados. Então, para cada cliente, precisamos executar seu próprio banco de dados e instância de aplicação. Isso significa que cada instância do aplicativo possui apenas um locatário. Essa abordagem é simples de implementar: você precisa apenas iniciar uma nova instância de cada serviço que possui. Mas, ao mesmo tempo, pode causar um problema: cada cliente da infraestrutura. Pode não ser grande coisa se você planeja ter apenas alguns clientes ou se cada instância for pequena. aumentará significativamente o custo Entretanto, vamos supor que estamos construindo uma grande empresa que fornece mensagens corporativas para 100.000 organizações. Imagine como pode ser caro duplicar toda a infraestrutura para cada novo cliente! E, quando precisarmos atualizar a versão do aplicativo, precisamos implantá-la para cada cliente, então . a implantação também ficará mais lenta Arquitetura multilocatário Existe outra abordagem que pode ajudar em um cenário em que temos muitos clientes para a aplicação: uma arquitetura multilocatário. Significa que temos vários clientes, que chamamos de , mas todos usam apenas uma instância da aplicação. inquilinos Embora esta arquitetura resolva o problema do alto custo das instâncias dedicadas para cada cliente, ela introduz um novo problema: como podemos ter certeza de que os dados do cliente estão isolados com segurança de outros clientes? Discutiremos as seguintes abordagens: Usando : podemos identificar qual inquilino possui os dados pela chave estrangeira que precisamos adicionar a cada tabela do banco de dados. um banco de dados compartilhado e um esquema de banco de dados compartilhado Usando : dessa forma, não precisaremos manter várias instâncias de banco de dados, mas obteremos um bom nível de isolamento de dados do locatário. um banco de dados compartilhado, mas esquemas de banco de dados separados Usando : é semelhante ao exemplo de locatário único, mas não será o mesmo, pois ainda usaremos uma instância de aplicativo compartilhada e selecionaremos qual banco de dados usar verificando o locatário. bancos de dados separados Vamos nos aprofundar nessas ideias e ver como integrá-las à aplicação Django. Um banco de dados compartilhado com esquema compartilhado Esta opção pode ser a primeira que vem à mente: adicionar uma ForeignKey às tabelas e usá-la para selecionar os dados apropriados para cada inquilino. No entanto, tem uma enorme desvantagem: os dados dos inquilinos não são isolados, portanto, um pequeno erro de programação pode ser suficiente para vazar os dados do inquilino para o cliente errado. Vamos dar um exemplo de estrutura de banco de dados da : documentação do 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) Precisaremos identificar quais registros pertencem a qual inquilino. Então, precisamos adicionar uma tabela e uma chave estrangeira em cada tabela existente: Tenant 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) Para simplificar um pouco o código, podemos criar um modelo base abstrato que será reaproveitado em cada outro modelo que criarmos. 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) Como você pode ver, há pelo menos dois riscos principais aqui: um ao novo modelo ou um desenvolvedor pode esquecer de adicionar um campo de locatário desenvolvedor pode esquecer de usar esse campo ao filtrar os dados. O código-fonte deste exemplo pode ser encontrado no GitHub: . https://github.com/bp72/django-multitenancy-examples/tree/main/01_shared_database_shared_schema Um banco de dados compartilhado com esquemas separados Tendo em mente os riscos do esquema compartilhado, vamos considerar outra opção: o banco de dados ainda será compartilhado, mas criaremos um esquema dedicado para cada locatário. Para implementação, podemos olhar para uma biblioteca popular ( ). Django-tenants documentação Vamos adicionar ao nosso pequeno projeto (as etapas oficiais de instalação podem ser encontradas ). django-tenants aqui O primeiro passo é a instalação da biblioteca via : pip pip install django-tenants Alterar os modelos: o modelo agora estará em um aplicativo separado. Os modelos e não terão mais conexão com o locatário. Como os dados de diferentes locatários estarão em esquemas separados, não precisaremos mais vincular os registros individuais às linhas do locatário. Tenant Question Choice O arquivo inquilinos/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 ... O arquivo polls/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) Observe que Question e Choice não possuem mais uma chave estrangeira para Tenant! A outra coisa que mudou é que o Tenant agora está em um aplicativo separado: não é apenas para separar os domínios, mas também é importante, pois precisaremos armazenar a tabela no esquema compartilhado, e tabelas serão criadas para cada inquilino esquema. tenants polls Faça alterações no arquivo para oferecer suporte a vários esquemas e locatários: settings.py 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" A seguir, vamos criar e aplicar as migrações: python manage.py makemigrations python manage.py migrate_schemas --shared Como resultado, veremos que o esquema público será criado e conterá apenas tabelas compartilhadas. Precisaremos criar um locatário padrão para o esquema : public python manage.py create_tenant --domain-domain=default.com --schema_name=public --name=default_tenant Defina como se solicitado. is_primary True E então podemos começar a criar os verdadeiros inquilinos do serviço: 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 Observe que agora existem mais 2 esquemas no banco de dados que contêm tabelas : polls Agora, você obterá perguntas e opções de diferentes esquemas ao chamar APIs nos domínios configurados para os locatários. Tudo pronto! Embora a configuração pareça mais complicada e talvez ainda mais difícil se você migrar o aplicativo existente, a abordagem em si ainda tem muitas vantagens, como a segurança dos dados. O código do exemplo pode ser encontrado . aqui Bancos de dados separados A última abordagem que discutiremos hoje é ir ainda mais longe e ter bancos de dados separados para os inquilinos. Desta vez, teremos alguns bancos de dados: Armazenaremos os dados compartilhados, como o mapeamento do locatário, para os nomes dos bancos de dados no e criaremos um banco de dados separado para cada locatário. default_db Então precisaremos definir a configuração dos bancos de dados em settings.py: DATABASES = { 'default': { 'NAME': 'default_db', ... }, 'tenant_1': { 'NAME': 'tenant_1', ... }, 'tenant_2': { 'NAME': 'tenant_2', ... }, } E agora poderemos obter os dados de cada inquilino chamando o método QuerySet: using Questions.objects.using('tenant_1')… A desvantagem do método é que você precisará aplicar todas as migrações em cada banco de dados usando: python manage.py migrate --database=tenant_1 Também pode ser menos conveniente criar um novo banco de dados para cada inquilino, em comparação com o uso dos ou apenas usar uma chave estrangeira como na abordagem de esquema compartilhado. django-tenants Por outro lado, o isolamento dos dados do inquilino é muito bom: as bases de dados podem ser separadas fisicamente. Outra vantagem é que não ficaremos limitados em usar apenas o Postgresql conforme exigido pelos , podemos selecionar qualquer mecanismo que atenda às nossas necessidades. django-tenants Mais informações sobre o tópico de múltiplos bancos de dados podem ser encontradas na do Django. documentação Comparação Inquilino único MT com esquema compartilhado MT com esquema separado MT com bancos de dados separados Isolamento de dados ✅Alto ❌Mais baixo ✅Alto ✅Alto Risco de vazamento de dados acidentalmente ✅Baixo ❌Alto ✅Baixo ✅Baixo Custo de infraestrutura ❌Maior com cada inquilino ✅Inferior ✅Inferior ✅❌ Inferior ao de locatário único Velocidade de implantação ❌Abaixar com cada inquilino ✅ ✅❌ As migrações serão mais lentas, pois precisam ser executadas para cada esquema ✅❌ As migrações serão mais lentas, pois precisam ser executadas para cada banco de dados Fácil de implementar ✅ ❌ Requer muitas alterações se o serviço já tiver sido implementado como um aplicativo de locatário único ✅ ✅ Conclusão Para resumir tudo o que foi dito acima, parece que não existe solução mágica para o problema, cada abordagem tem seus prós e contras, então cabe aos desenvolvedores decidir qual compensação eles podem ter. Bancos de dados separados fornecem o melhor isolamento para os dados do locatário e são simples de implementar, no entanto, custa mais caro para manutenção: n banco de dados para atualizar, os números de conexões de banco de dados são maiores. Um banco de dados compartilhado com um esquema separado é complexo para implementar e pode ter alguns problemas com a migração. Inquilino único é o mais simples de implementar, mas custa-lhe pelo consumo excessivo de recursos, uma vez que tem uma cópia completa do seu serviço por inquilino.