webentwicklung-frage-antwort-db.com.de

Aggregierte (und andere kommentierte) Felder in Django Rest Framework-Serialisierern

Ich versuche, den besten Weg herauszufinden, um kommentierte Felder hinzuzufügen, wie etwa aggregierte (berechnete) Felder zu DRF (Modell) -Serialisierern. Mein Anwendungsfall ist einfach eine Situation, in der ein Endpunkt Felder zurückgibt, die NICHT in einer Datenbank gespeichert sind, sondern aus einer Datenbank berechnet werden.

Schauen wir uns das folgende Beispiel an:

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()

serializers.py

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

gewünschte JSON-Ausgabe:

[

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

Ich habe ein paar Lösungen, die funktionieren, aber jede hat einige Probleme.

Option 1: Hinzufügen von Gettern zum Modell und Verwenden von 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']

serializers.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')

Der obige Code kann vielleicht etwas umgestaltet werden, ändert aber nichts an der Tatsache, dass diese Option zwei zusätzliche SQL-Abfragen per IceCreamCompany durchführt, was nicht sehr effizient ist.

Option 2: Annotieren in ViewSet.get_queryset

models.py wie ursprünglich beschrieben.

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')
        )

Dadurch werden die aggregierten Felder in einer einzelnen SQL-Abfrage abgerufen. Ich bin mir jedoch nicht sicher, wie ich sie zum Serializer hinzufügen würde, da DRF nicht magisch weiß, dass ich diese Felder in QuerySet mit Anmerkungen versehen habe. Wenn ich total_trucks und total_capacity zum Serializer hinzufüge, wird ein Fehler ausgegeben, wenn diese Felder nicht im Modell vorhanden sind. 

Option 2 kann ohne einen Serializer mithilfe von View funktionieren. Wenn das Modell jedoch viele Felder enthält und nur einige davon in der JSON enthalten sein müssen, wäre es ein hässlicher Hack, den Endpunkt ohne zu erstellen ein Serialisierer.

32
elnygren

Mögliche Lösung:

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')
        )

serializers.py

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

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

Durch die Verwendung von Serializer-Feldern habe ich ein kleines Beispiel zur Arbeit bekommen. Die Felder müssen als Klassenattribute des Serialisierers deklariert werden, damit DRF keinen Fehler darüber ausgibt, der nicht im IceCreamCompany-Modell vorhanden ist.

40
elnygren

Sie können den ModelSerializer-Konstruktor hacken, um das Abfrageset zu ändern, das von einer Ansicht oder einem Viewset übergeben wird.

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=...,
         )

Der Name _build_queryset hat keine Bedeutung (er überschreibt nichts), er ermöglicht es uns lediglich, den aufgeblähten Bereich aus dem Konstruktor herauszuhalten.

1
Andrey Berenda

Ich habe eine leichte Vereinfachung der Antwort von elnygreen gemacht , indem ich das Queryset kommentierte, als ich es definierte. Dann muss ich get_queryset() nicht überschreiben.

# 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')

Wie elnygreen gesagt hat, müssen die Felder als Klassenattribute des Serialisierers deklariert werden, um einen Fehler zu vermeiden, der nicht im IceCreamCompany-Modell vorhanden ist.

0
Don Kirkby