Django自定义QuerySet实现计算字段及序列化方法
问题1:是否可以在自定义QuerySet中实现批量计算total_cost
完全可以,但要注意QuerySet方法的作用范围是整个查询集合,不要写接收单个Recipe实例的方法——单实例逻辑属于模型层职责,QuerySet要做的是批量计算、避免重复代码、同时解决你现在property写法的N+1查询性能问题。
最推荐的实现方式是用数据库聚合注解(annotate)直接在数据库层完成计算,性能比Python层循环高几个量级,实现逻辑如下:
- 在自定义QuerySet中新增批量计算方法,用
Sum聚合关联表的成本值,用Coalesce处理无配料场景下的null值,转成默认0 - 计算逻辑直接通过
F表达式引用模型字段,和你原来单条配料的成本计算逻辑保持一致即可 - 不要和你现在写的一样传单个recipe参数,方法只需要返回追加了注解字段的查询集本身
代码示例:
from django.db.models import Sum, F, FloatField from django.db.models.functions import Coalesce class RecipeQueryset(models.QuerySet): """Custom queryset for Recipe Model""" def all_for_user(self, user): return self.filter(user=user) def with_total_cost(self): return self.annotate( total_cost=Coalesce( Sum( # 这里替换成你自己的单配料成本计算字段逻辑 # 示例逻辑:单配料成本 = 配料用量 * 关联原料的单位成本 F('ingredients__quantity') * F('ingredients__ingredient__unit_cost'), output_field=FloatField() ), 0.0 # 没有配料的配方总成本默认返回0 ) )
如果你的成本计算逻辑特别复杂,涉及大量分支判断、无法用数据库函数实现,也可以用Python批量计算的版本,记得必须加prefetch_related预取关联数据,避免N+1:
def with_total_cost_python(self): # 预取所有关联配料和关联原料,杜绝循环查库 qs = self.prefetch_related('ingredients__ingredient') for recipe in qs: total = 0 for r_ingredient in recipe.ingredients.all(): total += r_ingredient._get_individual_ingredient_cost() # 直接给实例挂载计算好的属性 recipe.total_cost = total return qs
注意:非必要不选Python循环的实现,数据量上来之后性能差距会非常明显。原来模型上写的
total_costproperty可以保留作为兜底——如果查询集调用了with_total_cost(),注解的属性会优先被读取,不会走property逻辑;如果没调用方法,property也能保证单实例访问时不报错。
问题2:计算字段如何接入序列化器
不需要做特殊配置,不管你是用数据库注解还是Python批量挂载属性,只要查询集返回的每个Recipe实例上带有total_cost属性,序列化器就可以像读取普通模型字段一样读取这个值。
你只需要在视图中链式调用刚才写的with_total_cost()方法,把计算字段追加到查询集上即可,序列化器部分不需要改动:
class RecipeListApi(APIView): class OutputSerializer(serializers.Serializer): id = serializers.IntegerField() name = serializers.CharField(max_length=255) yield_count = serializers.FloatField() yield_units = serializers.CharField() # 原有字段定义完全不用改,直接读实例属性即可 total_cost = serializers.FloatField() def get(self, request): # 链式调用追加计算字段 recipes = Recipe.objects.all_for_user(user=request.user).with_total_cost() serializer = self.OutputSerializer(recipes, many=True) return Response(serializer.data, status=status.HTTP_200_OK)
如果你用DRF的ModelSerializer,也只需要把total_cost加到fields列表里就行,和普通模型字段的使用没有任何区别。
内容的提问来源于stack exchange,提问作者Francois Paulsen

