使用TabularInline与代理模型时触发RelatedObjectDoesNotExist错误
我用Django实现了包含Service基类和三个代理子类的模型结构,代码如下:
class Service(models.Model): SERVICE_TYPE_CHOICES = [ ('AR', 'Area'), ('OC', 'Occupancy'), ('CO', 'Consumption'), ] service_type = models.CharField(max_length=2, choices=SERVICE_TYPE_CHOICES) building = models.ForeignKey(Building, on_delete=models.CASCADE) def __init__(self, *args, **kwargs): super(HouseService, self).__init__(*args, **kwargs) subclasses = { 'AR' : AreaService, 'OC' : OccupancyService, 'CO' : ConsumptionService, } self.__class__ = subclasses[self.service_type] class AreaServiceManager(models.Manager): def get_queryset(self): return super(AreaServiceManager, self).get_queryset().filter(service_type='AR') def create(self, **kwargs): kwargs.update({'service_type': 'AR'}) return super(AreaServiceManager, self).create(**kwargs) class AreaService(Service): objects = AreaServiceManager() def save(self, *args, **kwargs): self.service_type = 'AR' return super(AreaService, self).save(*args, **kwargs) class Meta: proxy = True # 另外两个代理模型的Manager和Model定义已省略
正常创建实例、通过AreaService.objects等查询都能返回对应子类实例,Service.objects.all()也会返回子类实例列表。但在admin中添加TabularInline后:
class ServiceInlineAdmin(admin.TabularInline): model = Service extra = 0 class BuildingAdmin(admin.ModelAdmin): inlines = [ServiceInlineAdmin,]
触发错误:RelatedObjectDoesNotExist: Service has no building,错误指向models.py里的self.__class__ = subclasses[self.service_type]行。数据库中所有Service实例都关联了Building,注释掉自定义的__init__函数能解决问题,但会失去子类功能,求指点错误原因。
错误根源:Django Admin处理Inline时,会先创建空的
Service实例用于渲染表单(比如添加新记录的空表单)。这个空实例的service_type字段为空,同时building关联还未绑定(Inline的关联Building是当前编辑对象,但空实例还未完成绑定)。执行__init__里的类切换逻辑时,首先self.service_type为空找不到对应子类;其次,即使有service_type值,切换类后会打乱Django ORM关联字段的初始化逻辑,导致building属性无法正确加载,触发RelatedObjectDoesNotExist错误。解决思路:放弃在
__init__中直接切换类,改用Django提供的model_from_db钩子,仅在从数据库加载已有实例时动态切换到代理子类,这样不会影响Admin创建空实例的流程。
修改后的Service基类代码:
class Service(models.Model): SERVICE_TYPE_CHOICES = [ ('AR', 'Area'), ('OC', 'Occupancy'), ('CO', 'Consumption'), ] service_type = models.CharField(max_length=2, choices=SERVICE_TYPE_CHOICES) building = models.ForeignKey(Building, on_delete=models.CASCADE) @classmethod def model_from_db(cls, db, field_names, values): # 从values中获取service_type字段的值 service_type_index = field_names.index('service_type') service_type = values[service_type_index] subclasses = { 'AR': AreaService, 'OC': OccupancyService, 'CO': ConsumptionService, } # 切换到对应的代理子类,默认用基类 model_class = subclasses.get(service_type, cls) instance = model_class(*values) instance._state.adding = False instance._state.db = db return instance # 其他代码保持不变
- 有效性说明:
model_from_db仅在从数据库读取已有实例时触发,Admin渲染空表单时不会执行该方法,避免了空实例切换类的问题。同时,该方法能保证从数据库读取的实例自动转换为对应代理子类,保留原有查询返回子类实例的功能。
内容的提问来源于stack exchange,提问作者janeden

