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.
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.
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.
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.