Encapsuler la complexité avec l'ORM de Django

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Comment combattre la complexité logicielle d'un monolithe Django qui grossit ?

En encapsulant cette complexité plutôt que de l'étaler partout (vues, sérialiseurs, formulaires, gabarits etc.).

Comment encapsuler ? Eh bien ! C'est un point de vue subjectif.

Certains mettent en place des service layers.

James Bennett a des arguments contre eux dans Against service layers in Django et More on service layers in Django parmi lesquels une mise en relief du caractère central et non-amovible de l'ORM dans Django :

And in Django-based applications it's even less likely to try to swap out the ORM without other huge code changes happening at the same time, because the Django ORM is probably the single most tightly-integrated component of the entire framework — if you stop using it, you're throwing away so much other stuff that “why are we even still using Django” becomes a really significant question.

Pour moi, une application Django bien conçue doit encapsuler la complexité métier avec l'ORM.

Model, Manager et QuerySet

L'ORM permet d'exposer des APIs par le truchement des classes Model, Manager et QuerySet.

La classe Model implémente le motif Active Record. La classe représente une table de la base de données, tandis qu'une instance de cette classe représente une ligne d'une table de la base de données.

Au minimum, un Manager et un QuerySet sont associés à chaque Model. Ces trois classes sont intimement liées et leur combinaison (avec une touche de métaclasse) offre une abstraction de base de données permettant de créer, obtenir, mettre à jour et supprimer des objets.

La classe QuerySet porte les opérations CRUD.

La classe Manager est surtout un proxy entre un Model et un QuerySet. La quasi-totalité de l'API QuerySet est exposée par l'intermédiaire d'un Manager dont le nom est objects par défaut :

all_entries = Entry.objects.all()

La majorité des objets en provenance de cette abstraction sont soit des instances de Model (un seul objet), soit des instances de QuerySet (une collection d’objets).

Les instances de QuerySet disposent en plus de propriétés spécifiques :

Des API de requêtes de plus haut niveau

L'ajout de méthodes à un Model permet d'ajouter des fonctionnalités au niveau ligne d'une table de la base de données.

Augmenter un Manager est la recommandation pour ajouter des fonctionnalités au niveau table de la base de données. Les nouvelles méthodes ne sont pas tenues de renvoyer exclusivement des instances de QuerySet.

L'apparition de from_queryset dans Django 1.7 facilite l'utilisation de QuerySet personnalisés pour tirer parti de la faculté de pouvoir en chaîner des instances :

Ces possibilités de personnalisation, mais aussi le fait de pouvoir avoir plusieurs Manager et QuerySet par Model, permettent de bâtir des API de requêtes de plus haut niveau.

Quoi mettre où ?

C'est là qu'on entre en zone grise parce qu'il n'y a pas toujours une réponse définitive.

Le mieux est de se mettre d'accord avec son équipe sur des conventions, par exemple :

  • ce qui implique une seule instance va sur la classe Model :
    • @property
    • ou méthode de classe si des paramètres sont nécessaires
  • ce qui implique plusieurs instances va :
    • sur le Manager
    • ou sur le QuerySet si ça peut être chaîné
  • un constructeur alternatif va dans une méthode de Manager (plutôt que dans une @classmethod)

Ces conventions doivent permettre de basculer vers une architecture répandue en Django, Use fat models, and thin views, qui encapsule la complexité métier au niveau des modèles.

Le conseil le plus pragmatique pour tendre vers cette architecture vient de Tom Christie dans Django models, encapsulation and data integrity. Il préconise pour tout le code en dehors des modèles :

Never write to a model field or call save() directly. Always use model methods and manager methods for state changing operations.

This is a simple, unambiguous convention, that's easily enforceable at the point of code review.

Cette architecture permet aussi de tester unitairement le code des modèles et de rendre les tests d'intégration des vues moins verbeux.

En résumé

Une application Django bien conçue encapsule la complexité métier grâce à l'ORM en exposant des APIs de requêtes de plus haut niveau au reste du code.

Cette approche produit une architecture fat models and thin views.

Une méthode pragmatique permettant d'y arriver :

  • ne jamais écrire directement dans un champ de modèle en dehors du code des modèles
  • ne jamais appeler save() directement en dehors du code des modèles

Si vous adoptez cette méthode à la lettre, vous devrez vous passer des ModelForm (voir le paragraphe Breaking the contract). Vous perdrez alors certains avantages tels que la validation et la concision. C’est peut-être beaucoup demander notamment à des débutants… Une voie intermédiaire est possible avec les ModelForm et une encapsulation un chouïa moins stricte.

Avant Notes sur L'Art de PostgreSQL Après Les métaclasses en Python et Django

Tag Kemar Joint