web-dev-qa-db-fra.com

Champs agrégés (et autres annotés) dans les sérialiseurs Django Rest Framework

J'essaie de trouver le meilleur moyen d'ajouter des champs annotés, tels que des champs agrégés (calculés) à des sérialiseurs DRF (modèle). Mon cas d'utilisation est simplement une situation dans laquelle un point de terminaison renvoie des champs qui ne sont PAS stockés dans une base de données mais calculés à partir d'une base de données.

Regardons l'exemple suivant:

models.py

class IceCreamCompany(models.Model):
    name = models.CharField(primary_key = True, max_length = 255)

class IceCreamTruck(models.Model):
    company = models.ForeignKey('IceCreamCompany', related_name='trucks')
    capacity = models.IntegerField()

sérialiseurs.py

class IceCreamCompanySerializer(serializers.ModelSerializer):
    class Meta:
        model = IceCreamCompany

sortie JSON souhaitée:

[

    {
        "name": "Pete's Ice Cream",
        "total_trucks": 20,
        "total_capacity": 4000
    },
    ...
]

J'ai quelques solutions qui fonctionnent, mais chacune a des problèmes.

Option 1: ajouter des getters à modéliser et utiliser SerializerMethodFields

models.py

class IceCreamCompany(models.Model):
    name = models.CharField(primary_key=True, max_length=255)

    def get_total_trucks(self):
        return self.trucks.count()

    def get_total_capacity(self):
        return self.trucks.aggregate(Sum('capacity'))['capacity__sum']

sérialiseurs.py

class IceCreamCompanySerializer(serializers.ModelSerializer):

    def get_total_trucks(self, obj):
        return obj.get_total_trucks

    def get_total_capacity(self, obj):
        return obj.get_total_capacity

    total_trucks = SerializerMethodField()
    total_capacity = SerializerMethodField()

    class Meta:
        model = IceCreamCompany
        fields = ('name', 'total_trucks', 'total_capacity')

Le code ci-dessus peut peut-être être légèrement modifié, mais cela ne changera pas le fait que cette option effectuera 2 requêtes SQL supplémentaires par IceCreamCompany qui n'est pas très efficace.

Option 2: annoter dans ViewSet.get_queryset

models.py comme décrit à l'origine.

views.py

class IceCreamCompanyViewSet(viewsets.ModelViewSet):
    queryset = IceCreamCompany.objects.all()
    serializer_class = IceCreamCompanySerializer

    def get_queryset(self):
        return IceCreamCompany.objects.annotate(
            total_trucks = Count('trucks'),
            total_capacity = Sum('trucks__capacity')
        )

Cela obtiendra les champs agrégés dans une requête SQL unique, mais je ne sais pas comment les ajouter au sérialiseur, car DRF ne sait pas, comme par magie, que j'ai annoté ces champs dans le QuerySet. Si j'ajoute total_trucks et total_capacity au sérialiseur, une erreur sera générée sur le fait que ces champs ne sont pas présents sur le modèle. 

L'option 2 peut fonctionner sans sérialiseur avec View , mais si le modèle contient beaucoup de champs, et que seuls certains doivent être dans le JSON, ce serait un peu moche de construire le point de terminaison sans un sérialiseur.

32
elnygren

Solution possible:

views.py

class IceCreamCompanyViewSet(viewsets.ModelViewSet):
    queryset = IceCreamCompany.objects.all()
    serializer_class = IceCreamCompanySerializer

    def get_queryset(self):
        return IceCreamCompany.objects.annotate(
            total_trucks=Count('trucks'),
            total_capacity=Sum('trucks__capacity')
        )

sérialiseurs.py

class IceCreamCompanySerializer(serializers.ModelSerializer):
    total_trucks = serializers.IntegerField()
    total_capacity = serializers.IntegerField()

    class Meta:
        model = IceCreamCompany
        fields = ('name', 'total_trucks', 'total_capacity')

En utilisant Les champs du sérialiseur J'ai un petit exemple à utiliser. Les champs doivent être déclarés en tant qu'attributs de classe du sérialiseur afin que DRF ne génère pas d'erreur les excluant dans le modèle IceCreamCompany.

40
elnygren

Vous pouvez pirater le constructeur ModelSerializer pour modifier le jeu de requêtes transmis par une vue ou un jeu de vues.

class IceCreamCompanySerializer(serializers.ModelSerializer):
    total_trucks = serializers.IntegerField(readonly=True)
    total_capacity = serializers.IntegerField(readonly=True)

    class Meta:
        model = IceCreamCompany
        fields = ('name', 'total_trucks', 'total_capacity')

    def __new__(cls, *args, **kwargs):
        if args and isinstance(args[0], QuerySet):
              queryset = cls._build_queryset(args[0])
              args = (queryset, ) + args[1:]
        return super().__new__(cls, *args, **kwargs)

    @classmethod
    def _build_queryset(cls, queryset):
         # modify the queryset here
         return queryset.annotate(
             total_trucks=...,
             total_capacity=...,
         )

Il n'y a aucune signification dans le nom _build_queryset (cela ne remplace rien), cela nous permet simplement de garder le bloat hors du constructeur.

1
Andrey Berenda

J'ai légèrement simplifié la réponse de elnygreen en annotant le jeu de requête lorsque je l'ai défini. Dans ce cas, je n'ai pas besoin de remplacer get_queryset().

# views.py
class IceCreamCompanyViewSet(viewsets.ModelViewSet):
    queryset = IceCreamCompany.objects.annotate(
            total_trucks=Count('trucks'),
            total_capacity=Sum('trucks__capacity'))
    serializer_class = IceCreamCompanySerializer

# serializers.py
class IceCreamCompanySerializer(serializers.ModelSerializer):
    total_trucks = serializers.IntegerField()
    total_capacity = serializers.IntegerField()

    class Meta:
        model = IceCreamCompany
        fields = ('name', 'total_trucks', 'total_capacity')

Comme le disait elnygreen, les champs doivent être déclarés en tant qu'attributs de classe du sérialiseur afin d'éviter toute erreur les concernant n'existant pas dans le modèle IceCreamCompany.

0
Don Kirkby