日常业务中傲娇(landuo)的前端经常会要求额外序列化一些数据.drf提供了方便的嵌套序列化方案,尤其对于酷爱使用serializermethodfield的我来说嵌套序列化更是超级日常的东西.
嵌套序列化的使用
这里仍然使用https://www.yuque.com/yuqueyonghuctfbfj/ddeyfu/tf21g9这个文档中建立的三级简单外键模型.
class TaskSerializer(serializers.ModelSerializer)
class Meta:
model = Task
fields = '__all__'
class StageSerializer(serializers.ModelSerializer)
tasks = TaskSerializer(many=True) #自定义嵌套序列化器.如果字段名与模型中定义的关系名不同,同样需要source来指明关系.
class Meta:
model = Stage
fields = '__all__'
qs = Stage.objects.all()
seriazlier = StageSerializer(instance = qs, many = True)
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有关,文档上说的很详细。
qs = Stage.objects.all().prefetch_related('tasks')# 参数为relatedname或者外键名,多级关系支持django的__语法。例如stage__flow.这种情况下查询会变成3条,同时查询stage和flow的信息。
seriazlier = StageSerializer(instance = qs, many = True)
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.
test_prefetch = Prefetch(
'tasks',
queryset=Task.objects.filter(name='task'),
to_attr='test_tasks')
qs = Stage.objects.all().prefetch_related(test_prefetch)
seriazlier = StageSerializer(instance = qs, many = True)
print(serializer.data)
prefetch类接受的3个参数.
第一个参数必须是qs中model的合法属性.如果只填写第一个参数的话name和上面的给函数字符串调用,是同样的.
第二个参数是指定这个prefetch的查询集.
第三个参数是可以为这个结果指定一个新的属性名来做存储.
如果第一个参数传入的字符串不是Stage模型的某个属性,会报错.
如果第二个参数传入的查询集不是第一个参数指向的模型,
但是仍然是与当前实例相关联的模型.例如
test_prefetch = Prefetch(
'tasks',
queryset=Flow.objects.filter(name='task'),
to_attr='test_tasks')
此时会分2种情况,
- 制定了to_attr的话.stage中的tasks属性正常返回全部值,stage中的test_tasks属性变为与此stage相关的flow列表.
- 未指定to_attr的话,stage中的tasks属性返回Task.objects.none()的空查询集.
- 是一个毫无关系的模型,例如
此时会报错.test_prefetch = Prefetch(
'tasks',
queryset=Book.objects.all(),
to_attr='test_tasks')
根据报错信息.推断实现方法其实是在queryset参数的值上,加入filter(stage=current)的条件.而且未校验参数1与参数2指向的模型是否是同一个.prefetch_related之后的使用
prefetch_related缓存属性之后,在serializermethodfield中也可以使用缓存的使用.class StageSerializer(serializers.ModelSerializer)
task_names = seriallizers.SerializerMethodField(read_only=True) #自定义嵌套序列化器.如果字段名与模型中定义的关系名不同,同样需要source来指明关系.
class Meta:
model = Stage
fields = '__all__'
def get_task_names(self,obj)
return [task.task_name for task in obj.test_tasks] # 此时test_tasks不再是queryset而是列表
重要:prefetch_related之后不要做的操作
不要调用会返回新queryset的api.如果有过滤需求,自己结合列表生成器和filter函数操作列表实现.(效率仍然远大于多次查询.)备注 请记住,与 QuerySets 一样,任何后续的链式方法,如果意味着不同的数据库查询,将忽略之前缓存的结果,并使用新的数据库查询来检索数据。所以,如果你写了以下内容:
pizzas = Pizza.objects.prefetch_related('toppings')
[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 上,关系的任何预取缓存将被清除。