日常业务中傲娇(landuo)的前端经常会要求额外序列化一些数据.drf提供了方便的嵌套序列化方案,尤其对于酷爱使用serializermethodfield的我来说嵌套序列化更是超级日常的东西.

嵌套序列化的使用

这里仍然使用https://www.yuque.com/yuqueyonghuctfbfj/ddeyfu/tf21g9这个文档中建立的三级简单外键模型.

  1. class TaskSerializer(serializers.ModelSerializer)
  2. class Meta:
  3. model = Task
  4. fields = '__all__'
  5. class StageSerializer(serializers.ModelSerializer)
  6. tasks = TaskSerializer(many=True) #自定义嵌套序列化器.如果字段名与模型中定义的关系名不同,同样需要source来指明关系.
  7. class Meta:
  8. model = Stage
  9. fields = '__all__'
  10. qs = Stage.objects.all()
  11. seriazlier = StageSerializer(instance = qs, many = True)
  12. print(serializer.data)

嵌套序列化的性能问题

然后调用stage_serializer.data的时候,打开性能分析软件就会发现.
1.执行了stages的sql,获取到了X条数据.
2.以stage的id为过滤条件,执行了X次sql来获取tasks的参数.
随着数据量的增长(全量数据获取,全量数据导出),性能逐渐变得令人无法忍受.

prefetch_related来解决性能问题.

django原生的prefetch_related可以解决这个问题。

文档:https://docs.djangoproject.com/zh-hans/4.0/ref/models/querysets/#prefetch-related

通过预取可以将上文的1+X次查询降低到2次查询。具体实现和原理与django模型类的cache有关,文档上说的很详细。

  1. qs = Stage.objects.all().prefetch_related('tasks')# 参数为relatedname或者外键名,多级关系支持django__语法。例如stage__flow.这种情况下查询会变成3条,同时查询stageflow的信息。
  2. seriazlier = StageSerializer(instance = qs, many = True)
  3. print(serializer.data)

实际测试下来,两千条数据的序列化可以从10秒优化到1.5秒。

Prefetch对象

文档:https://docs.djangoproject.com/zh-hans/4.0/ref/models/querysets/#prefetch-objects

当你需要更复杂的prefetch的时候.可以使用prefetch对象.
比如:stage序列化的时候只嵌套序列化名字是test的task.

  1. test_prefetch = Prefetch(
  2. 'tasks',
  3. queryset=Task.objects.filter(name='task'),
  4. to_attr='test_tasks')
  5. qs = Stage.objects.all().prefetch_related(test_prefetch)
  6. seriazlier = StageSerializer(instance = qs, many = True)
  7. print(serializer.data)

prefetch类接受的3个参数.
第一个参数必须是qs中model的合法属性.如果只填写第一个参数的话name和上面的给函数字符串调用,是同样的.
第二个参数是指定这个prefetch的查询集.
第三个参数是可以为这个结果指定一个新的属性名来做存储.

如果第一个参数传入的字符串不是Stage模型的某个属性,会报错.
如果第二个参数传入的查询集不是第一个参数指向的模型,

  • 但是仍然是与当前实例相关联的模型.例如

    1. test_prefetch = Prefetch(
    2. 'tasks',
    3. queryset=Flow.objects.filter(name='task'),
    4. to_attr='test_tasks')

    此时会分2种情况,

    1. 制定了to_attr的话.stage中的tasks属性正常返回全部值,stage中的test_tasks属性变为与此stage相关的flow列表.
    2. 未指定to_attr的话,stage中的tasks属性返回Task.objects.none()的空查询集.
  • 是一个毫无关系的模型,例如
    1. test_prefetch = Prefetch(
    2. 'tasks',
    3. queryset=Book.objects.all(),
    4. to_attr='test_tasks')
    此时会报错.
    根据报错信息.推断实现方法其实是在queryset参数的值上,加入filter(stage=current)的条件.而且未校验参数1与参数2指向的模型是否是同一个.

    prefetch_related之后的使用

    prefetch_related缓存属性之后,在serializermethodfield中也可以使用缓存的使用.
    1. class StageSerializer(serializers.ModelSerializer)
    2. task_names = seriallizers.SerializerMethodField(read_only=True) #自定义嵌套序列化器.如果字段名与模型中定义的关系名不同,同样需要source来指明关系.
    3. class Meta:
    4. model = Stage
    5. fields = '__all__'
    6. def get_task_names(self,obj)
    7. return [task.task_name for task in obj.test_tasks] # 此时test_tasks不再是queryset而是列表

    重要:prefetch_related之后不要做的操作

    不要调用会返回新queryset的api.如果有过滤需求,自己结合列表生成器和filter函数操作列表实现.(效率仍然远大于多次查询.)

    备注 请记住,与 QuerySets 一样,任何后续的链式方法,如果意味着不同的数据库查询,将忽略之前缓存的结果,并使用新的数据库查询来检索数据。所以,如果你写了以下内容:

  1. pizzas = Pizza.objects.prefetch_related('toppings')
  2. [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

…那么 pizza.toppings.all() 已经被预取的事实对你没有帮助。prefetch_related(‘toppings’) 意味着 pizza.toppings.all(),但 pizza.toppings.filter() 是一个新的、不同的查询。预设缓存在这里帮不上忙,事实上它损害了性能,因为你做了一个你没有使用过的数据库查询。所以要谨慎使用这个功能! 另外,如果你调用了 add()、remove()、clear() 或 set(),在 related managers 上,关系的任何预取缓存将被清除。