Blog // Exirel.me

La logique métier et les managers de Django

Par Florian Strzelecki - 13:26 - 03.10.2013

Tags : Django, Python, Programmation, Bonne pratique, ORM

Lors de la Djangocong 2013 à Belfort, @Evlf a proposé un très bon sujet de réflexions avec sa conférence : Une logique métier bien placée. Son constat est simple : tout bon débutant qui se respecte va avoir tendance à utiliser les fonctions spécifiques de l'ORM dans ses vues Django.

Ce qui est plutôt normal pour un débutant devient problématique aux yeux de quelqu'un d'un peu plus expérimenté : quel est la part de "logique métier" qui se retrouve ainsi dans un espace dédié à la vue, la présentation.

Je rejoins @Evlf sur ses questions : comment bien placer la logique métier ? Pour ma part, je pense qu'il est possible d'éviter bien des ennuis en utilisant correctement les managers de Django, et ces derniers sont donc le sujet de cet article.

Qu'est-ce qu'un manager pour l'ORM de Django ?

Quand il est question des managers, de quoi parle-t-on exactement ? La documentation est séparée en deux parties pour nous éclairer un peu sur leur rôle et leur fonctionnement : effectuer des requêtes avec l'ORM d'un côté, et les managers de l'autre. Je ne peux que vous inviter à lire ces documentations, qui expliquent globalement ce qu'il faut savoir sur le sujet - cet article n'est jamais qu'un apperçu plus synthétique.

J'aimerais souligner une phrase en particulier :

A Manager is the interface through which database query operations are provided to Django models.

Les managers, ce sont les interfaces pour effectuer des requêtes, là où les modèles ne sont, finalement, que des représentations (interne) des données. Ils agissent comme une couche intermédiaire entre le stockage des données et leurs représentations externes. Ce sont donc les bons outils où mettre de la logique métier.

Le soucis c'est lorsque ceci arrive directement dans une vue :

latest = Blog.objects.filter(published=True) \
                  .order_by('-published_at') \
                  .all()[0:10]

Pour récupérer les 10 dernières publications, il faut enchaîner les fonctions spécifiques de l'ORM. Cela ne ressemble pas vraiment à une abstraction de la logique métier, mais bien à son usage concret directement dans une vue. Que se passerait-il si, à la suite d'une mise à jour, un autre attribut devait être pris en compte (comme une date) ? Il faudrait alors modifier toutes les vues qui utilisent la même requête - et c'est sans parler d'un potentiel système de cache...

La solution : créer et utiliser son propre manager !

Utiliser ses propres managers

Les sous-classes de Model disposent tous d'un attribut objects qui est le manager par défaut, automatiquement associé à la création de la classe (via le mécanisme de méta classe). Il s'agit, tout simplement, d'un objet de la classe django.db.models.Manager. C'est ce qui permet d'effectuer la requête sus-mentionnée.

Pour profiter des fonctionnalités des managers, rien de plus simple : il suffit d'étendre la classe Manager, et de l'associer à la classe de son modèle. Voici un exemple :

from django.db import models

class PublishingManager(models.Manager):
    """Publishing manager for Blog's model"""
    def latest_published(self):
        """Returns the latest published blog"""
        return self.filter(published=True).order_by('-published_at')

class Blog(models.Model):
    """Blog's model"""
    # some attributes here...

    objects = PublishingManager()

# Somewhere in a view
latest = Blog.objects.latest_published()[0:10]

Il y a une nette progression pour la vue : elle est toujours responsable du nombre d'élément qu'elle récupère, mais plus de la façon dont elle les récupère. S'il faut ajouter des filtres ou des traitements, tout pourra être fait dans la méthode PublishingManager.latest_published.

Nommer un manager

Plutôt que passer par Blog.objects.latest_published, vous pourriez avoir envie de simplement faire Blog.published.latest. Rien de plus simple, il suffit de faire ceci dans la classe du modèle :

class Blog(models.Model):
    """Blog's model"""
    # some attributes here...

    published = PublishingManager()

En renommant latest_published par latest dans le manager, vous aurez tout ce qu'il faut. De plus, la forme Blog.objects.filter fonctionnerait toujours grâce à un fonctionnement un peu "magique" des modèles Django : le premier manager défini pour une classe est aussi assigné à l'attribut "objects" de la classe.

Vous pourriez ne pas vouloir de ce comportement, pour éviter de modifier l'usage "par défaut" de vos objets (ce qui se comprend tout à fait). Pour se faire, il suffit de déclarer un attribut de classe objects comme suit :

class Blog(models.Model):
    """Blog's model"""
    # some attributes here...

    objects = models.Manager()
    published = PublishingManager()

Modifier la base de la requête

Parfois, vous avez besoin d'appliquer des règles métiers très spécifiques à un ensemble de requêtes. Par exemple, vous pourriez vouloir n'effectuer que des requêtes sur des blogs "publiés", que ce soit "les derniers" ou bien simplement "tous les blogs du mois dernier", voire "tous les blogs publiés ayant pour tag Django". Il faut alors modifier le comportement de la requête de base effectuée par le manager : il faut modifier la QuerySet !

La encore, la solution est dans la documentation : Modifying initial Manager QuerySets. Voici ce qu'elle propose, illustrée par nos précédents exemples :

class PublishingManager(models.Manager):
    """Publishing manager for Blog's model"""
    def get_query_set(self):
        """Returns the base QuerySet."""
        return super(PublishingManager, self).get_query_set() \
                                             .filter(published=True)

    def latest(self):
        """Returns the latest published blog"""
        return self.order_by('-published_at')

Vous noterez que :

  1. La méthode latest n'a plus besoin de définir un filtre.
  2. La méthode get_query_set utilise la fonction parent, et ajoute un simple filter(published=True).

Il ne reste plus qu'à utiliser alors votre manager comme ceci :

all_blog = Blog.objects.all()
all_published_blog = Blog.published.all()

Pensez bien à définir vous-même le manager par défaut, surtout si vous utilisez l'interface d'administration de Django, qui pourrait avoir quelque soucis en utilisant votre manager.

Logique métier et bonnes pratiques

Vous avez pu le voir, il est très simple de manipuler les managers, et d'ajouter des comportements métiers à vos managers. Je pense que c'est la meilleure façon de conserver cette logique dans une seule partie du code, et de ne pas l'exposer directement dans les vues, là où cette logique n'a pas à être.

Il faut penser les managers comme la couche d'abstraction entre le stockage et la représentation, ce qui est un principe d'architecture déclinable à plusieurs niveaux de granularité :

Dans tous les cas, les données sont stockées d'un côté, avec une représentation interne qui ne dispose que des règles spécifiques liées au stockage. De l'autre côté, il y a une représentation externe de ces données, dénuée de la logique métier car ce n'est pas son rôle. Et au milieu, un mécanisme qui permet d'obtenir les données, de passer d'un monde à l'autre, avec des règles bien définies.

Ce n'est que mon avis, mais je pense que c'est une approche qui permet de simplifier grandement la maintenance, et qu'elle améliore la lisibilité du code. Surtout avec un peu de documentation...