ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DRF ModelSerializer实战:原理、字段映射、校验与嵌套优化

DRF ModelSerializer实战:原理、字段映射、校验与嵌套优化 写Django接口没碰过DRF的序列化器几乎不可能。ModelSerializer是我在DRF里面用得最多的一个类没有之一。它解决的问题很直接你手上已经有一个跟数据库表强相关的数据结构要把它安全地进出接口如果是手写Serializer不仅要把字段声明重复一遍还得自己实现create、update字段一多就全是样板代码。ModelSerializer就是干这个的——通过一个Meta类声明把模型定义直接翻译成序列化字段。今天这篇文章不打算做成官方文档的翻译而是按我自己的理解把ModelSerializer从原理、配置、嵌套、校验到实战改造整个链路拆开讲一遍。无论你是刚接触DRF的新手还是已经在用但老觉得某些行为“反直觉”的老手这篇文章应该都能给你一些参考。1. ModelSerializer是什么先搞懂它解决什么问题1.1 序列化器在DRF里的角色先理清一个概念。DRF里的Serializer干的其实是两件事把Python对象变成JSON返回给前端这是序列化把前端传上来的JSON校验之后变成Python对象再落库这是反序列化。看似简单但围绕这两个动作会牵扯出字段校验、嵌套关系、只读只写、自定义逻辑等一系列问题。ModelSerializer是Serializer的子类但它额外做了一件关键的事读取你定义的模型字段自动生成对应的序列化字段。也就是说模型的CharField会变成序列化器的CharField模型的DateTimeField会变成DateTimeField外键会变成PrimaryKeyRelatedField多对多字段会变成带manyTrue的关系字段。你不再需要手动声明每个字段而是告诉它“这个序列化器针对哪个模型”就够了。1.2 ModelSerializer相比Serializer到底省了什么我用一个最简单的用户模型举例。from django.db import models class User(models.Model): username models.CharField(max_length50, uniqueTrue) email models.EmailField(uniqueTrue) password models.CharField(max_length128) avatar models.URLField(blankTrue) is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue)如果手写Serializer大概是这个样子class UserSerializer(serializers.Serializer): id serializers.IntegerField(read_onlyTrue) username serializers.CharField(max_length50) email serializers.EmailField() password serializers.CharField(max_length128, write_onlyTrue) avatar serializers.URLField(requiredFalse) is_active serializers.BooleanField(defaultTrue) created_at serializers.DateTimeField(read_onlyTrue) def create(self, validated_data): return User.objects.create(**validated_data) def update(self, instance, validated_data): instance.username validated_data.get(username, instance.username) instance.email validated_data.get(email, instance.email) instance.password validated_data.get(password, instance.password) instance.avatar validated_data.get(avatar, instance.avatar) instance.is_active validated_data.get(is_active, instance.is_active) instance.save() return instance再看ModelSerializer的写法class UserSerializer(serializers.ModelSerializer): class Meta: model User fields __all__看见区别了吗手写版本里create、update是必须自己实现的字段声明也一个都不能少模型里加一个字段Serializer就要同步加一行。ModelSerializer把这些全部自动化了默认的create就是Model.objects.create(**validated_data)默认的update就是逐个字段更新并save。不过“自动”不总是“正确”后面我会专门讲什么时候需要覆盖这两个方法。这里只是想说明ModelSerializer给你的并不是什么“魔法”而是把常规逻辑做了默认实现让你可以把精力放在真正需要定制的地方。对比项手写SerializerModelSerializer字段声明每个字段手动写从模型自动映射create/update手动实现默认实现字段校验规则手动声明继承模型约束代码量多易出错少直观定制灵活性高高但需要知道覆盖点1.3 自动映射背后的机制ModelSerializer之所以能自动生成字段核心在于它的Metaclass会遍历Meta.model的_meta.fields然后通过一张映射表把模型的Field类型转成序列化器Field类型。比如模型字段序列化器字段CharField / TextFieldCharFieldIntegerFieldIntegerFieldBooleanFieldBooleanFieldDateTimeField / DateFieldDateTimeField / DateFieldForeignKeyPrimaryKeyRelatedFieldManyToManyFieldPrimaryKeyRelatedField(manyTrue)FileField / ImageFieldFileField / ImageField这个映射不是死板的。模型字段上如果有blankTrue序列化字段的required会被设成False如果模型字段有default序列化字段也会拿到对应的默认逻辑如果模型字段有uniqueTrue序列化器还会在validators里自动加上UniqueValidator。换句话说模型的约束会尽可能传导到序列化层。注意模型的nullTrue主要影响数据库列是否允许为空序列化器的requiredFalse影响的是输入校验时是否必须传。这俩容易混很多新手在这里踩坑。比如模型中nullTrue是让你能存NULL但序列化器若不写requiredFalse前端少传这个字段照样报错。2. 用对基础配置才能少踩坑2.1 fields、exclude和__all__怎么选Meta里的fields是必填的或者用exclude替代再或者用__all__。我见过不少代码把fields漏了一启动就报AssertionError提示你没有定义字段。三种写法的区别说白了一句话你要显式告诉ModelSerializer哪些字段需要暴露。# 全部暴露省事但不一定安全 class UserSerializer(serializers.ModelSerializer): class Meta: model User fields __all__# 排除敏感字段剩余全要 class UserSerializer(serializers.ModelSerializer): class Meta: model User exclude [password]# 显式列出字段我最推荐的做法 class UserSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, email, avatar, is_active, created_at]为什么不推荐无脑用__all__因为它会把所有模型字段都暴露出去password这种字段一旦出现在序列化输出里就相当于把用户密码哈希直接丢给前端。就算哈希了也不该出现在接口返回里更别说有些模型里还有内部状态字段、软删除标记之类的东西。用exclude虽然能排除但它要求你先知道所有需要排除的字段对后来接手的人来说可读性也差。显式列出字段还有一个额外的好处字段列表本身就是接口文档的一部分。别人看你代码一眼就知道这个接口返回什么、接收什么不用去模型里数一遍字段。维护起来也直接新增字段需要明确决定“我要不要把它加进序列化器”而不是模型一加字段接口就自动多出个返回项。2.2 read_only_fields的边界在哪里只读字段在序列化器里是高频需求。id、created_at这类由系统生成的字段前端不该传也不能传就算传了也应当被忽略。在ModelSerializer里最省事的写法是在Meta里指定read_only_fieldsclass UserSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, email, password, avatar, is_active, created_at] read_only_fields [id, created_at, is_active]这里有个容易被忽略的细节read_only_fields只有在ModelSerializer里才支持手写Serializer根本不吃这个配置只能在字段声明里一个个加read_onlyTrue。这个限制其实挺合理的因为ModelSerializer知道模型字段和序列化字段的对应关系才能把名字翻译过去手写Serializer的字段声明是独立体系没法按模型字段名去匹配。那read_onlyTrue和requiredFalse有什么区别一个只读字段进入反序列化流程时前端就算传了值也会被忽略掉所以它天然就是“不必填”的状态你不需要再给它加requiredFalse。而requiredFalse只是不强制传但前端一旦传了值它就会参与校验和赋值。提示判断一个字段该不该设只读我的习惯是问一个问题——这个值是由服务端决定还是由客户端决定由服务端决定的主键、创建时间、当前登录用户、后端计算出来的状态一律read_only。由客户端决定的标题、内容、用户名、邮箱那就在输入校验里管好。还有一个不太容易察觉的坑read_only_fields对关联字段的名称解析依赖模型字段名。假如你的模型外键叫author但序列化器里有一个自定义字段叫author_info你把author_info写进read_only_fields是不生效的因为author_info不是模型的直接字段。这种自定义字段只能在声明时手动加read_onlyTrue。2.3 extra_kwargs把约束写进序列化器模型字段的约束会自动映射到序列化字段但这个映射往往不够用。比如模型里密码字段max_length128那是为了兼容哈希结果的长度而不是告诉你密码明文最长允许128字节比如某个字段模型里允许为空但接口上你要求必填再比如你想给某个字段定制错误提示文案。这些都需要extra_kwargs出场。class UserSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, email, password, avatar, is_active, created_at] extra_kwargs { password: { write_only: True, min_length: 8, error_messages: { min_length: 密码至少需要8位, required: 密码不能为空, }, }, email: { required: True, error_messages: { required: 邮箱地址不能为空, }, }, }extra_kwargs的键是模型字段名值是一个字典里面可以放任何序列化器Field支持的参数。write_onlyTrue就是让这个字段只在写入时校验序列化输出时不出现密码这种字段一定要这样处理。validators也能通过extra_kwargs传进去但我得提醒一句validators的用法和min_length这种参数不太一样。validators接收的是一个可调用对象的列表它会追加到序列化字段已有的validators里。如果你传的是UniqueValidator它还会因为字段的queryset上下文而产生额外的行为这时候要特别小心queryset是不是被正确传递了。extra_kwargs { username: { validators: [ validators.UniqueValidator(querysetUser.objects.all()) ], }, }上面这种写法容易出问题如果你在ModelSerializer里用了这个DRF会自动检测到字段已经有唯一性校验并且它自己也会基于模型生成一个。结果就是同一个字段在接口层被校验两次第二次数据库查询白白多出来。我的建议是模型上已有的uniqueTrue约束序列化器层不需要你再手动加UniqueValidator除非你有特殊的自定义逻辑。2.4 核心配置速查表拿我常用的几个配置项整理成一张表方便你写代码前对照。配置项作用使用场景fields指定序列化字段推荐显式列出exclude排除字段字段多时用来快速剔除敏感字段read_only_fields设置只读字段主键、时间戳、服务端生成值extra_kwargs为字段传额外参数隐藏密码、设置min/max、定制错误文案depth自动展开嵌套关联读取场景下简化嵌套序列化model绑定的模型必填项ordering / ordering_fields排序支持列表接口配合filter后端使用3. 嵌套关联字段的几种正确写法3.1 默认的外键映射方式PrimaryKeyRelatedField模型里一旦出现ForeignKey、ManyToManyFieldModelSerializer默认映射成PrimaryKeyRelatedField。表现就是序列化输出时返回关联对象的主键ID反序列化输入时接收一个主键ID。比如下面这个文章模型class Article(models.Model): title models.CharField(max_length200) content models.TextField() author models.ForeignKey(User, on_deletemodels.CASCADE, related_namearticles) created_at models.DateTimeField(auto_now_addTrue)默认的序列化器输出是这样的{ id: 1, title: DRF实战笔记, content: 正文内容, author: 3, created_at: 2024-01-15T10:30:00Z }前端拿到的author只是一个数字3。大多数场景下前端想知道作者叫什么名、头像是什么还得拿着id再调一次用户接口。这就是N1问题的接口层版本不仅多一次请求代码也啰嗦。所以在真实项目里我很少直接用默认外键映射作为输出。常见做法是下面几种按需求选用。3.2 用SlugRelatedField输出人类可读的值如果你希望外键在输出和输入时都用某个业务字段代替ID比如用户名、订单号、手机号SlugRelatedField是最好的选择。这里的slug不是说URL里的slug而是“作为标识符的某个字段”。class ArticleSerializer(serializers.ModelSerializer): author serializers.SlugRelatedField( slug_fieldusername, querysetUser.objects.all(), read_onlyTrue, ) class Meta: model Article fields [id, title, content, author, created_at]这样输出的author就是用户名前端拿到直接就能展示。如果还需要read_onlyTrue可以去掉queryset参数因为只读不需要校验输入的关联是否存在。另一种写法是用StringRelatedField它直接调用关联模型__str__的返回值连slug_field都不用指定。缺点是可控性弱__str__一旦改接口输出就跟着变多个接口如果对同一关联字段的展示要求不同这玩意就不太好使。我一般在调试阶段或者对输出要求较粗的场景用正式接口更倾向SlugRelatedField或自定义SerializerMethodField。3.3 用嵌套序列化器做完整对象输出如果你需要输出的是关联对象的完整信息而不是某一个字段那就直接在当前序列化器里引用另一个序列化器。DRF支持这种嵌套。class UserBriefSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, avatar] class ArticleSerializer(serializers.ModelSerializer): author UserBriefSerializer(read_onlyTrue) class Meta: model Article fields [id, title, content, author, created_at]注意这里author字段的声明方式它没有被包在Meta里而是作为Serializer的属性直接定义一个类变量。此时ModelSerializer的自动映射逻辑会优先使用这个显式声明的字段不会再把它当成PrimaryKeyRelatedField。read_onlyTrue是指这个嵌套对象在后端侧生成前端不传如果前端要传作者ID来创建文章这个字段就要改成PrimaryKeyRelatedField而不是嵌套Serializer。嵌套序列化有个天然的双向问题如果UserSerializer里又嵌套了ArticleSerializer而ArticleSerializer里又嵌套了UserSerializer就会形成循环引用Python直接报NameError。解决办法有三个其中一个方向不嵌套、使用depth避开手写循环、或者把其中一个序列化器定义到另一个的SerializerMethodField内部。我推荐最简单的是只做单向嵌套文章看作者、作者详情里不嵌文章列表接口设计本身也更清晰。3.4 depth最省事的嵌套利器也是性能陷阱ModelSerializer支持depth配置比如depth 1它会自动把外键展开成嵌套对象不需要你手写任何嵌套序列化器。class ArticleSerializer(serializers.ModelSerializer): class Meta: model Article fields [id, title, content, author, created_at] depth 1输出就变成了{ id: 1, title: DRF实战笔记, content: 正文内容, author: { id: 3, username: 张三, email: zhangsanexample.com }, created_at: 2024-01-15T10:30:00Z }depth默认的值是0表示不展开。越大展开层级越深。听起来很省事但有两个问题必须记住。一是字段不可控。depth1展开用户对象时会把它所有字段都带出来包括你可能不想暴露的字段。二是性能隐患。展开关联对象意味着DRF要去查询关联表数据如果没有正确的select_related数据库会被打爆出现典型的N1查询。提示我在实际项目中很少用depth宁可手写UserBriefSerializer做嵌套。原因很简单嵌套序列化器可以精确控制输出字段、可以按不同接口定制不同的展示层depth虽然快但不好控制。3.5 SerializerMethodField最灵活的展示方式有些字段既不是模型字段也不是普通关联展示而是经过计算的统计数量、拼接字符串、从关联表里取最新一条数据。这时候用SerializerMethodField。class ArticleListSerializer(serializers.ModelSerializer): comment_count serializers.IntegerField(sourcecomments.count, read_onlyTrue) author_name serializers.SerializerMethodField() class Meta: model Article fields [id, title, author_name, comment_count, created_at] def get_author_name(self, obj): return obj.author.username if obj.author else 这里的SerialzerMethodField要和get_字段名方法一一对应。方法接收一个obj参数obj是当前序列化的模型实例你在这个方法里可以任意读取关联数据、做计算、甚至再读一次数据库。用source参数直接指定字段来源也是很常见的技巧比如上面的comment_count我直接用sourcecomments.countDRF会去调用obj.comments.count()比SerializerMethodField写起来还少几行。但要留意sourcecomments.count每次都会执行一次COUNT查询如果文章列表有100篇就是100次COUNT性能上要配合查询优化考虑。4. 校验逻辑防止脏数据混进系统4.1 模型校验与序列化器校验的分工Django模型自带full_clean校验但DRF默认不会调用模型的full_clean。这意味着模型层的一些自定义约束例如某个字段不能等于另一个字段或者某个字段需要满足自定义的条件在序列化器里不会自动生效。理解这个分工很重要模型校验负责数据库层的合法性序列化器校验负责接口层的合法性。你的接口如果只依赖模型校验很多情况下等于没有校验。ModelSerializer会自动把模型字段的max_length、null、unique这些声明映射成序列化器的内置校验所以这些约束不用你重复写。但是模型里如果用validators写了自定义校验函数DRF默认也会把它纳入序列化器的validators里。这块行为在不同版本有些差异所以我建议自定义的复杂校验不要依赖模型自动传导直接在序列化器里显式写行为可控。4.2 单字段校验validate_字段名最简单的自定义校验是定义validate_字段名(self, value)方法。它接收一个参数就是当前字段的值返回校验后的值如果校验不过就抛serializers.ValidationError。class RegisterSerializer(serializers.ModelSerializer): confirm_password serializers.CharField(write_onlyTrue) class Meta: model User fields [id, username, email, password, confirm_password, created_at] extra_kwargs { password: {write_only: True, min_length: 8}, } def validate_username(self, value): if in value: raise serializers.ValidationError(用户名不能包含空格) return value这里定义了一个用户注册场景。confirm_password是额外的写字段它不属于模型所以在fields里显式列出来ModelSerializer会自动把它声明为普通CharField。注意这个字段不会映射到模型因此创建时它会被单独提取不进入create的validated_data。单字段校验的执行顺序早于反序列化的整体校验所以在这个阶段你能拿到的是单独字段的值不能依赖其他字段。如果你要根据多个字段联合判断就要走到下一步。4.3 多字段联合校验validate()validate(self, attrs)方法接收的是整个校验后的字段字典。这里你可以同时拿到多个字段的值适合做“两次密码一致”、“开始时间早于结束时间”这类逻辑。def validate(self, attrs): password attrs.get(password) confirm_password attrs.pop(confirm_password, None) if password ! confirm_password: raise serializers.ValidationError({confirm_password: 两次输入的密码不一致}) return attrs这个例子演示了一个重要技巧confirm_password这种校验完就该丢掉的字段在validate里直接pop掉避免它进入后面的create流程。如果不pop默认的create会尝试把它当成User模型的字段创建直接报TypeError。ValidationError可以传字符串也可以传字典。传字典时错误信息会挂到对应字段上前端可以拿到字段级别的报错提示传字符串时错误挂在非字段错误non_field_errors里。我一般建议用字典前端处理起来更直观。4.4 validators可复用的校验函数如果同一套校验逻辑在多个序列化器里都要用那就提取成一个独立的函数或类。def validate_password_strength(value): if not any(char.isdigit() for char in value): raise serializers.ValidationError(密码必须包含数字) if not any(char.isalpha() for char in value): raise serializers.ValidationError(密码必须包含字母) return value class UserSerializer(serializers.ModelSerializer): password serializers.CharField( write_onlyTrue, validators[validate_password_strength], ) class Meta: model User fields [id, username, email, password, created_at]注意这里validators是加在显式声明的字段上的。如果你只想通过extra_kwargs传也不冲突但可读性差一些。函数校验的好处是容易被单元测试覆盖而且多个接口复用起来非常顺手。实操心得校验逻辑放序列化器而不是View里。我刚用DRF时喜欢在View里写一堆if判断后来发现序列化器报错信息根本传不到前端只有400状态码。把校验下沉到序列化器之后错误数据结构统一了、代码也瘦身了。记住View只负责拿数据、调序列化器、返回响应业务校验一律进序列化器。5. 实战改造从默认序列化器到可用的业务代码5.1 最经典的场景注册接口的序列化器改造默认的ModelSerializer在创建用户时存在一个大坑它会把明文密码直接存进数据库而且不经过哈希。这是初学者非常容易犯的错误也是实战中必须覆盖create方法的典型场景。import hashlib class RegisterSerializer(serializers.ModelSerializer): confirm_password serializers.CharField(write_onlyTrue) class Meta: model User fields [id, username, email, password, confirm_password, created_at] extra_kwargs { password: {write_only: True, min_length: 8}, } def validate(self, attrs): confirm_password attrs.pop(confirm_password, None) if attrs.get(password) ! confirm_password: raise serializers.ValidationError({confirm_password: 两次输入的密码不一致}) return attrs def create(self, validated_data): password validated_data.pop(password) user User(**validated_data) user.set_password(password) user.save() return user这里create方法做的是从校验后的数据里取出密码用Django自带的set_password做哈希再创建用户。这样User表中的password字段存的是哈希值而不是明文。覆盖create之后序列化器返回的对象就是创建好的user实例。在View里这样用from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .serializers import RegisterSerializer class RegisterView(APIView): def post(self, request): serializer RegisterSerializer(datarequest.data) serializer.is_valid(raise_exceptionTrue) serializer.save() return Response( {id: serializer.instance.id, username: serializer.instance.username}, statusstatus.HTTP_201_CREATED, )is_valid(raise_exceptionTrue)是DRF里很推荐的一种写法校验失败直接抛出异常由DRF的异常处理器统一返回400响应错误信息自动带上每个字段的报错。这样你就不用在自己的View里手写错误响应了。5.2 覆盖update方法处理需要特殊逻辑的更新默认的update实现是遍历validated_data逐个setattr然后save。多数情况下够用但遇到外键关联的创建、多对多关系维护就得自己动手了。看这个评论发布场景class Comment(models.Model): article models.ForeignKey(Article, on_deletemodels.CASCADE, related_namecomments) user models.ForeignKey(User, on_deletemodels.CASCADE) content models.TextField() created_at models.DateTimeField(auto_now_addTrue)如果接口希望前端传article_id来发布评论而当前登录用户从request.user取可以这么写class CommentCreateSerializer(serializers.ModelSerializer): article serializers.PrimaryKeyRelatedField(querysetArticle.objects.all()) class Meta: model Comment fields [id, article, content, created_at] def validate_article(self, value): if not value.is_published: raise serializers.ValidationError(文章未发布不能评论) return value def create(self, validated_data): user self.context[request].user return Comment.objects.create(useruser, **validated_data)这里有一个重点create方法里通过self.context[request]拿到了当前请求的用户。context是序列化器内部的一个字典View在实例化序列化器时会自动注入request、view、format等键。你不需要自己传只要在View里用CommentCreateSerializer(datarequest.data)这种正常实例化方式context就天然包含request。校验和创建的链路已经很清晰了前端传article_id和content序列化器校验文章是否存在和可评论创建时自动绑定登录用户。这个模式在写“当前用户创建自己的资源”类接口时非常常见。5.3 控制序列化输出的字段视图很多时候同一个模型在不同接口里要展示不同的字段集合。列表页只要标题和作者名详情页要完整正文和创建时间用户自己的资料接口要邮箱和头像别人看他资料时邮箱要隐藏。ModelSerializer允许你为同一个模型定义多个序列化器每个序列化器专注一种输出视图。我最常用的做法是一个基础序列化器几个继承它的子序列化器子类只修改Meta.fields和追加字段。class ArticleBaseSerializer(serializers.ModelSerializer): class Meta: model Article fields [id, title, content, created_at] class ArticleListSerializer(ArticleBaseSerializer): author_name serializers.CharField(sourceauthor.username, read_onlyTrue) comment_count serializers.IntegerField(sourcecomments.count, read_onlyTrue) class Meta(ArticleBaseSerializer.Meta): fields [id, title, author_name, comment_count, created_at] class ArticleDetailSerializer(ArticleBaseSerializer): author UserBriefSerializer(read_onlyTrue) class Meta(ArticleBaseSerializer.Meta): fields [id, title, content, author, created_at]这样做的好处不用多说列表接口轻量详情接口完整每个接口返回的数据都是明确的。不需要一个序列化器里堆一堆SerializerMethodField然后靠View里删字段来控制输出。5.4 在序列化器里判断请求类型动态调整字段还有一类场景同一个序列化器在创建和更新时行为不同。比如用户名创建时必填更新时可选密码创建时必填更新时可选。实现这个效果有个经典手法在初始化时根据self.context或者操作类型调整字段的required属性。class UserUpdateSerializer(UserSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance is None: # 创建场景username 必填password 必填 self.fields[username].required True self.fields[password].required True else: # 更新场景两个字段都选填 self.fields[username].required False self.fields[password].required False判断self.instance是否为None就能区分是创建还是更新创建时instance为None更新时instance是已有的模型实例。当然如果逻辑更复杂我还是建议直接拆成两个序列化器毕竟一个序列化器里塞太多分支时间长了连自己都会看晕。5.5 性能优化别让序列化器引发N1ModelSerializer在输出嵌套字段时不会自动帮你优化数据库查询。如果一个列表返回50篇文章每篇文章都要查一次作者那就是51条SQL。对比一下这个数字如果你不做优化这个接口的数据库压力是非常难看的。解决方案不是改序列化器而是在View的查询集里做预加载。DRF的ListAPIView允许重写get_querysetclass ArticleListView(ListAPIView): serializer_class ArticleListSerializer def get_queryset(self): return Article.objects.select_related(author).prefetch_related(comments)select_related适用于外键这种单值关联prefetch_related适用于多对多、反向外键这种集合关联。配合前面的sourcecomments.countprefetch_related(comments)可以把COUNT查询也合并优化但要注意count()在prefetch之后依然会对每个对象执行聚合并不会自动缓存。如果需要极致优化可以用annotate在QuerySet层面一次性把数量算好。from django.db.models import Count class ArticleListView(ListAPIView): serializer_class ArticleListSerializer def get_queryset(self): return Article.objects.select_related(author).annotate(comment_countCount(comments))然后用IntegerField(read_onlyTrue)接收这个注解字段接口的SQL数量直接从几十条降到两三条。序列化器本身不背性能的锅但理解序列化器怎么消耗查询才能真正优化到位。6. 常见问题与排查技巧实录6.1 使用ModelSerializer时最容易踩的坑先说一个新人必踩的创建带外键的嵌套数据时默认的create只支持扁平数据。如果你前端传的是嵌套JSON希望一次性创建主表和关联表默认实现做不到。DRF会直接报错或者只创建主表数据。解决办法就是在create里手动处理嵌套结构先pop出嵌套字段创建主表再逐一创建或更新关联数据。再说一个看起来很冤的报错TypeError: Field object is not callable。class UserSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, email, password, created_at] read_only_fields [created_at]这代码看着没问题吧但如果你的模型里恰好有个字段叫created_at而你在Meta的read_only_fields里把它列为只读某些版本下DRF会尝试对created_at字段本身调用__call__引发上面的错。这类问题的排查思路就是检查有没有字段名和模型元信息重复尤其是Meta类里的配置键写错。还有个很典型的AssertionError: The model is not valid。这个一般是你把Meta.model写成了字符串而不是模型类或者模型类没有被正确导入。模型类必须是类对象不能是字符串。AttributeError也常见。定义了一个SerializerMethodField的get_xxx方法但方法名和字段名对不上DRF会报找不到对应方法。或者source参数指向的模型字段不存在也会AttributeError。6.2 实战中遇到的调试思路序列化器出问题第一个动作永远是看data和errors。is_valid()返回False时别急着改代码先在errors里看具体报错字段。serializer UserSerializer(datarequest.data) if not serializer.is_valid(): print(serializer.errors)很多时候你会发现报错并不是逻辑问题而是字段的required、write_only配置不对。比如前端传了password但你没设write_onlyTrue校验通过后密码又会出现在序列化输出这属于配置层面问题而不是模型问题。如果接口返回了500常见原因是序列化器输出时某个字段访问了不存在的属性。例如嵌套序列化器里的source写成了author.username但查询集没有被select_relatedDRF照样能查出来但如果你在SerializerMethodField里写了obj.author.username必须保证obj.author已经加载。没加载时Django会额外查询一次不算报错但却是N1的起点。6.3 容易忽略的小经验经验一永远不要拿默认的ModelSerializer直接做创建类接口。至少过一遍字段列表看看有没有不该暴露的字段、有没有需要隐藏的字段。我见过生产环境接口直接把用户密码哈希返回给前端的案例就是因为开发省事用了fields __all__。经验二ModelSerializer的Meta.fields里写不存在的字段会直接报ImproperlyConfigured这其实是好事能帮你尽早发现模型和序列化器不同步的问题。别用__all__绕过这种检查。经验三requiredFalse和default不要写重。如果字段配置了默认值前端不传时就用默认值两个同时出现有时会产生预期外的行为。建议二选一。经验四序列化器的create和update方法与模型表单的save殊途同归都是把校验后的数据落到库里。因此但凡你需要在落库前“加工”数据都放在这里干别放在View里。把数据加工逻辑留在View会导致其他接口要复用同一套序列化器时还得把那套加工逻辑再复制一遍。经验五多序列化器协同输出时注意给每个序列化器单独命名别都用Serializer结尾。项目大了以后ArticleSerializer和ArticleCreateSerializer混在一起看代码真的会晕。我在项目里统一用ArticleDetailSerializer、ArticleCreateSerializer、UserBriefSerializer这种命名一眼就知道用途。经验六调试嵌套序列化器输出时直接在Python shell里手动实例化看结果比发请求调接口快得多from myapp.serializers import ArticleListSerializer from myapp.models import Article article Article.objects.select_related(author).first() serializer ArticleListSerializer(article) print(serializer.data)这个方法能快速验证字段配置是否生效而且不依赖前端。ModelSerializer最难的地方从来不是语法而是搞清楚每个字段在这个接口里的定位。是输入、输出还是校验边界是只读、必填还是选填是自己手写的字段还是从模型自动映射出来的字段。把这几个维度想清楚用起来基本上就顺了。我自己最常用的节奏是先按接口需求列出字段清单再定义Meta里的fields和extra_kwargs然后补充校验和create/update覆盖最后用shell验证输出。这套流程走下来ModelSerializer基本不会出什么幺蛾子希望这篇内容也能让你的DRF开发少走点弯路。
返回列表