Django et logique de dev

a marqué ce sujet comme résolu.

Hello tout le monde,

Je suis en train de développer mon app Django (comme certains peuvent déjà l’avoir compris ^^). J’en suis encore au tout début, mais j’ai mis en place mes modèles de BDD et je commence sur mes vues. J’ai actuellement la vue suivante :

def enterprise(request, enterprise_id):
    enterprise = get_object_or_404(Enterprise, pk=enterprise_id)
    addresses = Address.objects.filter(enterprise=enterprise)
    context = {
        "page_title": enterprise.name,
        "enterprise": enterprise,
        "addresses": addresses,
    }
    return render(request, "ecoliste/enterprise.html", context)

Et en fait, je me rends compte que j’aimerais bien avoir des fonctions nommées plutôt que d’écrire des filtres, et qu’en plus cela augmenterait la réutilisabilité de mon code. Par exemple :

def enterprise(request, enterprise_id):
    enterprise = get_enterprise(request, enterprise_id)
    addresses = get_enterprise_addresses(enterprise)
    context = {
        "page_title": enterprise.name,
        "enterprise": enterprise,
        "addresses": addresses,
    }
    return render(request, "ecoliste/enterprise.html", context)

Bon, la 1e je suis pas convaincu, parce que la requête get_object_or_404 est vraiment pas complexe, et définir une autre fonction nécessite de passer la requête en paramètre pour gérer la 404. Edit : pas du tout en fait, comme c’est une erreur y a pas besoin x) Par contre, c’est normalement le seul cas où j’ai besoin de gérer une 404, donc pour les autres je pourrais définir des fonctions nommées qui fonctionnent bien. D’un autre côté, ce serait vraiment pour quelques lignes de codes.

Du coup, qu’en pensez-vous ? Est-ce que cela rendrait vraiment mon code plus lisible, et peut-être plus testable ? Et si oui, où est-ce que je devrais rédiger ces fonctions, dans les models, ou dans les views ? (Ou ailleurs ?)

Merci :)

+0 -0

Salut,

Je ne suis pas un pro de django mais si tu as une relation entre entreprise et adresse pourquoi ne pas directement utiliser le related_name plutôt qu’un filtre ?

Depuis une instance de enterprise tu dois pouvoir accéder aux addresses avec quelque chose comme enterprise.addresse_set non ?

Je pense que faire une fonction get_entreprise qui (à priori) appelle directement get_object_or_404 donne un code moins lisible de mon point de vue. En lisant la fonction avec get_enterprise on a pas la notion d’erreur 404 (sauf si on va voir la définition de get_enterprise).

+1 -0

Parce que je ne savais pas le faire, tout simplement :magicien: Merci du conseil, je testerai demain !

Effectivement, le get or 404 a cet avantage, et en plus est assez lisible par défaut.

Non mais du coup, c’est vrai que si c’est aussi simple, sachant qu’en plus je me suis embêté à écrire des related names…

+0 -0

Une autre option si jamais ça se révélait vraiment devenir complexe et que tu voulais avoir comme tu dis des fonctions nommées pour faire des requêtes particulières, Django a quelque chose de pensé pour ça : les managers (ou gestionnaires).

En fait, un gestionnaire, tu en as déjà utilisé un : c’est le Model.objects. Le fait est que ça, tu peux le modifier pour mettre ton gestionnaire perso, avec tes méthodes en plus de filter, get, etc.

C’est assez simple, juste une classe à écrire avec tes méthodes et un attribut à ajouter pour dire d’utiliser ce gestionnaire. En fait, tu peux même avoir plusieurs gestionnaires pour un même modèle, pour des cas d’usage plus avancés (avec Model.objects et un autre Model.other_objects par exemple). Par exemple (je reprends honteusement celui de la doc de Django liée plus haut, je t’invite à la lire pour tous les détails) :

from django.db import models
from django.db.models.functions import Coalesce

class PollManager(models.Manager):
    def with_counts(self):
        return self.annotate(
            num_responses=Coalesce(models.Count("response"), 0)
        )

class OpinionPoll(models.Model):
    question = models.CharField(max_length=200)
    objects = PollManager()

…ce que tu peux tout simplement utiliser comme ça (les méthodes usuelles comme filter, order_by, etc., restent disponible) :

polls = OpinionPoll.objects.with_counts()

Ici with_count prend la requête et l’annote mais tu pourrais faire n’importe quoi (filtrer, annoter, trier, que sais-je), et comme toutes les méthodes derrière objects, c’est chainable (donc tu peux avoir ta méthode qui filtre, avec un autre filtre ou un tri derrière).

Dans ce cas présent je pense que ce ne sera pas nécessaire d’aller jusque là, mais ça peut être intéressant que tu saches que ça existe :) .

+2 -0
def enterprise(request, enterprise_id):
    enterprise = get_object_or_404(Enterprise, pk=enterprise_id)
    addresses = enterprise.addresses.all()
    context = {
        "page_title": enterprise.name,
        "enterprise": enterprise,
        "addresses": addresses,
    }
    return render(request, "ecoliste/enterprise.html", context)

Merci @Jeph :magicien:

Alors du coup, c’est super le related_name, et c’est bien ce qu’il me fallait :) Je me note aussi ce que tu as dit Amaury, actuellement je n’en aurai pas besoin, mais dans le futur…

Merci :)

+1 -0

(Merci Aabu, c’est tout con mais avec la tête dedans, je n’y ai pas pensé…)

Maintenant, j’ai une autre question. Sur mes données, je rentre des matériaux produits par les entreprises. Enfin, pas vraiment des matériaux, mais ce genre de données : Lafarge produit du ciment pour dalle (typologie de Structure) avec des produits recyclés.

J’ai modélisé ça en BDD :

Et tout ceci répond à cette idée :

J’ai donc une table, avec des matériaux qui sont produits par telle ou telle entreprise. Pour les relier à leur typologie de matériaux, j’ai une clef étrangère vers une table de typologies, et elle-même est reliée à de plus grandes catégories de matériaux. Et j’ai un champ order qui me permet de définir un ordre d’affichage. Et je me demande si c’est une bonne logique.

Voilà mon implémentation pour récupérer la liste des typologies et catégories (il y a sans doute moyen de faire une list comprehension, mais c’est pas ma priorité et je me pose d’abord la question sur l’implémentation) (et je me suis même pas encore occupé de filtrer par la production de l’entreprise) :

    materials = {}
    for category in MaterialTypeCategory.objects.order_by("order"):
        types = []
        for usage in category.usages.order_by("order"):
            types.append(usage.name)
        materials[category.name] = types

Le truc, c’est que j’ai fait tout ça en BDD, en me disant que ce serait un peu plus souple à l’usage : je peux ajouter ou enlever des typologies/catégories facilement (à condition de repasser sur mes données derrière, mais ça c’est une autre question). D’un autre côté, ces catégories ne sont pas censées changer, sinon ça rend caduque la moitié des données déjà rentrées. Également, il ne me semble pas que ce soit super top d’utiliser des appels en BDD juste pour faire des catégories et des ordres de lecture pour d’autres données ?

Du coup, je me demandais si je ne devais pas hardcoder une liste ou un dictionnaire, ou encore définir des choices pour ma table MaterialByEnterprise ?

+0 -0

Je ne sais pas trop si c’est une bonne idée de hard-codé cela ou non. Un avantage pour moi de ne pas hard-codé c’est que cela devient possible de réorganiser l’ordre des types de matériaux / category depuis l’admin assez facilement (en modifiant la valeur de order, ce qui se fait très facilement via des packages comme django-admin-sortable) alors que ce n’est pas le cas si c’est hard-codé.

En remarque non lié à ta question. Ta query a un problème de "N+1 select problem".

for category in MaterialTypeCategory.objects.order_by("order"):
    for usage in category.usages.order_by("order"):
  1. Requête les catégories
  2. Pour chaque catégorie -> requête les usages Donc tu va faire (|categories| + 1) queries.

Il est possible de faire cela en 2 requêtes en utilisant prefetch-related et Prefetch. Lien vers la doc à ce sujet : https://docs.djangoproject.com/fr/3.1/ref/models/querysets/#prefetch-related

+0 -0

Merci pour la réponse ! C’est vrai que je devrais envisager d’installer des extensions à Django pour certaines fonctionnalités. Ceci dit, je ne sais pas si celle-là fonctionnerait vraiment sachant que mes MaterialTypes sont tous dans une même table, mais doivent être groupées par catégorie.

En remarque non lié à ta question. Ta query a un problème de "N+1 select problem".

for category in MaterialTypeCategory.objects.order_by("order"):
    for usage in category.usages.order_by("order"):
  1. Requête les catégories
  2. Pour chaque catégorie -> requête les usages Donc tu va faire (|categories| + 1) queries.

Il est possible de faire cela en 2 requêtes en utilisant prefetch-related et Prefetch. Lien vers la doc à ce sujet : https://docs.djangoproject.com/fr/3.1/ref/models/querysets/#prefetch-related

Jeph

Oui ! J’ai découvert ça hier. J’ai fait quelques tests avec select_related puisqu’il me semble que c’est censé suffire pour les clefs étrangères, mais je n’ai pas réussi. Je retente ce soir :) (note : il me semble, en lisant mieux la doc, que c’est parce que select_related ne fonctionne que d’un seul côté de la clef étrangère).

+0 -0

Bon, en fait prefetch_related ne fonctionne pas. Je pense que c’est parce que ça ne gère pas la relation one-to-many depuis plusieurs objets. J’ai testé également de le mettre sur le enterprise, mais prefetch fait en réalité plusieurs requêtes, alors ça ne change pas le nombre de requêtes.

C’est peut-être moi qui n’y arrive pas, ou alors parce qu’il y a autre chose à faire (notamment via les Prefetch personnalisés, je ne sais pas). J’ai testé :

    materials = {}
    for category in MaterialTypeCategory.objects.order_by("order").prefetch_related("usages"):
        types = []
        for usage in category.usages.order_by("order"):
            types.append(usage.name)
        materials[category.name] = types

Ou encore :

    materials = {}
    categories = MaterialTypeCategory.objects.prefetch_related("usages").order_by("order")
    usages = categories.usages.all
    for category in categories:
        types = []
        for usage in usages.filter(category=category):
            types.append(usage.name)
        materials[category.name] = types

Et d’autres trucs, mais rien n’y fait. Tant pis, je vais continuer comme ça, et je verrai plus tard.

+0 -0

Je crois que tu n’es pas si loin du bon résultat !

Le problème dans ton premier exemple c’est que l’order by refait une query, pareil pour le filter dans le second exemple.

Je crois que pour avoir order_by avec prefetch_related il faut utiliser Prefetch.

Quelque chose dans ce genre (où Usage est le modèle du champ usages) devrait fonctionner je pense (ou quelque chose dans l’idée, il manque peut être des .all().

categories = MaterialTypeCategory.objects.order_by("order").prefetch_related(
  Prefetch("usages", queryset=Usage.objects.order_by("order")
)
for category in categories:
  for usage in category.usages:
     ...

Si ça ne fonctionne pas il y a quelques exemples avec Prefetch dans la documentation https://docs.djangoproject.com/fr/3.2/ref/models/querysets/

+0 -0

Salut @Jeph, merci pour la réponse :)

Alors déjà, du coup, j’ai changé le code parce que ce n’était pas ça dont j’avais besoin sur cette view. Je n’ai besoin que des matériaux actuellement produits par l’entreprise. Mais surtout, j’ai modifié mes models pour ajouter des order dessus. Notamment (je l’écris comme ça pour le montrer, mais dans mes class Meta de mes modèles la syntaxe est différente, bien sûr) :

MaterialTypeCategory.order_by("order")
MaterialType.order_by("category", "order")
MaterialByEnterprise.order_by("type__category", "type")

Sur le code pour les matériaux d’une entreprise spécifique, ça me permet de faire ça :

    materials = (
        enterprise.products.all()
        .select_related("type", "type__category", "origin")
        .prefetch_related("address")
    )
    types = {}
    for material in materials:
        if material.type in types.keys():
            types[material.type].append(material)
        else:
            types[material.type] = [material]
    materials_categorised = {}
    for type in types.keys():
        if type.category in materials_categorised.keys():
            materials_categorised[type.category][type] = types[type]
        else:
            materials_categorised[type.category] = {type: types[type]}

Et du coup, c’est vachement bien parce que le select_related() génère une seule requête, un peu complexe mais quand même. Et là, le prefetech_related() fonctionne également parce qu’il génère une seule requête supplémentaire mais crée une jointure avec les matériaux (ce qui n’est pas le cas sur enterprise parce que ce n’est pas nécessaire).

Du coup, pour récupérer tous les MaterialType, je peux faire MaterialType.objects.all().select_related("category") et faire une boucle dessus pour itérer de la même façon. Bon, faut que je refactorise un peu, je pense que c’est améliorable. Peut-être pas avec une list comprehension qui serait pas forcément très lisible, mais je vais étudier ça :)

Edit : ceci dit, je dis ça, mais pouvoir faire un MaterialTypeCategory.objects.all().prefetch_related("usages", "usages__products").filter(usages__products__enterprise=enterprise) serait super pratique, mais je crois que j’ai déjà testé.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte