如何在 Django 注册时同时创建用户和角色资料?
结论
可以在一次请求中创建用户及其资料。应由一个明确的流程负责创建资料:可以在自定义注册序列化器的 create() 中创建,也可以由信号创建空资料,再由序列化器更新。避免信号和注册序列化器同时创建同一条资料记录。
对于需要接收嵌套资料字段的注册接口,把校验和保存统一放在注册序列化器中通常更直接。将用户和资料的写入放在同一个数据库事务中,可以避免用户创建成功、资料保存失败后留下不完整记录。
精简注册载荷
user 是由注册流程确定的关联对象,客户端不应传入。嵌套资料序列化器应排除该字段,或将其设为只读;保存资料时,由服务端传入刚创建的用户。
当前示例的 role 是 admin,但请求中同时提交了教师和学生资料。接口应明确每个角色允许提交哪些资料,并拒绝角色不匹配或同时提交多种角色资料的请求。
在自定义序列化器中创建资料
以下示例假设 User.TEACHER、User.STUDENT 是项目中定义的角色值,请替换为实际使用的常量。资料序列化器不应包含可写的 user 字段。
from django.db import transaction
from rest_framework import serializers
class CustomUserCreateSerializer(UserCreateSerializer):
teacher_profile = TeacherProfileSerializer(required=False)
student_profile = StudentProfileSerializer(required=False)
class Meta(UserCreateSerializer.Meta):
model = User
fields = (
"id",
"email",
"password",
"role",
"first_name",
"last_name",
"teacher_profile",
"student_profile",
)
def validate(self, attrs):
attrs = super().validate(attrs)
role = attrs.get("role")
teacher_data = attrs.get("teacher_profile")
student_data = attrs.get("student_profile")
if teacher_data and student_data:
raise serializers.ValidationError(
"一次注册只能提交一种角色资料。"
)
if teacher_data and role != User.TEACHER:
raise serializers.ValidationError(
{"teacher_profile": "该资料与用户角色不匹配。"}
)
if student_data and role != User.STUDENT:
raise serializers.ValidationError(
{"student_profile": "该资料与用户角色不匹配。"}
)
return attrs
@transaction.atomic
def create(self, validated_data):
teacher_data = validated_data.pop("teacher_profile", None)
student_data = validated_data.pop("student_profile", None)
user = super().create(validated_data)
if user.role == User.TEACHER and teacher_data is not None:
TeacherProfile.objects.create(user=user, **teacher_data)
elif user.role == User.STUDENT and student_data is not None:
StudentProfile.objects.create(user=user, **student_data)
return user
DRF 会先校验声明的嵌套序列化器,再调用 create()。示例中直接创建对应资料,并由服务端设置 user。如果创建用户或写入资料时抛出异常,transaction.atomic 会回滚整个事务。
如果业务要求每个用户都必须有一条资料记录,可以在同一个事务中创建空资料;如果资料是可选的,则像示例一样,只在收到相应嵌套数据时创建。
处理 djoser 的密码确认序列化器
如果项目使用密码确认注册序列化器,也需要让它采用相同的嵌套字段、角色校验和资料创建逻辑。可以抽出公共基类或 mixin,避免两份 create() 逻辑逐渐不一致。保留对应 djoser 父类的密码处理逻辑,并确认自定义序列化器已按项目所用版本配置到 djoser 的用户创建序列化器设置中。
信号与文件上传
如果继续使用 post_save 信号创建资料,就不要再由注册序列化器创建一次。可以让信号创建空资料,再由注册流程更新,但这样会增加信号与注册逻辑之间的耦合。无论采用哪种方式,都应在事务中写入用户和资料,并确保每个用户的资料只创建一次。
如果 profile_picture 是图片文件字段,JSON 中的 "string" 只是字符串,并不等于上传文件。接口需要使用支持文件上传的请求格式(通常是 multipart/form-data),并确认当前解析器和序列化器能够接收嵌套字段。若客户端不便提交嵌套文件,可以先完成注册,再单独上传图片。
备注:内容仅供参考。
