Separação da lógica de negócios e acesso a dados no django

484

Estou escrevendo um projeto no Django e vejo que 80% do código está no arquivo models.py. Esse código é confuso e, depois de um certo tempo, deixo de entender o que realmente está acontecendo.

Aqui está o que me incomoda:

  1. Acho feio que o nível do meu modelo (que deveria ser responsável apenas pelo trabalho com dados de um banco de dados) também esteja enviando e-mails, andando na API para outros serviços, etc.
  2. Além disso, acho inaceitável colocar a lógica de negócios na exibição, pois dessa maneira fica difícil de controlar. Por exemplo, no meu aplicativo, há pelo menos três maneiras de criar novas instâncias de User, mas tecnicamente ele deve criá-las de maneira uniforme.
  3. Nem sempre percebo quando os métodos e propriedades dos meus modelos se tornam não determinísticos e quando desenvolvem efeitos colaterais.

Aqui está um exemplo simples. A princípio, o Usermodelo era assim:

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

Com o tempo, tornou-se isso:

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

O que eu quero é separar entidades no meu código:

  1. Entidades do meu banco de dados, nível do banco de dados: O que contém meu aplicativo?
  2. Entidades do meu aplicativo, nível da lógica de negócios: O que pode fazer meu aplicativo?

Quais são as boas práticas para implementar essa abordagem que pode ser aplicada no Django?

desfazer
fonte
14
Leia sobre sinais
Konstant
1
bem você removeu a tag, mas você poderia usar DCI para accmplish a separação de que o sistema faz (a funcionalidade) e que o sistema é (os dados / modelo de domínio)
Rune FS
2
Você propõe implementar toda a lógica comercial nos retornos de chamada de sinal? Infelizmente, nem todo o meu aplicativo pode ser vinculado a eventos no banco de dados.
defuz
No Rune FS, tentei usar o DCI, mas me pareceu que ele não precisava de muito para o meu projeto: contexto, definição de papéis como mixin aos objetos, etc. Existe uma maneira mais fácil de separar "faz" e " é"? Você poderia dar um exemplo mínimo?
defuz

Respostas:

635

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

publysher
fonte
11
É bom ver uma resposta incorporando DDD em uma pergunta relacionada ao django. Só porque o Django emprega o ActiveRecord para persistência não significa que a separação de preocupações deva sair pela janela. Ótima resposta.
Scott Coates
6
Se eu quiser validar se o usuário looged é o proprietário de um objeto antes de excluí-lo, devo verificar isso na exibição ou no módulo de formulário / serviço?
Ivan
6
@ Ivan: ambos. Ele deve estar no módulo formulário / serviço porque faz parte das restrições de negócios. Também deve estar na visualização, porque você deve apresentar apenas ações que os usuários possam realmente executar.
Publiclysher 5/05
4
Gerente costume métodos são uma boa maneira de implementar consultas: User.objects.inactive_users(). Mas o exemplo do modelo de proxy aqui da IMO leva à semântica incorreta: u = InactiveUser.objects.all()[0]; u.active = True; u.save()e ainda isinstance(u, InactiveUser) == True. Também mencionaria que uma maneira eficaz de manter um modelo de consulta em muitos casos é com uma exibição de banco de dados.
Aryeh Leib Taurog
1
@adnanmuttaleb Isso está correto. Observe que a resposta em si só usa o termo "Modelo de Domínio". Incluí o link para DDD não porque minha resposta seja DDD, mas porque esse livro faz um ótimo trabalho em ajudá-lo a pensar em modelos de domínio.
publysher 11/03
148

Normalmente, implico uma camada de serviço entre visualizações e modelos. Isso funciona como a API do seu projeto e oferece uma boa visão de helicóptero do que está acontecendo. Eu herdei essa prática de um colega meu que usa muito essa técnica de camadas com projetos Java (JSF), por exemplo:

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

services.py

from library.models import Book

def get_books(limit=None, **filters):
    """ simple service function for retrieving books can be widely extended """
    return Book.objects.filter(**filters)[:limit]  # list[:None] will return the entire list

views.py

from library.services import get_books

class BookListView(ListView):
    """ simple view, e.g. implement a _build and _apply filters function """
    queryset = get_books()

Lembre-se, eu costumo levar modelos, visualizações e serviços ao nível do módulo e me separar ainda mais, dependendo do tamanho do projeto

Hedde van der Heide
fonte
8
Gosto da abordagem geral, embora, pelo meu entendimento, seu exemplo específico seja implementado como gerente .
Arie
9
@arie não necessariamente, talvez um melhor exemplo, para um serviço de loja online que incluem coisas como gerar sessões de carrinho, tarefas assíncronas, como cálculos de avaliações de produtos, criação e envio de e-mails etc
Hedde van der Heide
4
Também gosto dessa abordagem, vindo também do java. Eu sou novo no python. Como você testaria o views.py? Como você zombaria da camada de serviço (se, por exemplo, o serviço faz algumas chamadas remotas por API)?
Teimuraz
71

Primeiro de tudo, não se repita .

Então, tenha cuidado para não fazer o excesso de engenharia, às vezes é apenas uma perda de tempo e faz com que alguém perca o foco no que é importante. Revise o zen do python de tempos em tempos.

Dê uma olhada em projetos ativos

  • mais pessoas = mais necessidade de se organizar adequadamente
  • o repositório django eles têm uma estrutura direta.
  • No repositório pip, eles têm uma estrutura de diretórios direta.
  • o repositório de malha também é bom de se olhar.

    • você pode colocar todos os seus modelos em yourapp/models/logicalgroup.py
  • por exemplo User, Groupe modelos relacionados podem ficaryourapp/models/users.py
  • por exemplo Poll, Question,Answer ... poderia ir ao abrigoyourapp/models/polls.py
  • carregue o que você precisa __all__dentro deyourapp/models/__init__.py

Mais sobre MVC

  • modelo é seus dados
    • isso inclui seus dados reais
    • isso também inclui seus dados de sessão / cookie / cache / fs / index
  • O usuário interage com o controlador para manipular o modelo
    • pode ser uma API ou uma exibição que salva / atualiza seus dados
    • isso pode ser ajustado com request.GET/request.POST ... etc
    • pense em paginação ou filtragem também.
  • os dados atualizam a visualização
    • os modelos pegam os dados e os formatam de acordo
    • APIs, mesmo sem modelos, fazem parte da visualização; por exemplotastypie oupiston
    • isso também deve ser responsável pelo middleware.

Aproveite o middleware / templatetags

  • Se você precisar de algum trabalho para cada solicitação, o middleware é um caminho a percorrer.
    • por exemplo, adicionando carimbos de data e hora
    • por exemplo, atualizando métricas sobre hits da página
    • por exemplo, preenchendo um cache
  • Se você tiver trechos de código que sempre ocorrem para formatar objetos, as tags de modelo são boas.
    • por exemplo, migalhas de guia ativadas / url

Aproveite os gerenciadores de modelos

  • criar Userpode entrar em um UserManager(models.Manager).
  • detalhes complicados para instâncias devem continuar no models.Model.
  • detalhes sangrentos para querysetpoderiam entrar em um models.Manager.
  • convém criar Userum de cada vez, para poder pensar que ele deve permanecer no próprio modelo, mas ao criar o objeto, você provavelmente não possui todos os detalhes:

Exemplo:

class UserManager(models.Manager):
   def create_user(self, username, ...):
      # plain create
   def create_superuser(self, username, ...):
      # may set is_superuser field.
   def activate(self, username):
      # may use save() and send_mail()
   def activate_in_bulk(self, queryset):
      # may use queryset.update() instead of save()
      # may use send_mass_mail() instead of send_mail()

Faça uso de formulários sempre que possível

Um monte de código padrão pode ser eliminado se você tiver formulários mapeados para um modelo. O ModelForm documentationé muito bom. Separar o código para formulários do código do modelo pode ser bom se você tiver muita customização (ou às vezes evite erros de importação cíclicos para usos mais avançados).

Use comandos de gerenciamento quando possível

  • por exemplo yourapp/management/commands/createsuperuser.py
  • por exemplo yourapp/management/commands/activateinbulk.py

se você tiver lógica de negócios, poderá separá-la

  • django.contrib.auth usa back- end, assim como o db tem um back-end ... etc.
  • adicione um settingpara a sua lógica de negócios (por exemplo AUTHENTICATION_BACKENDS)
  • você poderia usar django.contrib.auth.backends.RemoteUserBackend
  • você poderia usar yourapp.backends.remote_api.RemoteUserBackend
  • você poderia usar yourapp.backends.memcached.RemoteUserBackend
  • delegar a lógica de negócios difícil para o back-end
  • certifique-se de definir a expectativa diretamente na entrada / saída.
  • alterar a lógica de negócios é tão simples quanto alterar uma configuração :)

exemplo de back-end:

class User(db.Models):
    def get_present_name(self): 
        # property became not deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

poderia se tornar:

class User(db.Models):
   def get_present_name(self):
      for backend in get_backends():
         try:
            return backend.get_present_name(self)
         except: # make pylint happy.
            pass
      return None

mais sobre padrões de design

mais sobre os limites da interface

  • O código que você deseja usar realmente faz parte dos modelos? ->yourapp.models
  • O código faz parte da lógica de negócios? ->yourapp.vendor
  • O código faz parte de ferramentas / libs genéricas? ->yourapp.libs
  • O código faz parte das bibliotecas de lógica de negócios? -> yourapp.libs.vendorouyourapp.vendor.libs
  • Aqui está uma boa: você pode testar seu código de forma independente?
    • sim bom :)
    • não, você pode ter um problema de interface
    • quando houver uma separação clara, o mais unittest deve ser fácil com o uso de zombaria
  • A separação é lógica?
    • sim bom :)
    • não, você pode ter problemas para testar esses conceitos lógicos separadamente.
  • Você acha que precisará refatorar ao obter código 10x mais?
    • sim, não é bom, não é bom, refatorar pode dar muito trabalho
    • não, isso é incrível!

Em suma, você poderia ter

  • yourapp/core/backends.py
  • yourapp/core/models/__init__.py
  • yourapp/core/models/users.py
  • yourapp/core/models/questions.py
  • yourapp/core/backends.py
  • yourapp/core/forms.py
  • yourapp/core/handlers.py
  • yourapp/core/management/commands/__init__.py
  • yourapp/core/management/commands/closepolls.py
  • yourapp/core/management/commands/removeduplicates.py
  • yourapp/core/middleware.py
  • yourapp/core/signals.py
  • yourapp/core/templatetags/__init__.py
  • yourapp/core/templatetags/polls_extras.py
  • yourapp/core/views/__init__.py
  • yourapp/core/views/users.py
  • yourapp/core/views/questions.py
  • yourapp/core/signals.py
  • yourapp/lib/utils.py
  • yourapp/lib/textanalysis.py
  • yourapp/lib/ratings.py
  • yourapp/vendor/backends.py
  • yourapp/vendor/morebusinesslogic.py
  • yourapp/vendor/handlers.py
  • yourapp/vendor/middleware.py
  • yourapp/vendor/signals.py
  • yourapp/tests/test_polls.py
  • yourapp/tests/test_questions.py
  • yourapp/tests/test_duplicates.py
  • yourapp/tests/test_ratings.py

ou qualquer outra coisa que ajude você; encontrar as interfaces necessárias e os limites o ajudarão.

dnozay
fonte
27

O Django emprega um tipo ligeiramente modificado de MVC. Não há conceito de "controlador" no Django. O proxy mais próximo é uma "view", que tende a causar confusão com os convertidos do MVC, porque no MVC uma view é mais parecida com o "template" do Django.

No Django, um "modelo" não é meramente uma abstração de banco de dados. Em alguns aspectos, ele compartilha o dever com a "visão" do Django como o controlador do MVC. Ele contém todo o comportamento associado a uma instância. Se essa instância precisar interagir com uma API externa como parte de seu comportamento, ainda será o código do modelo. De fato, não é necessário que os modelos interajam com o banco de dados; portanto, é possível conceber modelos que existam inteiramente como uma camada interativa para uma API externa. É um conceito muito mais gratuito de "modelo".

Chris Pratt
fonte
7

No Django, a estrutura MVC é como Chris Pratt disse, diferente do modelo MVC clássico usado em outras estruturas, acho que a principal razão para fazer isso é evitar uma estrutura de aplicação muito rigorosa, como acontece em outras estruturas MVC como CakePHP.

No Django, o MVC foi implementado da seguinte maneira:

A camada de visualização é dividida em duas. As visualizações devem ser usadas apenas para gerenciar solicitações HTTP, elas são chamadas e respondem a elas. As visualizações se comunicam com o restante do seu aplicativo (formulários, modelos, classes personalizadas, em casos simples, diretamente com os modelos). Para criar a interface, usamos modelos. Os modelos são do tipo string para o Django, ele mapeia um contexto para eles, e esse contexto foi comunicado à view pelo aplicativo (quando a view pede).

A camada de modelo fornece encapsulamento, abstração, validação, inteligência e torna seus dados orientados a objetos (eles dizem que um dia o DBMS também fará). Isso não significa que você deve criar arquivos models.py enormes (na verdade, um conselho muito bom é dividir seus modelos em arquivos diferentes, colocá-los em uma pasta chamada 'models', criar um arquivo '__init__.py' neste pasta onde você importa todos os seus modelos e, finalmente, usa o atributo 'app_label' da classe models.Model). O modelo deve abstraí-lo da operação com dados, isso simplificará seu aplicativo. Você também deve, se necessário, criar classes externas, como "ferramentas" para seus modelos. Você também pode usar a herança nos modelos, configurando o atributo 'abstract' da classe Meta do seu modelo para 'True'.

Onde está o resto? Bem, pequenas aplicações web geralmente são uma espécie de interface para dados; em alguns casos de programas pequenos, usar visualizações para consultar ou inserir dados seria suficiente. Casos mais comuns usarão Forms ou ModelForms, que na verdade são "controladores". Isso não é senão uma solução prática para um problema comum e muito rápido. É o que um site costuma fazer.

Se o Forms não é adequado para você, você deve criar suas próprias classes para fazer a mágica, um exemplo muito bom disso é o aplicativo admin: você pode ler o código ModelAmin, na verdade ele funciona como um controlador. Não existe uma estrutura padrão, sugiro que você examine os aplicativos Django existentes, isso depende de cada caso. É isso que os desenvolvedores do Django pretendem, você pode adicionar a classe analisador xml, uma classe de conector API, adicionar o Celery para executar tarefas, torcer para um aplicativo baseado em reator, usar apenas o ORM, criar apenas um ORM, criar um serviço da Web, modificar o aplicativo de administração e muito mais. .. É sua responsabilidade criar código de boa qualidade, respeitar a filosofia MVC ou não, torná-lo baseado em módulo e criar suas próprias camadas de abstração. É muito flexível.

Meu conselho: leia o máximo de código possível, existem muitos aplicativos de django por aí, mas não os leve tão a sério. Cada caso é diferente, os padrões e a teoria ajudam, mas nem sempre, isso é uma ciência imprecisa, o django apenas fornece boas ferramentas que você pode usar para aliviar algumas dores (como interface de administrador, validação de formulário da web, i18n, implementação de padrões de observadores, tudo o mencionado anteriormente e outros), mas os bons designs vêm de designers experientes.

PS .: use a classe 'Usuário' do aplicativo auth (do django padrão), você pode criar, por exemplo, perfis de usuário ou, pelo menos, ler seu código, será útil para o seu caso.

Nate Gentile
fonte
1

Uma pergunta antiga, mas eu gostaria de oferecer minha solução de qualquer maneira. É baseado na aceitação de que os objetos de modelo também exigem alguma funcionalidade adicional, enquanto é difícil colocá-lo dentro de models.py . Lógica comercial pesada pode ser escrita separadamente, dependendo do gosto pessoal, mas eu pelo menos gosto do modelo para fazer tudo relacionado a si mesmo. Essa solução também suporta aqueles que gostam de ter toda a lógica colocada nos próprios modelos.

Como tal, criei um hack que me permite separar a lógica das definições de modelo e ainda obter todas as dicas do meu IDE.

As vantagens devem ser óbvias, mas isso lista algumas que observei:

  • As definições de banco de dados permanecem exatamente isso - nenhum "lixo" lógico anexado
  • A lógica relacionada ao modelo é toda organizada em um só lugar
  • Todos os serviços (formulários, REST, visualizações) têm um único ponto de acesso à lógica
  • O melhor de tudo: não precisei reescrever nenhum código depois que percebi que meu models.py ficou muito confuso e precisava separar a lógica. A separação é suave e iterativa: eu poderia fazer uma função por vez ou por uma classe inteira ou por todo o models.py.

Eu tenho usado isso com Python 3.4 e superior e Django 1.8 e superior.

app / models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

app / logic / user.py

if False:
    # This allows the IDE to know about the User model and its member fields
    from main.models import User

class UserLogic(object):
    def logic_function(self: 'User'):
        ... code with hinting working normally ...

A única coisa que não consigo descobrir é como fazer meu IDE (PyCharm neste caso) reconhecer que o UserLogic é na verdade um modelo de usuário. Mas, como isso é obviamente um truque, fico feliz em aceitar o pequeno incômodo de sempre especificar o tipo de selfparâmetro.

velis
fonte
Na verdade, eu vejo isso como uma abordagem fácil de usar. Mas eu mudaria o modelo final para outro arquivo e não herdaria o models.py. Seria como service.py foram colidir userlogic modelo +
Maks
1

Eu teria que concordar com você. Existem muitas possibilidades no django, mas o melhor lugar para começar é revisar a filosofia de design do Django .

  1. Chamar uma API a partir de uma propriedade de modelo não seria o ideal, parece que faria mais sentido fazer algo assim na visualização e possivelmente criar uma camada de serviço para manter as coisas secas. Se a chamada para a API for sem bloqueio e a chamada for cara, o envio da solicitação a um trabalhador de serviço (um trabalhador que consome de uma fila) pode fazer sentido.

  2. De acordo com a filosofia de design do Django, os modelos englobam todos os aspectos de um "objeto". Portanto, toda a lógica de negócios relacionada a esse objeto deve residir lá:

Incluir toda lógica de domínio relevante

Os modelos devem encapsular todos os aspectos de um "objeto", seguindo o padrão de design do Active Record de Martin Fowler.

  1. Os efeitos colaterais que você descreve são aparentes, a lógica aqui poderia ser melhor dividida em Querysets e gerentes. Aqui está um exemplo:

    models.py

    import datetime
    
    from djongo import models
    from django.db.models.query import QuerySet
    from django.contrib import admin
    from django.db import transaction
    
    
    class MyUser(models.Model):
    
        present_name = models.TextField(null=False, blank=True)
        status = models.TextField(null=False, blank=True)
        last_active = models.DateTimeField(auto_now=True, editable=False)
    
        # As mentioned you could put this in a template tag to pull it
        # from cache there. Depending on how it is used, it could be
        # retrieved from within the admin view or from a custom view
        # if that is the only place you will use it.
        #def get_present_name(self):
        #    # property became non-deterministic in terms of database
        #    # data is taken from another service by api
        #    return remote_api.request_user_name(self.uid) or 'Anonymous'
    
        # Moved to admin as an action
        # def activate(self):
        #     # method now has a side effect (send message to user)
        #     self.status = 'activated'
        #     self.save()
        #     # send email via email service
        #     #send_mail('Your account is activated!', '…', [self.email])
    
        class Meta:
            ordering = ['-id']  # Needed for DRF pagination
    
        def __unicode__(self):
            return '{}'.format(self.pk)
    
    
    class MyUserRegistrationQuerySet(QuerySet):
    
        def for_inactive_users(self):
            new_date = datetime.datetime.now() - datetime.timedelta(days=3*365)  # 3 Years ago
            return self.filter(last_active__lte=new_date.year)
    
        def by_user_id(self, user_ids):
            return self.filter(id__in=user_ids)
    
    
    class MyUserRegistrationManager(models.Manager):
    
        def get_query_set(self):
            return MyUserRegistrationQuerySet(self.model, using=self._db)
    
        def with_no_activity(self):
            return self.get_query_set().for_inactive_users()

    admin.py

    # Then in model admin
    
    class MyUserRegistrationAdmin(admin.ModelAdmin):
        actions = (
            'send_welcome_emails',
        )
    
        def send_activate_emails(self, request, queryset):
            rows_affected = 0
            for obj in queryset:
                with transaction.commit_on_success():
                    # send_email('welcome_email', request, obj) # send email via email service
                    obj.status = 'activated'
                    obj.save()
                    rows_affected += 1
    
            self.message_user(request, 'sent %d' % rows_affected)
    
    admin.site.register(MyUser, MyUserRegistrationAdmin)
Radtek
fonte
0

Concordo principalmente com a resposta escolhida ( https://stackoverflow.com/a/12857584/871392 ), mas quero adicionar a opção na seção Fazendo consultas.

Pode-se definir classes QuerySet para modelos para fazer consultas de filtro e assim por diante. Depois disso, você pode fazer proxy dessa classe de conjunto de consultas para o gerente do modelo, como fazem as classes Manager e QuerySet integradas.

Embora, se você tivesse que consultar vários modelos de dados para obter um modelo de domínio, me parecesse mais razoável colocar isso em um módulo separado, como sugerido anteriormente.

l0ki
fonte
0

Artigo mais abrangente sobre as diferentes opções com prós e contras:

  1. Idéia # 1: Modelos gordos
  2. Idéia # 2: Colocando lógica de negócios em modos de exibição / formulários
  3. Idéia # 3: Serviços
  4. Idéia # 4: QuerySets / Managers
  5. Conclusão

Fonte: https://sunscrapers.com/blog/where-to-put-business-logic-django/

FSE
fonte
Você deve adicionar alguma explicação.
m02ph3u5
-6

O Django foi projetado para ser facilmente usado para entregar páginas da web. Se você não estiver confortável com isso, talvez use outra solução.

Estou escrevendo as operações raiz ou comuns no modelo (para ter a mesma interface) e as outras no controlador do modelo. Se eu precisar de uma operação de outro modelo, importo seu controlador.

Essa abordagem é suficiente para mim e para a complexidade dos meus aplicativos.

A resposta de Hedde é um exemplo que mostra a flexibilidade do django e do próprio python.

Pergunta muito interessante de qualquer maneira!

pvilas
fonte
9
Como é dizer que isso é bom o suficiente para você ser útil na minha compreensão da pergunta?
22412 Chris Wesseling
1
O Django tem muito mais a oferecer além do django.db.models, mas a maior parte do ecossistema depende muito do seu modelo usando os modelos do django.
andho
1
O padrão de design usado para o desenvolvimento de software. E o django foi projetado para ser facilmente usado para fornecer software em médio ou grande escala, não apenas em páginas da web!
Mohammad Torkashvand