Séparation de la logique métier et de l'accès aux données dans Django

485

J'écris un projet dans Django et je vois que 80% du code est dans le fichier models.py. Ce code prête à confusion et, après un certain temps, je cesse de comprendre ce qui se passe réellement.

Voici ce qui me dérange:

  1. Je trouve moche que mon niveau de modèle (qui était censé être uniquement responsable du travail avec les données d'une base de données) envoie également des e-mails, marche sur l'API vers d'autres services, etc.
  2. De plus, je trouve inacceptable de placer la logique métier dans la vue, car de cette façon, il devient difficile à contrôler. Par exemple, dans mon application, il existe au moins trois façons de créer de nouvelles instances de User, mais techniquement, il doit les créer de manière uniforme.
  3. Je ne remarque pas toujours quand les méthodes et propriétés de mes modèles deviennent non déterministes et quand elles développent des effets secondaires.

Voici un exemple simple. Au début, le Usermodèle était comme ceci:

class User(db.Models):

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

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

Au fil du temps, cela s'est transformé en ceci:

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])

Ce que je veux, c'est séparer les entités dans mon code:

  1. Entités de ma base de données, niveau base de données: que contient mon application?
  2. Entités de mon application, niveau logique métier: que peut faire mon application?

Quelles sont les bonnes pratiques pour mettre en œuvre une telle approche qui peut être appliquée dans Django?

defuz
la source
14
En savoir plus sur les signaux
Konstant
1
vous avez bien supprimé la balise mais vous pouvez utiliser DCI pour effectuer la séparation de ce que fait le système (la fonctionnalité) et de ce qu'est le système (le modèle de données / domaine)
Rune FS
2
Vous proposez de mettre en œuvre toute la logique métier dans les rappels de signaux? Malheureusement, toutes mes applications ne peuvent pas être liées à des événements dans la base de données.
Defuz
Rune FS, j'ai essayé d'utiliser le DCI, mais il m'a semblé qu'il n'en fallait pas beaucoup pour mon projet: Contexte, définition des rôles comme mixage aux objets, etc. Il y a un moyen plus simple de séparation "fait" et " est"? Pourriez-vous donner un exemple minimal?
Defuz

Réponses:

635

Il semble que vous posiez des questions sur la différence entre le modèle de données et le modèle de domaine - ce dernier est l'endroit où vous pouvez trouver la logique métier et les entités telles que perçues par votre utilisateur final, le premier est l'endroit où vous stockez réellement vos données.

De plus, j'ai interprété la troisième partie de votre question comme: comment remarquer l'échec de garder ces modèles séparés.

Ce sont deux concepts très différents et il est toujours difficile de les séparer. Cependant, certains modèles et outils communs peuvent être utilisés à cette fin.

À propos du modèle de domaine

La première chose que vous devez reconnaître est que votre modèle de domaine ne concerne pas vraiment les données; il s'agit d' actions et de questions telles que "activer cet utilisateur", "désactiver cet utilisateur", "quels utilisateurs sont actuellement activés?" et "quel est le nom de cet utilisateur?". En termes classiques: il s'agit de requêtes et de commandes .

Penser en commandes

Commençons par regarder les commandes de votre exemple: "activer cet utilisateur" et "désactiver cet utilisateur". La bonne chose à propos des commandes est qu'elles peuvent facilement être exprimées par de petits scénarios donnés quand ils sont alors:

étant donné un utilisateur inactif
lorsque l'administrateur active cet utilisateur
alors l'utilisateur devient actif
et un e-mail de confirmation est envoyé à l'utilisateur
et une entrée est ajoutée dans le journal du système
(etc. , etc.)

De tels scénarios sont utiles pour voir comment différentes parties de votre infrastructure peuvent être affectées par une seule commande - dans ce cas, votre base de données (une sorte d'indicateur `` actif ''), votre serveur de messagerie, votre journal système, etc.

De tels scénarios vous aident également à configurer un environnement de développement piloté par les tests.

Et enfin, la réflexion dans les commandes vous aide vraiment à créer une application orientée tâche. Vos utilisateurs l'apprécieront :-)

Exprimer des commandes

Django propose deux façons simples d'exprimer des commandes; ce sont deux options valables et il n'est pas rare de mélanger les deux approches.

La couche service

Le module de service a déjà été décrit par @Hedde . Ici, vous définissez un module séparé et chaque commande est représentée comme une fonction.

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

Utilisation de formulaires

L'autre façon est d'utiliser un formulaire Django pour chaque commande. Je préfère cette approche, car elle combine plusieurs aspects étroitement liés:

  • exécution de la commande (que fait-elle?)
  • validation des paramètres de commande (est-ce possible?)
  • présentation de la commande (comment faire?)

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

Penser dans les requêtes

Votre exemple ne contenait aucune requête, j'ai donc pris la liberté de créer quelques requêtes utiles. Je préfère utiliser le terme «question», mais les requêtes sont la terminologie classique. Les requêtes intéressantes sont: "Quel est le nom de cet utilisateur?", "Cet utilisateur peut-il se connecter?", "Me montrer une liste d'utilisateurs désactivés" et "Quelle est la répartition géographique des utilisateurs désactivés?"

Avant de vous lancer dans la réponse à ces requêtes, vous devez toujours vous poser deux questions: s'agit-il d'une requête de présentation uniquement pour mes modèles, et / ou d'une requête de logique métier liée à l'exécution de mes commandes, et / ou d'une requête de rapport .

Les requêtes de présentation sont simplement faites pour améliorer l'interface utilisateur. Les réponses aux requêtes de logique métier affectent directement l'exécution de vos commandes. Les requêtes de rapport sont uniquement à des fins analytiques et ont des contraintes de temps plus lâches. Ces catégories ne s'excluent pas mutuellement.

L'autre question est: "ai-je un contrôle total sur les réponses?" Par exemple, lorsque nous demandons le nom de l'utilisateur (dans ce contexte), nous n'avons aucun contrôle sur le résultat, car nous nous appuyons sur une API externe.

Faire des requêtes

La requête la plus élémentaire dans Django est l'utilisation de l'objet Manager:

User.objects.filter(active=True)

Bien sûr, cela ne fonctionne que si les données sont réellement représentées dans votre modèle de données. Ce n'est pas toujours le cas. Dans ces cas, vous pouvez considérer les options ci-dessous.

Balises et filtres personnalisés

La première alternative est utile pour les requêtes qui sont simplement de présentation: balises personnalisées et filtres de modèle.

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éthodes de requête

Si votre requête n'est pas simplement de présentation, vous pouvez ajouter des requêtes à votre services.py (si vous l'utilisez), ou introduire un module 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 

Modèles de proxy

Les modèles de proxy sont très utiles dans le contexte de la logique métier et du reporting. Vous définissez essentiellement un sous-ensemble amélioré de votre modèle. Vous pouvez remplacer le QuerySet de base d'un gestionnaire en remplaçant la Manager.get_queryset()méthode.

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

Modèles de requête

Pour les requêtes qui sont intrinsèquement complexes, mais qui sont exécutées assez souvent, il existe la possibilité de modèles de requête. Un modèle de requête est une forme de dénormalisation dans laquelle les données pertinentes pour une seule requête sont stockées dans un modèle distinct. L'astuce est bien sûr de synchroniser le modèle dénormalisé avec le modèle principal. Les modèles de requête ne peuvent être utilisés que si les modifications sont entièrement sous votre contrôle.

models.py

class InactiveUserDistribution(models.Model):
    country = CharField(max_length=200)
    inactive_user_count = IntegerField(default=0)

La première option consiste à mettre à jour ces modèles dans vos commandes. Ceci est très utile si ces modèles ne sont modifiés que par une ou deux commandes.

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()

Une meilleure option serait d'utiliser des signaux personnalisés. Ces signaux sont bien entendu émis par vos commandes. Les signaux ont l'avantage de pouvoir synchroniser plusieurs modèles de requête avec votre modèle d'origine. De plus, le traitement du signal peut être déchargé sur des tâches d'arrière-plan, en utilisant Celery ou des cadres similaires.

signaux.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()

Le garder propre

Lorsque vous utilisez cette approche, il devient ridiculement facile de déterminer si votre code reste propre. Suivez simplement ces directives:

  • Mon modèle contient-il des méthodes qui font plus que gérer l'état de la base de données? Vous devez extraire une commande.
  • Mon modèle contient-il des propriétés qui ne correspondent pas aux champs de la base de données? Vous devez extraire une requête.
  • Mon modèle fait-il référence à une infrastructure qui n'est pas ma base de données (comme le courrier)? Vous devez extraire une commande.

Il en va de même pour les vues (car les vues souffrent souvent du même problème).

  • Ma vue gère-t-elle activement les modèles de base de données? Vous devez extraire une commande.

Quelques références

Documentation Django: modèles proxy

Documentation Django: signaux

Architecture: conception pilotée par domaine

éditeur
la source
11
C'est agréable de voir une réponse incorporant DDD dans une question liée à django. Ce n'est pas parce que Django utilise ActiveRecord pour la persistance que la séparation des préoccupations doit disparaître. Très bonne réponse.
Scott Coates
6
Si je veux valider que l'utilisateur loogé est le propriétaire d'un objet avant de supprimer cet objet, dois-je le vérifier dans la vue ou dans le module formulaire / service?
Ivan
6
@Ivan: les deux. Il doit être dans le module formulaire / service car il fait partie de vos contraintes métier. Il doit également apparaître dans la vue, car vous ne devez présenter que les actions que les utilisateurs peuvent réellement exécuter.
éditeur
4
Gestionnaire personnalisé méthodes sont une bonne façon de mettre en œuvre des requêtes: User.objects.inactive_users(). Mais l'exemple de modèle proxy ici IMO conduit à une sémantique incorrecte: u = InactiveUser.objects.all()[0]; u.active = True; u.save()et pourtant isinstance(u, InactiveUser) == True. Je mentionnerais également un moyen efficace de maintenir un modèle de requête dans de nombreux cas avec une vue db.
Aryeh Leib Taurog
1
@adnanmuttaleb C'est correct. Notez que la réponse elle-même utilise uniquement le terme "modèle de domaine". J'ai inclus le lien vers DDD non pas parce que ma réponse est DDD, mais parce que ce livre fait un excellent travail pour vous aider à réfléchir aux modèles de domaine.
éditeur
148

J'implémente généralement une couche de service entre les vues et les modèles. Cela agit comme l'API de votre projet et vous donne une bonne vue d'hélicoptère de ce qui se passe. J'ai hérité de cette pratique d'un de mes collègues qui utilise beaucoup cette technique de superposition avec les projets Java (JSF), par exemple:

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()

Attention, je prends généralement des modèles, des vues et des services au niveau du module et je les sépare encore plus en fonction de la taille du projet

Hedde van der Heide
la source
8
J'aime l'approche générale, mais d'après ma compréhension, votre exemple spécifique serait généralement mis en œuvre en tant que gestionnaire .
arie
9
@arie pas nécessairement, peut-être un meilleur exemple, pour une boutique en ligne, les services incluraient des choses comme la génération de sessions de panier, des tâches asynchrones comme les calculs de notes de produit, la création et l'envoi d'e-mails, etc.
Hedde van der Heide
4
J'aime aussi cette approche, venant aussi de Java. Je suis nouveau sur python, comment testeriez-vous views.py? Comment feriez-vous pour se moquer de la couche de service (si, par exemple, le service effectue des appels API distants)?
Teimuraz
71

Tout d'abord, ne vous répétez pas .

Ensuite, veuillez faire attention à ne pas trop concevoir, parfois c'est juste une perte de temps et cela fait perdre à quelqu'un l'attention sur ce qui est important. Passez en revue le zen du python de temps en temps.

Jetez un œil aux projets actifs

  • plus de gens = plus besoin de s'organiser correctement
  • le référentiel django ils ont une structure simple.
  • le référentiel pip, ils ont une structure de répertoires straigtforward.
  • le référentiel de matrice est également une bonne chose à regarder.

    • vous pouvez placer tous vos modèles sous yourapp/models/logicalgroup.py
  • par exemple User, Groupet les modèles connexes peuvent passer sousyourapp/models/users.py
  • par exemple Poll, Question, Answer... pourrait passer sousyourapp/models/polls.py
  • charger ce dont vous avez besoin à l' __all__intérieuryourapp/models/__init__.py

En savoir plus sur MVC

  • le modèle est vos données
    • cela inclut vos données réelles
    • cela inclut également vos données de session / cookie / cache / fs / index
  • l'utilisateur interagit avec le contrôleur pour manipuler le modèle
    • il peut s'agir d'une API ou d'une vue qui enregistre / met à jour vos données
    • cela peut être réglé avec request.GET/ request.POST... etc
    • pensez aussi à la pagination ou au filtrage .
  • les données mettent à jour la vue
    • les modèles prennent les données et les mettent en forme en conséquence
    • Les API, même sans modèles, font partie de la vue; par exempletastypie oupiston
    • cela devrait également tenir compte du middleware.

Profitez des middleware / templatetags

  • Si vous avez besoin de travail pour chaque demande, le middleware est une solution.
    • par exemple en ajoutant des horodatages
    • par exemple la mise à jour des métriques sur les hits
    • ex. remplissage d'un cache
  • Si vous avez des extraits de code qui se reproduisent toujours pour les objets de mise en forme, les balises de modèle sont bonnes.
    • ex. onglet actif / fil d'Ariane

Profitez des gestionnaires de modèles

  • la création Userpeut aller dans un UserManager(models.Manager).
  • les détails sanglants des instances devraient être affichés sur le models.Model.
  • détails sanglants pour queryset pourrait aller dans un models.Manager.
  • vous voudrez peut-être créer un User à la fois, vous pouvez donc penser qu'il devrait vivre sur le modèle lui-même, mais lors de la création de l'objet, vous n'avez probablement pas tous les détails:

Exemple:

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()

Utilisez des formulaires dans la mesure du possible

Un grand nombre de codes passe-partout peuvent être éliminés si vous avez des formulaires qui correspondent à un modèle. C'est ModelForm documentationassez bon. La séparation du code des formulaires du code du modèle peut être utile si vous avez beaucoup de personnalisation (ou évitez parfois les erreurs d'importation cycliques pour des utilisations plus avancées).

Utilisez les commandes de gestion lorsque cela est possible

  • par exemple yourapp/management/commands/createsuperuser.py
  • par exemple yourapp/management/commands/activateinbulk.py

si vous avez une logique métier, vous pouvez la séparer

  • django.contrib.auth utilise des backends , tout comme db a un backend ... etc.
  • ajoutez un settingpour votre logique métier (par exempleAUTHENTICATION_BACKENDS )
  • tu pourrais utiliser django.contrib.auth.backends.RemoteUserBackend
  • tu pourrais utiliser yourapp.backends.remote_api.RemoteUserBackend
  • tu pourrais utiliser yourapp.backends.memcached.RemoteUserBackend
  • déléguer la logique métier difficile au backend
  • assurez-vous de définir l'attente directement sur l'entrée / la sortie.
  • changer la logique métier est aussi simple que changer un paramètre :)

exemple de backend:

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' 

pourrait devenir:

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

en savoir plus sur les modèles de conception

en savoir plus sur les limites de l'interface

  • Le code que vous souhaitez utiliser fait-il vraiment partie des modèles? ->yourapp.models
  • Le code fait-il partie de la logique métier? ->yourapp.vendor
  • Le code fait-il partie des outils / bibliothèques génériques? ->yourapp.libs
  • Le code fait-il partie des bibliothèques de logique métier? -> yourapp.libs.vendorouyourapp.vendor.libs
  • En voici une bonne: pouvez-vous tester votre code indépendamment?
    • Oui bien :)
    • non, vous pouvez avoir un problème d'interface
    • quand il y a une séparation claire, le plus uniforme devrait être un jeu d'enfant avec l'utilisation de moquerie
  • La séparation est-elle logique?
    • Oui bien :)
    • non, vous pouvez avoir du mal à tester ces concepts logiques séparément.
  • Pensez-vous que vous devrez refactoriser lorsque vous obtenez 10 fois plus de code?
    • oui, pas bon, pas de bueno, refactor pourrait être beaucoup de travail
    • non, c'est juste génial!

Bref, vous auriez pu

  • 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 toute autre chose qui vous aide; trouver les interfaces dont vous avez besoin et les limites vous aidera.

dnozay
la source
27

Django utilise un type de MVC légèrement modifié. Il n'y a pas de concept de "contrôleur" dans Django. Le proxy le plus proche est une "vue", ce qui a tendance à créer de la confusion avec les convertis MVC car dans MVC une vue ressemble plus au "modèle" de Django.

Dans Django, un "modèle" n'est pas simplement une abstraction de base de données. À certains égards, il partage son devoir avec la «vision» de Django en tant que contrôleur de MVC. Il contient l'intégralité du comportement associé à une instance. Si cette instance doit interagir avec une API externe dans le cadre de son comportement, c'est toujours du code de modèle. En fait, les modèles ne sont pas du tout tenus d'interagir avec la base de données, vous pouvez donc concevoir des modèles qui existent entièrement en tant que couche interactive pour une API externe. C'est un concept beaucoup plus libre d'un "modèle".

Chris Pratt
la source
7

À Django, la structure MVC est, comme l'a dit Chris Pratt, différente du modèle MVC classique utilisé dans d'autres cadres, je pense que la principale raison pour cela est d'éviter une structure d'application trop stricte, comme cela se produit dans d'autres cadres MVC comme CakePHP.

Dans Django, MVC a été implémenté de la manière suivante:

Le calque de vue est divisé en deux. Les vues ne doivent être utilisées que pour gérer les requêtes HTTP, elles sont appelées et y répondent. Les vues communiquent avec le reste de votre application (formulaires, formulaires, classes personnalisées, ou dans des cas simples directement avec les modèles). Pour créer l'interface, nous utilisons des modèles. Les modèles sont de type chaîne pour Django, il y mappe un contexte, et ce contexte a été communiqué à la vue par l'application (lorsque la vue le demande).

La couche modèle donne l'encapsulation, l'abstraction, la validation, l'intelligence et rend vos données orientées objet (ils disent qu'un jour le SGBD le fera également). Cela ne signifie pas que vous devez créer d'énormes fichiers models.py (en fait, un très bon conseil est de diviser vos modèles en différents fichiers, de les placer dans un dossier appelé 'models', de créer un fichier '__init__.py' dans ce dossier dans lequel vous importez tous vos modèles et enfin utilisez l'attribut 'app_label' de models.Model class). Le modèle devrait vous empêcher d'utiliser des données, il simplifiera votre application. Vous devez également, si nécessaire, créer des classes externes, comme des «outils» pour vos modèles. Vous pouvez également utiliser l'héritage dans les modèles, en définissant l'attribut «abstrait» de la méta-classe de votre modèle sur «True».

Où est le reste? Eh bien, les petites applications Web sont généralement une sorte d'interface avec les données, dans certains petits cas de programme, utiliser des vues pour interroger ou insérer des données serait suffisant. Les cas les plus courants utiliseront des formulaires ou des formulaires, qui sont en fait des "contrôleurs". Ce n'est autre qu'une solution pratique à un problème commun, et très rapide. C'est ce que fait un site Web.

Si les formulaires ne vous conviennent pas, vous devez créer vos propres classes pour faire la magie, un très bon exemple de ceci est l'application d'administration: vous pouvez lire le code ModelAmin, cela fonctionne en fait comme un contrôleur. Il n'y a pas de structure standard, je vous suggère d'examiner les applications Django existantes, cela dépend de chaque cas. C'est ce que les développeurs Django voulaient, vous pouvez ajouter une classe d'analyseur XML, une classe de connecteur API, ajouter Celery pour effectuer des tâches, tordu pour une application basée sur un réacteur, utiliser uniquement l'ORM, créer un service Web, modifier l'application d'administration et plus encore. .. Il est de votre responsabilité de créer du code de bonne qualité, de respecter ou non la philosophie MVC, de le faire basé sur des modules et de créer vos propres couches d'abstraction. C'est très flexible.

Mon conseil: lisez autant de code que possible, il y a beaucoup d'applications django, mais ne les prenez pas si au sérieux. Chaque cas est différent, les modèles et la théorie sont utiles, mais pas toujours, c'est une science imprécise, Django vous fournit juste de bons outils que vous pouvez utiliser pour atténuer certaines douleurs (comme l'interface d'administration, la validation de formulaire Web, i18n, la mise en œuvre de modèles d'observateurs, tous) les précédents et d'autres), mais les bons designs proviennent de designers expérimentés.

PS: utilisez la classe 'User' de l'application auth (du django standard), vous pouvez faire par exemple des profils utilisateurs, ou au moins lire son code, cela sera utile pour votre cas.

Nate Gentile
la source
1

Une vieille question, mais j'aimerais quand même proposer ma solution. Il est basé sur l'acceptation du fait que les objets de modèle nécessitent également des fonctionnalités supplémentaires alors qu'il est difficile de les placer dans models.py . La logique métier lourde peut être écrite séparément selon les goûts personnels, mais j'aime au moins le modèle pour faire tout ce qui concerne lui-même. Cette solution prend également en charge ceux qui aiment que toute la logique soit placée dans les modèles eux-mêmes.

En tant que tel, j'ai conçu un hack qui me permet de séparer la logique des définitions de modèle et d'obtenir toujours toutes les indications de mon IDE.

Les avantages devraient être évidents, mais cela en énumère quelques-uns que j'ai observés:

  • Les définitions de base de données restent telles quelles - aucune logique "poubelle" attachée
  • La logique liée au modèle est parfaitement placée au même endroit
  • Tous les services (formulaires, REST, vues) ont un seul point d'accès à la logique
  • Mieux encore: je n'ai pas eu à réécrire de code une fois que j'ai réalisé que mon models.py était devenu trop encombré et devait séparer la logique. La séparation est fluide et itérative: je pourrais faire une fonction à la fois ou toute la classe ou l'ensemble des models.py.

J'utilise ceci avec Python 3.4 et supérieur et Django 1.8 et supérieur.

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 ...

La seule chose que je ne peux pas comprendre, c'est comment faire en sorte que mon IDE (PyCharm dans ce cas) reconnaisse que UserLogic est en fait un modèle utilisateur. Mais comme il s'agit évidemment d'un hack, je suis assez heureux d'accepter la petite nuisance de toujours spécifier le type de selfparamètre.

velis
la source
En fait, je le vois comme une approche facile à utiliser. Mais je déplacerais le modèle final dans un autre fichier et n'hériterais pas de models.py. Ce serait comme service.py étaient clash userlogic + model
Maks
1

Je devrais être d'accord avec vous. Il y a beaucoup de possibilités dans django mais le meilleur endroit pour commencer est de revoir la philosophie de conception de Django .

  1. L'appel d'une API à partir d'une propriété de modèle ne serait pas idéal, il semble qu'il serait plus logique de faire quelque chose comme ça dans la vue et éventuellement de créer une couche de service pour garder les choses au sec. Si l'appel à l'API n'est pas bloquant et que l'appel est coûteux, l'envoi de la demande à un technicien de service (un technicien qui consomme à partir d'une file d'attente) peut avoir un sens.

  2. Selon la philosophie de conception de Django, les modèles englobent tous les aspects d'un "objet". Donc, toute logique métier liée à cet objet devrait y vivre:

Inclure toute la logique de domaine pertinente

Les modèles doivent encapsuler chaque aspect d'un «objet», en suivant le modèle de conception Active Record de Martin Fowler.

  1. Les effets secondaires que vous décrivez sont évidents, la logique ici pourrait être mieux décomposée en ensembles de requêtes et gestionnaires. Voici un exemple:

    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
la source
0

Je suis principalement d'accord avec la réponse choisie ( https://stackoverflow.com/a/12857584/871392 ), mais je veux ajouter une option dans la section Faire des requêtes.

On peut définir des classes QuerySet pour les modèles pour effectuer des requêtes de filtre et ainsi de suite. Après cela, vous pouvez proxy cette classe d'ensemble de requêtes pour le gestionnaire de modèle, comme le font les classes Manager et QuerySet intégrées.

Bien que, si vous deviez interroger plusieurs modèles de données pour obtenir un modèle de domaine, il me semble plus raisonnable de le mettre dans un module séparé comme suggéré précédemment.

l0ki
la source
0

Article le plus complet sur les différentes options avec des avantages et des inconvénients:

  1. Idée n ° 1: gros modèles
  2. Idée n ° 2: mettre la logique métier dans les vues / formulaires
  3. Idée n ° 3: les services
  4. Idée n ° 4: QuerySets / Managers
  5. Conclusion

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

FSE
la source
Vous devez ajouter quelques explications.
m02ph3u5
-6

Django est conçu pour être facilement utilisé pour diffuser des pages Web. Si vous n'êtes pas à l'aise avec cela, vous devriez peut-être utiliser une autre solution.

J'écris la racine ou les opérations courantes sur le modèle (pour avoir la même interface) et les autres sur le contrôleur du modèle. Si j'ai besoin d'une opération d'un autre modèle, j'importe son contrôleur.

Cette approche me suffit et la complexité de mes applications.

La réponse de Hedde est un exemple qui montre la flexibilité de django et de python lui-même.

Question très intéressante quand même!

pvilas
la source
9
Comment le fait de dire que c'est assez bon pour vous est-il utile à ma compréhension de la question?
Chris Wesseling du
1
Django a beaucoup plus à offrir en dehors de django.db.models, mais la plupart de l'écosystème dépend fortement de votre modèle utilisant les modèles django.
andho
1
Le modèle de conception utilisé pour développer des logiciels. Et django conçu pour être facilement utilisé pour fournir des logiciels à moyenne ou grande échelle, pas seulement des pages Web!
Mohammad Torkashvand