Parece que você está perguntando sobre a diferença entre o modelo de dados e o modelo de domínio - o último é onde você pode encontrar a lógica de negócios e as entidades percebidas pelo usuário final; o primeiro é onde você realmente armazena seus dados.
Além disso, interpretei a 3ª parte da sua pergunta como: como perceber a falha em manter esses modelos separados.
Esses são dois conceitos muito diferentes e é sempre difícil mantê-los separados. No entanto, existem alguns padrões e ferramentas comuns que podem ser usados para esse fim.
Sobre o modelo de domínio
A primeira coisa que você precisa reconhecer é que seu modelo de domínio não é realmente sobre dados; trata-se de ações e perguntas como "ativar este usuário", "desativar esse usuário", "quais usuários estão atualmente ativados?" e "qual é o nome desse usuário?". Em termos clássicos: trata-se de consultas e comandos .
Pensando em Comandos
Vamos começar examinando os comandos no seu exemplo: "ativar este usuário" e "desativar este usuário". O bom dos comandos é que eles podem ser facilmente expressados por pequenos cenários de quando e quando:
dado a um usuário inativo
quando o administrador ativa esse usuário
, ele se torna ativo
e um e-mail de confirmação é enviado ao usuário
e uma entrada é adicionada ao log do sistema
(etc. etc.)
Esse cenário é útil para ver como diferentes partes da sua infraestrutura podem ser afetadas por um único comando - nesse caso, seu banco de dados (algum tipo de sinalizador 'ativo'), seu servidor de email, seu log do sistema etc.
Esse cenário também ajuda muito na configuração de um ambiente de desenvolvimento orientado a testes.
E, finalmente, pensar em comandos realmente ajuda a criar um aplicativo orientado a tarefas. Seus usuários irão apreciar isso :-)
Expressando comandos
O Django fornece duas maneiras fáceis de expressar comandos; ambas são opções válidas e não é incomum misturar as duas abordagens.
A camada de serviço
O módulo de serviço já foi descrito por @Hedde . Aqui você define um módulo separado e cada comando é representado como uma função.
services.py
def activate_user(user_id):
user = User.objects.get(pk=user_id)
# set active flag
user.active = True
user.save()
# mail user
send_mail(...)
# etc etc
Usando formulários
A outra maneira é usar um Django Form para cada comando. Eu prefiro essa abordagem, porque combina vários aspectos intimamente relacionados:
- execução do comando (o que ele faz?)
- validação dos parâmetros de comando (ele pode fazer isso?)
- apresentação do comando (como posso fazer isso?)
forms.py
class ActivateUserForm(forms.Form):
user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
# the username select widget is not a standard Django widget, I just made it up
def clean_user_id(self):
user_id = self.cleaned_data['user_id']
if User.objects.get(pk=user_id).active:
raise ValidationError("This user cannot be activated")
# you can also check authorizations etc.
return user_id
def execute(self):
"""
This is not a standard method in the forms API; it is intended to replace the
'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern.
"""
user_id = self.cleaned_data['user_id']
user = User.objects.get(pk=user_id)
# set active flag
user.active = True
user.save()
# mail user
send_mail(...)
# etc etc
Pensando em consultas
Seu exemplo não continha nenhuma consulta, então tomei a liberdade de fazer algumas consultas úteis. Eu prefiro usar o termo "pergunta", mas consultas é a terminologia clássica. As consultas interessantes são: "Qual é o nome deste usuário?", "Esse usuário pode efetuar login?", "Mostre-me uma lista de usuários desativados" e "Qual é a distribuição geográfica dos usuários desativados?"
Antes de começar a responder a essas perguntas, você deve sempre fazer duas perguntas: é uma consulta de apresentação apenas para meus modelos e / ou uma lógica de negócios vinculada à execução de meus comandos e / ou uma consulta de relatório .
As consultas de apresentação são feitas apenas para melhorar a interface do usuário. As respostas às consultas da lógica de negócios afetam diretamente a execução de seus comandos. As consultas de relatório são apenas para fins analíticos e têm restrições de tempo mais fracas. Essas categorias não são mutuamente exclusivas.
A outra pergunta é: "eu tenho controle completo sobre as respostas?" Por exemplo, ao consultar o nome do usuário (neste contexto), não temos controle sobre o resultado, porque contamos com uma API externa.
Fazendo consultas
A consulta mais básica no Django é o uso do objeto Manager:
User.objects.filter(active=True)
Obviamente, isso só funciona se os dados estiverem realmente representados no seu modelo de dados. Isso não é sempre o caso. Nesses casos, você pode considerar as opções abaixo.
Tags e filtros personalizados
A primeira alternativa é útil para consultas que são meramente de apresentação: tags personalizadas e filtros de modelo.
template.html
<h1>Welcome, {{ user|friendly_name }}</h1>
template_tags.py
@register.filter
def friendly_name(user):
return remote_api.get_cached_name(user.id)
Métodos de consulta
Se sua consulta não for meramente de apresentação, você poderá adicionar consultas ao services.py (se estiver usando isso) ou introduzir um módulo queries.py :
queries.py
def inactive_users():
return User.objects.filter(active=False)
def users_called_publysher():
for user in User.objects.all():
if remote_api.get_cached_name(user.id) == "publysher":
yield user
Modelos de proxy
Modelos de proxy são muito úteis no contexto da lógica de negócios e dos relatórios. Você basicamente define um subconjunto aprimorado do seu modelo. Você pode substituir o QuerySet básico de um gerente substituindo o Manager.get_queryset()
método
models.py
class InactiveUserManager(models.Manager):
def get_queryset(self):
query_set = super(InactiveUserManager, self).get_queryset()
return query_set.filter(active=False)
class InactiveUser(User):
"""
>>> for user in InactiveUser.objects.all():
… assert user.active is False
"""
objects = InactiveUserManager()
class Meta:
proxy = True
Modelos de consulta
Para consultas que são inerentemente complexas, mas são executadas com bastante frequência, existe a possibilidade de modelos de consulta. Um modelo de consulta é uma forma de desnormalização em que dados relevantes para uma única consulta são armazenados em um modelo separado. O truque, é claro, é manter o modelo desnormalizado em sincronia com o modelo primário. Os modelos de consulta podem ser usados apenas se as alterações estiverem totalmente sob seu controle.
models.py
class InactiveUserDistribution(models.Model):
country = CharField(max_length=200)
inactive_user_count = IntegerField(default=0)
A primeira opção é atualizar esses modelos em seus comandos. Isso é muito útil se esses modelos forem alterados apenas por um ou dois comandos.
forms.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
Uma opção melhor seria usar sinais personalizados. É claro que esses sinais são emitidos por seus comandos. Os sinais têm a vantagem de poder manter vários modelos de consulta sincronizados com o modelo original. Além disso, o processamento do sinal pode ser transferido para tarefas em segundo plano, usando o Aipo ou estruturas semelhantes.
signs.py
user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
forms.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
user_activated.send_robust(sender=self, user=user)
models.py
class InactiveUserDistribution(models.Model):
# see above
@receiver(user_activated)
def on_user_activated(sender, **kwargs):
user = kwargs['user']
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
Mantendo-o limpo
Ao usar essa abordagem, torna-se ridiculamente fácil determinar se seu código permanece limpo. Basta seguir estas diretrizes:
- Meu modelo contém métodos que fazem mais do que gerenciar o estado do banco de dados? Você deve extrair um comando.
- Meu modelo contém propriedades que não são mapeadas para os campos do banco de dados? Você deve extrair uma consulta.
- Meu modelo faz referência à infraestrutura que não é meu banco de dados (como correio)? Você deve extrair um comando.
O mesmo vale para as visualizações (porque as visualizações geralmente sofrem do mesmo problema).
- Minha visão gerencia ativamente os modelos de banco de dados? Você deve extrair um comando.
Algumas referências
Documentação do Django: modelos de proxy
Documentação do Django: sinais
Arquitetura: Design Orientado a Domínio