Django序列化器计算钱包实例收支总和出错,求正确实现方案
问题描述
在应用中,每个用户拥有专属钱包存储日常收支记录,需要获取每个钱包实例下关联的收支金额总和。使用serializers.SerializerMethodField()实现时计算结果错误:示例中两个income类型的金额分别为7和12,正确总和应为19,但返回的expense_sum为83。
现有Serializer代码
from django.db.models import Sum from django.db.models import Q class WalletInstanceSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.id') entry = BudgetEntrySerializer(many=True, read_only=True) expense_sum = serializers.SerializerMethodField() class Meta: model = WalletInstance fields = '__all__' def get_expense_sum(self, obj): return WalletInstance.objects.filter(Q(id=obj.id)|Q(entry__entry_type='income')).aggregate(Sum('entry__amount'))['entry__amount__sum']
返回数据示例
[ { "id": "d458196e-49f1-42db-8bc2-ee1dba438953", "owner": 1, "entry": [ { "id": 3, "owner": 1, "title": "dsfdsf", "amount": 7, "description": "sdfdsf", "entry_type": "income", "date_added": "2022-08-13", "entry_category": { "id": 2, "name": "Transport" } }, { "id": 4, "owner": 1, "title": "fesfvsdfgvbtdg", "amount": 12, "description": "efesf", "entry_type": "income", "date_added": "2022-08-13", "entry_category": { "id": 2, "name": "Transport" } } ], "viewable": [ "admin@gmail.com" ], "expense_sum": 83, "name": "dsdsds", "date_added": "2022-08-13" } ]
相关Model代码
class BudgetEntry(models.Model): STATE= [ ('income','income'), ('expenses','expenses'), ] owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='owner_of_entry', on_delete=models.CASCADE) title = models.CharField(max_length=20) amount = models.IntegerField() description = models.CharField(max_length=60, null=True) entry_type = models.CharField(max_length=15, choices=STATE, null=True) entry_category = models.ForeignKey(Category, null=True, blank=True, related_name='category_of_entry', on_delete=models.SET_NULL) date_added = models.DateField(auto_now_add=True) class WalletInstance(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=30, null=True) owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='owner', on_delete=models.CASCADE) viewable = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='can_view', blank=True) entry = models.ManyToManyField(BudgetEntry, related_name='BudgetEntry', blank=True) date_added = models.DateField(auto_now_add=True)
问题分析
原代码的错误在于filter(Q(id=obj.id)|Q(entry__entry_type='income'))使用了逻辑或(|),这会筛选出当前钱包或者所有包含income类型收支的钱包,导致聚合计算时包含了其他钱包的收支数据,结果自然错误。
另外,通过WalletInstance反向关联BudgetEntry进行聚合时,多对多关系可能会产生重复行,影响求和结果,更高效的方式是直接从BudgetEntry模型筛选当前钱包关联的记录。
修正方案
方案一:修正SerializerMethodField的查询逻辑
直接从当前钱包的entry关联集合中筛选entry_type='income'的记录并求和,避免跨钱包的错误数据:
from django.db.models import Sum class WalletInstanceSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.id') entry = BudgetEntrySerializer(many=True, read_only=True) expense_sum = serializers.SerializerMethodField() class Meta: model = WalletInstance fields = '__all__' def get_expense_sum(self, obj): # 从当前钱包的entry关联中筛选income类型并求和,无数据时返回0避免None return obj.entry.filter(entry_type='income').aggregate(Sum('amount'))['amount__sum'] or 0
方案二:使用annotate提前聚合(更高效)
如果需要批量查询多个钱包,推荐在视图层使用annotate提前聚合数据,减少Serializer中的重复查询,提升性能:
# 视图中的查询逻辑 from django.db.models import Sum, Q wallet_instances = WalletInstance.objects.annotate( expense_sum=Sum( 'entry__amount', filter=Q(entry__entry_type='income'), output_field=models.IntegerField() ) ) # 此时Serializer可以直接使用annotate生成的字段,无需SerializerMethodField class WalletInstanceSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source='owner.id') entry = BudgetEntrySerializer(many=True, read_only=True) expense_sum = serializers.IntegerField(read_only=True) # 直接映射annotate的字段 class Meta: model = WalletInstance fields = '__all__'
内容的提问来源于stack exchange,提问作者rafaelHTML
相关产品推荐
相关产品推荐

