DRF HyperlinkedRelatedField跨OneToOne关联报无属性错误
开发基于Django/DRF的业务系统时,设计了办公室(Office)、HR用户两类模型:HR用户通过OneToOneField与HRProfile模型关联,需求为支持将HR用户分配至指定办公室。当前在OfficeSerializer中通过HyperlinkedRelatedField配置关联对象查找逻辑时,始终无法跨关联访问User模型的email字段,抛出'HRProfile' object has no attribute 'related_user.email'异常。
注:直接在
HRProfile的__str__方法中调用self.related_user.email可正常获取值,排除模型关联本身的配置错误。
模型层代码
class User(AbstractBaseUser, PermissionsMixin): ... email = models.EmailField(unique=True) ... from polymorphic.models import PolymorphicModel class Profile(PolymorphicModel): ... related_user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, related_name="profile", ) ... class HRProfile(Profile): some_hr_specific_field = models.CharField() def __str__(self) -> str: return self.related_user.email class Office(models.Model): ... assigned_hrs = models.ManyToManyField( "users.HRProfile", related_name="offices", blank=True ) ...
视图层代码
class UserViewSet(GenericViewSet): serializer_class = UserSerializer queryset = User.objects.all() lookup_field = "email" lookup_url_kwarg = "email" lookup_value_regex = "[\\w@.]+" ... # OfficeViewSet仅配置了queryset和serializer两个必填参数
存在问题的序列化器代码
class OfficeSerializer(serializers.HyperlinkedModelSerializer): ... assigned_hrs = serializers.HyperlinkedRelatedField( queryset=HRProfile.objects.all(), view_name="api:user-detail", lookup_field="related_user.email", many=True, ) ...
- 配置参数
source='assigned_hrs.related_user', lookup_field='email' - 将
HyperlinkedRelatedField替换为SlugRelatedField测试,设置slug_field="related_user.email" - 使用双下划线替代点号分隔关联字段路径
当前仅当设置lookup_field='some_hr_specific_field'时字段可以正常解析,但该字段不符合业务需求。
DRF内置的RelatedField系列字段(包括HyperlinkedRelatedField、SlugRelatedField)的lookup_field、slug_field参数不支持跨关联关系的链式属性查找:框架会直接在当前序列化的模型实例(此处为HRProfile实例)上查找与参数值完全同名的属性,传入related_user.email时,框架会直接寻找名为related_user.email的属性,自然会抛出属性不存在的异常。__str__方法中self.related_user.email可以正常访问,是因为这是Python原生的链式属性调用逻辑,和DRF字段的参数解析逻辑完全无关,二者没有可对比性。
根据业务需求选以下两种方案即可:
方案1:保留超链接形式(指向User详情接口)
自定义支持链式属性查找的关联字段,重写属性获取、反序列化逻辑:
from rest_framework.relations import HyperlinkedRelatedField from rest_framework.exceptions import ValidationError from django.core.exceptions import ObjectDoesNotExist class ChainedLookupHyperlinkedField(HyperlinkedRelatedField): def get_attribute(self, instance): """序列化输出时,按点号分割路径逐层取属性值""" current_attr = instance for field_part in self.lookup_field.split('.'): try: current_attr = getattr(current_attr, field_part) except ObjectDoesNotExist: return None return current_attr def to_internal_value(self, data): """反序列化输入时,根据传入的email值匹配对应的HRProfile实例""" try: user = User.objects.get(**{self.lookup_url_kwarg: data}) # 适配django-polymorphic的多态查询,返回真实的HRProfile实例 return user.profile.get_real_instance() except (User.DoesNotExist, ObjectDoesNotExist): raise ValidationError(f"对应邮箱为{data}的HR用户不存在")
替换序列化器中原有字段配置即可:
assigned_hrs = ChainedLookupHyperlinkedField( queryset=HRProfile.objects.all(), view_name="api:user-detail", lookup_field="related_user.email", lookup_url_kwarg="email", many=True, )
方案2:仅需返回邮箱值、不需要超链接
直接使用ListField配合序列化、反序列化逻辑重写实现,代码更简单易维护:
class OfficeSerializer(serializers.HyperlinkedModelSerializer): assigned_hrs = serializers.ListField( child=serializers.EmailField(), required=False ) def to_representation(self, instance): rep = super().to_representation(instance) # 序列化输出时替换为HR的邮箱列表 rep['assigned_hrs'] = [hr.related_user.email for hr in instance.assigned_hrs.all()] return rep def create(self, validated_data): hr_emails = validated_data.pop('assigned_hrs', []) office = super().create(validated_data) # 根据邮箱列表匹配对应的HRProfile matched_hrs = HRProfile.objects.filter(related_user__email__in=hr_emails) office.assigned_hrs.set(matched_hrs) return office def update(self, instance, validated_data): hr_emails = validated_data.pop('assigned_hrs', None) office = super().update(instance, validated_data) if hr_emails is not None: matched_hrs = HRProfile.objects.filter(related_user__email__in=hr_emails) office.assigned_hrs.set(matched_hrs) return office
内容的提问来源于stack exchange,提问作者aSadLamp

