Como usar django-tenant-schemas para criar uma aplicação multilocatária

Uma aplicação multilocatária, ou software multitenancy, é uma arquitetura em que uma única instância atende a vários locatários. Em outras palavras, hospedamos a aplicação em um único lugar e todos os nossos clientes, os locatários, usam os mesmos recursos. Essa arquitetura é uma alternativa à tradicional, em que cada locatário hospeda uma instância dedicada. A imagem a seguir mostra a diferença entre as duas abordagens.

Escolher uma arquitetura tradicional ou multilocatária não é simples. Às vezes, regras da empresa tornam a opção multilocatária inviável; por exemplo, ela exige hospedar a aplicação nos próprios servidores. Se não for o caso, essa abordagem tem algumas vantagens:
- Gerenciamento de versões mais fácil. Em uma abordagem de locatário único, cada mudança de versão exige atualizar todas as instâncias. Na multilocatária, só há uma instância para atualizar.
- Recursos mais baratos. Hospedar uma aplicação multilocatária costuma exigir menos recursos, como CPU e memória, do que hospedar a mesma aplicação em instâncias independentes. Além disso, uma gestão inteligente dos recursos, por exemplo com um orquestrador como Kubernetes, pode economizar muitos recursos.
- Mais facilidade para compartilhar informações entre locatários. Às vezes, há informações públicas às quais todos precisam ter acesso. Gerenciá-las é mais fácil com uma abordagem multilocatária do que com uma tradicional.
Vários tipos de aplicações podem se beneficiar de uma arquitetura multilocatária. Entre os exemplos mais claros estão os produtos SaaS, software como serviço.
Bancos de dados em aplicações multilocatárias
Aplicações multilocatárias precisam lidar com questões complexas. A principal característica dessa arquitetura é compartilhar recursos, o que traz desafios como distribuição de recursos, segurança e personalizações. Nesta publicação, vamos focar apenas no projeto do banco de dados.
Em uma aplicação multilocatária, os recursos são compartilhados, mas as informações de cada locatário costumam ser privadas. Precisamos garantir que estejam protegidas e acessíveis apenas aos seus membros. Há várias maneiras de implementar isso, mas vamos listar somente 3.
Múltiplas instâncias de banco de dados
Talvez seja a opção mais segura em termos de privacidade e segurança. Cada locatário tem uma instância independente de banco de dados onde suas informações são armazenadas. O servidor da aplicação precisa se conectar a todas e determinar qual banco ativar para cada usuário conforme o locatário ao qual pertence.
Algumas vantagens dessa abordagem:
- Os dados são seguros e privados porque ficam em bancos independentes. Cada locatário acessa somente o próprio banco, mantendo as informações dos demais privadas.
- Outros locatários nunca se conectam ao seu banco de dados, o que melhora o desempenho. Como mencionamos, distribuir recursos entre locatários é um desafio. Ter bancos diferentes elimina suas conexões como recurso compartilhado.
Algumas desvantagens:
- Ter várias instâncias significa custos mais altos. Como mencionamos na introdução, uma das principais vantagens da arquitetura multilocatária é compartilhar recursos e reduzir custos. Nessa abordagem, o banco deixa de ser compartilhado.
- Adicionar novos locatários exige novas conexões a novos bancos. Essa abordagem requer uma conexão independente entre o servidor da aplicação e cada banco. Ao adicionar um locatário, é preciso criar um banco e conectá-lo ao servidor. Essa conexão pode causar uma indisponibilidade no servidor da aplicação que afete os demais locatários.
- A gestão de cada banco é independente. Tarefas como backups e vacuum precisam ser realizadas por instância.
- Ter dados compartilhados entre locatários não é trivial. Todos os dados compartilhados precisam ser replicados em todos os bancos.
Um banco de dados, um esquema
Essa situação é o oposto da anterior. As informações de todos os locatários ficam no mesmo esquema, usando as mesmas tabelas. Cada tabela tem um atributo que identifica o locatário proprietário da informação, como uma coluna tenant_id. O servidor da aplicação precisa filtrar por locatário em cada solicitação.
Vantagens dessa abordagem:
- Todos os dados ficam em uma única instância. A configuração é idêntica à de uma aplicação independente, facilitando tarefas de gestão do banco, como backups e vacuum.
- Existe uma única conexão entre o banco e o servidor da aplicação. Adicionar novos locatários não exige mudanças de configuração e é uma tarefa trivial.
- Compartilhar dados entre locatários é fácil. Basta armazená-los em uma tabela sem a coluna tenant_id .
Por outro lado, a abordagem também tem desvantagens:
- O servidor da aplicação sempre precisa filtrar por tenant_id antes de acessar os dados. Se o filtro não for aplicado, um usuário pode acessar informações de um locatário do qual não é membro. Essa vulnerabilidade de privacidade significa que um único erro de desenvolvimento em um componente pode comprometer informações sensíveis.
- Todo o desempenho da aplicação pode ser afetado por um único locatário, pois temos uma única instância e todas as tabelas são compartilhadas. Por exemplo, se um usuário executar uma operação em massa sobre uma tabela enorme que exige bloqueá-la, ela ficará bloqueada para todos. A usabilidade pode ser afetada porque um locatário executa uma operação em massa.
Um banco de dados, múltiplos esquemas
Essa opção combina as anteriores. Temos uma única instância de banco de dados para armazenar as informações de todos os locatários. A diferença em relação à abordagem anterior é criar um esquema separado para cada um. Quando o servidor precisa acessar os dados, ativa o esquema conforme o usuário e seu locatário.
Essa abordagem compartilha algumas das vantagens anteriores:
- Os dados são seguros e privados porque ficam em esquemas independentes.
- Todos os dados ficam em uma única instância, reduzindo custos, configurações e tarefas de gestão.
- Adicionar novos locatários envolve apenas criar novos esquemas. A conexão com o banco é compartilhada e não exige mudanças de configuração.
- Compartilhar dados entre locatários é fácil. Todos os dados compartilhados podem ficar em um esquema público acessível a todos.
Porém, essa abordagem também tem limitações e desvantagens:
- O desempenho geral pode ser afetado por um único locatário. Se um usuário começar a criar novas conexões à instância, todos perceberão lentidão. Porém, o problema de bloqueio mencionado antes não existe porque as tabelas são armazenadas separadamente para cada locatário.
- Mudar a estrutura das tabelas exige modificar múltiplos esquemas. Essa tarefa de gestão pode ser um pouco cansativa e complexa.
Implementando multitenancy em uma aplicação Django
Agora que definimos o que é uma aplicação multilocatária e as diferentes abordagens para a arquitetura do banco de dados, vamos ver como implementá-la em Django.
Por que usar a biblioteca django-tenant-schemas?
O primeiro problema é que Django não oferece uma forma padrão de lidar com múltiplos locatários. Pela nossa experiência com Django, a melhor maneira de resolver essa limitação é usar o pacote django-tenant-schemas.
Alguns motivos que justificam essa decisão:
- É uma solução de multitenancy com a abordagem de “um banco de dados, múltiplos esquemas”. Consideramos que ela oferece mais vantagens e pode ser usada em mais situações.
- É muito fácil integrar a uma aplicação Django existente. A solução não afeta suas aplicações. Ao contrário de alternativas que exigem modificar todas elas, esse pacote só precisa de uma pequena configuração para começar a ser usado.
- A biblioteca resolve o roteamento entre locatários. Como mencionamos, precisamos ativar o esquema correspondente ao locatário com o qual o usuário está trabalhando. A biblioteca já implementa uma solução que facilita essa tarefa.
- Por fim, a biblioteca também permite compartilhar dados entre locatários.
Uma aplicação de exemplo
Vamos ver como transformar facilmente uma aplicação Django em multilocatária com um exemplo simples usando django-tenant-schemas. Imagine que estamos criando um produto para o setor de transporte que atribui motoristas a remessas.
No início do projeto, não imaginávamos que o produto cresceria, então criamos um único projeto Django com 3 aplicações.
Locations
Aplicação responsável por armazenar todas as localizações. Seu modelo principal é Location e se parece com isto:
from django.db import models
class Location(models.Model):
name = models.CharField(max_length=255, unique=True)
address = models.CharField(max_length=255)
latitude = models.FloatField()
longitude = models.FloatField()
Drivers
Aplicação responsável por armazenar todos os motoristas. Seu modelo principal é Driver e se parece com isto:
from django.db import models
class Driver(models.Model):
name = models.CharField(max_length=255, unique=True)
Shipments
Aplicação responsável por armazenar todas as remessas. Seu modelo principal é Shipment e se parece com isto:
from django.db import models
from locations.models import Location
from drivers.models import Driver
class Shipment(models.Model):
origin = models.ForeignKey(Location, on_delete=models.CASCADE)
destination = models.ForeignKey(Location, on_delete=models.CASCADE)
driver = models.ForeignKey(Driver, on_delete=models.CASCADE)
completion = models.DateTimeField()
Agora imagine que, depois de algum tempo, a solução funciona muito bem, várias empresas querem usá-la e o produto simples se torna uma solução SaaS. Esse é claramente um caso de uso da arquitetura multilocatária. Vamos converter essa aplicação simples em multilocatária.
Instalando a biblioteca
O primeiro passo é instalar o pacote django-tenant-schemas com pip.
pip install django-tenant-schemas
Configurando a aplicação para trabalhar com locatários
O segundo passo é configurar a aplicação para trabalhar com locatários. A vantagem do pacote django-tenant-schemas é que só precisamos modificar o arquivo de configurações do Django.
Por enquanto, vamos supor que identificaremos o locatário pela URL, por exemplo com subdomínios.
As mudanças que precisamos implementar nesse arquivo são:
- Alterar DATABASE_ENGINE:
DATABASES = {
'default': {
'ENGINE': 'tenant_schemas.postgresql_backend',
# ..
}
}
- Adicionar tenant_schemas.routers.TenantSyncRouter a DATABASE_ROUTERS:
DATABASE_ROUTERS = (
'tenant_schemas.routers.TenantSyncRouter',
)
- Como supomos que os locatários são identificados pela URL, podemos usar o middleware do pacote para direcionar corretamente as solicitações aos esquemas.
MIDDLEWARE_CLASSES = (
'tenant_schemas.middleware.TenantMiddleware',
)
- Criar e declarar um modelo de locatário. Em toda aplicação multilocatária, precisamos especificar em algum lugar os diferentes locatários com os quais trabalhamos. Aqui, precisamos criar um modelo que herde de_ TenantMixin _e declará-lo no arquivo de configurações como TENANT_MODEL. No exemplo, criaremos uma aplicação chamada organizations e um modelo de locatário chamado Organization.
from django.db import models
from tenant_schemas.models import TenantMixin
class Organization(TenantMixin):
name = models.CharField(max_length=100)
TENANT_MODEL = "organizations.Organization"
- Dividir as aplicações em compartilhadas ou de locatário. Com esse pacote, a diferença entre um modelo compartilhado e um de locatário é definida no nível da aplicação e declarada nos arquivos de configuração. As aplicações compartilhadas ficam no esquema público, acessível a todos; as de locatário têm um esquema independente. Podemos supor que as localizações são públicas e gerenciar essa aplicação como compartilhada. Já drivers e shipments armazenam informações privadas de cada locatário, então precisamos declará-las como aplicações de locatário. Com essas suposições, a configuração fica assim:
SHARED_APPS = (
'tenant_schemas', # mandatory, should always be before any django app
'organizations', # you must list the app where your tenant model resides in
'locations',
'django.contrib.contenttypes',
)
TENANT_APPS = (
'django.contrib.contenttypes',
# your tenant-specific apps
'drivers',
'shipments',
)
INSTALLED_APPS = (
'tenant_schemas', # mandatory, should always be before any django app
'organizations',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'locations',
'drivers',
'shipments',
)
Após essas etapas, tudo estará pronto para implantar e executar a aplicação em uma arquitetura multilocatária. Só é preciso considerar as migrações. O comando migrate padrão do Django cria todas as tabelas no esquema público, fazendo a aplicação falhar ao acessar uma aplicação de locatário. Para implantá-la corretamente, use o comando migrate_schema.
Limitações do pacote
Depois de trabalhar com o pacote, a única limitação que encontramos é que ele só funciona com Postgres.
Como mostramos, criar uma aplicação multilocatária em Django não é complexo e permite implementar uma solução SaaS sem problemas. Nosso exemplo apresenta o uso mais básico e simples de django-tenant-schemas. O pacote tem mais funcionalidades para criar soluções mais complexas; por exemplo, você pode personalizar o middleware e escolher o locatário por algo além da URL. Para mais informações, consulte a documentação oficial. Esperamos que esta publicação ajude você a entender melhor o que é uma aplicação multilocatária e como criá-la em Django.