J'écris un projet dans Django et je constate que 80% du code est dans le fichier models.py
. Ce code est déroutant et, après un certain temps, je ne comprends plus ce qui se passe réellement.
Voici ce qui me dérange:
User
, mais techniquement, il convient de les créer de manière uniforme.Voici un exemple simple. Au début, le modèle User
ressemblait à 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:
Quelles sont les bonnes pratiques pour mettre en œuvre une telle approche pouvant être appliquée dans Django?
Il semble que vous vous interrogiez 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 l'utilisateur final. , le premier est celui où vous stockez réellement vos données.
De plus, j'ai interprété la troisième partie de votre question comme suit: comment remarquer un échec pour 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 courants peuvent être utilisés à cette fin.
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 de actions et 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 commandes.
Commençons par examiner 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: quand-alors-alors:
étant donné un utilisateur inactif
lorsque l'administrateur active cet utilisateur
puis l'utilisateur devient actif
et un e-mail de confirmation est envoyé à l'utilisateur
et une entrée est ajoutée au journal système
(etc.)
De tels scénarios sont utiles pour voir comment une seule commande peut affecter différentes parties de votre infrastructure - 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, penser aux commandes vous aide vraiment à créer une application orientée tâche. Vos utilisateurs apprécieront ceci :-)
Django fournit deux moyens simples d’exprimer des commandes; ce sont deux options valables et il n’est pas rare de mélanger les deux approches.
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 sous forme de 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
L'autre façon consiste à utiliser un Django Form pour chaque commande. Je préfère cette approche, car elle combine plusieurs aspects étroitement liés:
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
Votre exemple ne contenait aucune requête. Je me suis donc permis de créer quelques requêtes utiles. Je préfère utiliser le terme "question", mais requêtes est la terminologie classique. Les requêtes intéressantes sont: "Quel est le nom de cet utilisateur?", "Cet utilisateur peut-il se connecter?", "Afficher la liste des utilisateurs désactivés" et "Quelle est la répartition géographique des utilisateurs désactivés?"
Avant de répondre à ces questions, vous devez toujours vous poser deux questions: s'agit-il d'une requête présentation pour mes modèles uniquement et/ou d'une requête logique applicative liée à l'exécution de mon commandes, et/ou une requête reporting.
Les requêtes de présentation sont simplement faites pour améliorer l'interface utilisateur. Les réponses aux requêtes de la logique métier affectent directement l'exécution de vos commandes. Les requêtes de rapport servent uniquement à des fins d'analyse et ont des contraintes de temps plus souples. Ces catégories ne sont pas mutuellement exclusives.
L'autre question est: "est-ce que j'ai un contrôle complet sur les réponses?" Par exemple, lorsque vous interrogez 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.
La requête la plus élémentaire dans Django est l'utilisation de l'objet Manager:
User.objects.filter(active=True)
Bien entendu, 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 envisager les options ci-dessous.
La première alternative est utile pour les requêtes qui ne sont que des présentations: balises personnalisées et filtres de modèles.
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)
Si votre requête n’est pas simplement une 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
Les modèles de proxy sont très utiles dans le contexte de la logique métier et du reporting. Vous définissez en gros un sous-ensemble amélioré de votre modèle. Vous pouvez remplacer un QuerySet de base du gestionnaire en remplaçant la méthode Manager.get_queryset()
.
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
Pour les requêtes intrinsèquement complexes, mais exécutées assez souvent, il existe une 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 requête unique sont stockées dans un modèle séparé. L'astuce consiste évidemment à maintenir le modèle dénormalisé synchronisé 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 sûr émis par vos commandes. Les signaux présentent l'avantage de pouvoir synchroniser plusieurs modèles de requête avec votre modèle d'origine. En outre, le traitement du signal peut être déchargé sur des tâches en arrière-plan, à l'aide de Celery ou de 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()
Avec cette approche, il est ridiculement facile de déterminer si votre code reste propre. Suivez simplement ces instructions:
Il en va de même pour les vues (car les vues souffrent souvent du même problème).
J'implémente généralement une couche de service entre les vues et les modèles. Cela ressemble à l'API de votre projet et vous donne une bonne vue en hélicoptère de ce qui se passe. J'ai hérité de cette pratique d'un collègue à moi qui utilise beaucoup cette technique de superposition avec des 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 """
if limit:
return Book.objects.filter(**filters)[:limit]
return Book.objects.filter(**filters)
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()
Remarquez que je prends généralement les modèles, les vues et les services au niveau du module et les sépare encore plus en fonction de la taille du projet.
Tout d'abord, ne vous répétez pas .
Ensuite, veillez à ne pas trop d'ingénieurs, c'est parfois une perte de temps qui fait perdre la concentration sur ce qui est important. Passez en revue le zen of python de temps en temps.
Regardez les projets actifs
le référentiel de structure est également un bon exemple à regarder.
yourapp/models/logicalgroup.py
_User
, Group
et les modèles associés peuvent aller sous _yourapp/models/users.py
_Poll
, Question
, Answer
... peut aller sous _yourapp/models/polls.py
___all__
_ à l'intérieur de _yourapp/models/__init__.py
_request.GET
_/_request.POST
_ ... etctastypie
ou piston
Profitez de middleware / tags du je
Tirez parti de gestionnaires de modèles
User
peut aller dans une UserManager(models.Manager)
.models.Model
_.queryset
pourraient aller dans un _models.Manager
_.User
un à la fois, de sorte que vous pensiez 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 autant que possible
Une grande partie du code standard peut être éliminée si vous avez des formulaires mappés à un modèle. Le ModelForm documentation
est très bon. Séparer le 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 commandes de gestion si possible
yourapp/management/commands/createsuperuser.py
_yourapp/management/commands/activateinbulk.py
_si vous avez une logique métier, vous pouvez la séparer
Django.contrib.auth
_ tilise backends , tout comme db a un backend ... etc.setting
pour votre logique métier (par exemple, _AUTHENTICATION_BACKENDS
_)Django.contrib.auth.backends.RemoteUserBackend
_yourapp.backends.remote_api.RemoteUserBackend
_yourapp.backends.memcached.RemoteUserBackend
_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
_
plus sur les modèles de conception
plus sur les limites de l'interface
yourapp.models
_yourapp.vendor
_yourapp.libs
_yourapp.libs.vendor
_ ou _yourapp.vendor.libs
_En bref, vous pourriez avoir
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 quelque chose d'autre qui vous aide; trouver les interfaces dont vous avez besoin et les limites vous aideront.
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 tend à semer la confusion avec les conversions MVC car, dans MVC, une vue ressemble davantage 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 le devoir avec la "vue" 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, il s'agit toujours d'un code modèle. En fait, les modèles ne sont pas obligés d'interagir avec la base de données. Vous pouvez donc concevoir des modèles qui existent entièrement en tant que couche interactive vers une API externe. C'est un concept beaucoup plus libre de "modèle".
Dans Django, la structure MVC est, comme Chris Pratt l'a dit, différente du modèle MVC classique utilisé dans d'autres frameworks. Je pense que la raison principale est d'éviter une structure d'application trop stricte, comme cela se produit dans d'autres frameworks MVC tels que CakePHP.
Dans Django, MVC a été implémenté de la manière suivante:
La couche de vue est divisée 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, modèles, classes personnalisées ou, dans des cas simples, directement avec des modèles). Pour créer l'interface, nous utilisons des modèles. Les modèles ressemblent à des chaînes pour Django, cela mappe un contexte, et ce contexte a été communiqué à la vue par l'application (lorsque la vue le demande).
La couche modèle fournit l'encapsulation, l'abstraction, la validation, l'intelligence et rend vos données orientées objet (on dit qu'un jour, le SGBD le sera é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é 'modèles', de créer un fichier '__init__.py' dans ce dossier. dossier dans lequel vous importez tous vos modèles et utilisez enfin l'attribut 'app_label' de models.Model class). Le modèle devrait vous éviter d'utiliser des données, cela simplifiera votre application. Vous devez également, si nécessaire, créer des classes externes, telles que 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 classe Meta 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 programmes, il serait suffisant d’utiliser des vues pour interroger ou insérer des données. Les cas les plus courants utiliseront Forms ou ModelForms, qui sont en réalité des "contrôleurs". Ce n'est pas autre qu'une solution pratique à un problème commun, et très rapide. C'est ce qu'un site Web fait.
Si les formulaires ne vous conviennent pas, vous devez créer vos propres classes pour faire de la magie. Un très bon exemple de ceci est l’application admin: vous pouvez lire le code ModelAmin, cela fonctionne en tant que contrôleur. Il n'y a pas de structure standard, je vous suggère donc 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 d’API, ajouter Celery pour la réalisation de tâches, torsadé pour une application basée sur un réacteur, utiliser uniquement l’ORM, créer un service Web, modifier l’application admin et bien plus encore… C’est votre responsabilité de créer un code de bonne qualité, de respecter la philosophie de MVC ou non, d’en faire un module et de créer vos propres couches d’abstraction. C'est très flexible.
Mon conseil: lisez autant de code que vous pouvez, il y a beaucoup d'applications Django, mais ne les prenez pas si au sérieux. Chaque cas est différent, les schémas et la théorie aident, mais pas toujours, c’est une science imprécise, Django vous fournit simplement de bons outils que vous pouvez utiliser pour atténuer certaines difficultés (telles que l'interface d'administration, la validation de formulaire Web, i18n). , mise en œuvre de modèles d’observateur, toutes les précédentes et d’autres), mais les bonnes conceptions viennent de designers expérimentés.
PS .: utilisez la classe 'User' de l’authentification (de Django standard), vous pouvez par exemple créer des profils d’utilisateur, ou au moins lire son code, cela vous sera utile.
Je suis principalement d'accord avec la réponse choisie ( https://stackoverflow.com/a/12857584/871392 ), mais je souhaite ajouter une option dans la section Création de requêtes.
On peut définir des classes QuerySet pour les modèles pour les requêtes de filtre et les fils. Après cela, vous pouvez créer un proxy pour cette classe de requête pour le gestionnaire de modèle, comme le font les classes de gestionnaire 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 placer cela dans un module séparé, comme suggéré précédemment.
Je serais d'accord avec vous. Il y a beaucoup de possibilités dans Django, mais le meilleur endroit pour commencer est de passer en revue philosophie de conception de Django .
L'appel d'une API à partir d'une propriété de modèle ne serait pas idéal. Il semble plus logique de faire quelque chose comme cela dans la vue et éventuellement de créer une couche de service pour que tout reste au sec. Si l'appel à l'API n'est pas bloquant et que l'appel est coûteux, il peut être judicieux d'envoyer la demande à un agent de service (un agent qui consomme depuis une file d'attente).
Selon la philosophie de conception de Django, les modèles englobent tous les aspects d'un "objet". Ainsi, toute logique métier liée à cet objet doit y vivre:
Inclut toute la logique de domaine pertinente
Les modèles doivent englober tous les aspects d’un "objet", conformément au modèle de conception Active Record de Martin Fowler.
Les effets secondaires que vous décrivez sont évidents, la logique ici pourrait être mieux décomposée en groupes de requêtes et en 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)