You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django2.2升级至4.2后AccessPolicy多对多字段报错求助

问题

将遗留项目从Django 2.2升级到Django 4.2后,仅AccessPolicy模型(包含ManyToMany字段)功能异常,报错信息:

'AccessPolicy' instance needs to have a primary key value before this relationship can be used.

相关代码片段:

模型代码(models.py)

class AccessPolicy(TimeStampedModel):
    name = models.CharField(max_length=255, null=False, blank=False)
    external_id = models.CharField(max_length=255, unique=True, null=True)
    version_hash = models.CharField(max_length=255, null=False, blank=False)
    read_only = models.BooleanField(default=False, help_text=('Some text'))
    users = models.ManyToManyField(User, blank=True)

    class Meta:
        verbose_name = 'Access Policy'
        verbose_name_plural = 'Access Policies'

    def __str__(self):
        return f'{self._meta.object_name} Object ({self.pk})'

表单代码(forms.py)

class AccessPolicyForm(forms.ModelForm):
    external_id = forms.CharField(disabled=True, required=False)
    rules = forms.CharField(widget=QueryBuilderWidget)

    class Meta:
        model = AccessPolicy
        fields = 'name', 'external_id', 'read_only', 'rules', 'users',

    def clean_external_id(self):
        external_id = self.cleaned_data.get('external_id')
        if not external_id:
            return md5(f"{str(self.instance.created)} {self.cleaned_data.get('name')} {self.data.get('rules')}")
        return external_id

    def clean_rules(self) -> CleanedRules:
        raw_rules: RawRules = json.loads(self.cleaned_data.get('rules'))
        conditional_serializer = RulesSerializer(data=raw_rules)
        if not conditional_serializer.is_valid():
            raise ValidationError(conditional_serializer.errors)
        return conditional_serializer.validated_data

    def save(self, commit=True):
        self.instance.version_hash = hash_dict(self.cleaned_data.get('rules'))
        return super().save(commit)

    def _save_m2m(self):
        self.instance.rules.all().delete()
        self._save_condition()
        self._save_users()

    def _save_users(self):
        self.instance.users.clear()
        self.instance.users.add(*self.cleaned_data.get('users'))

    def _save_condition(self):
        root_condition = RulesSerializer().create(self.cleaned_data.get('rules'))
        root_condition.access_policy = self.instance
        root_condition.save()

Admin代码(admin.py)

class AccessPolicyAdmin(admin.ModelAdmin):
    form = AccessPolicyForm
    list_display = 'name',
    search_fields = 'name',
    formfield_overrides = {
        models.ManyToManyField: {
            'widget': FilteredSelectMultiple(attrs={'size': 20},
                                             verbose_name='linked users',
                                             is_stacked=False)
        },
    }

    def get_form(self, request, obj: AccessPolicy = None, change=False, **kwargs):
        form = super().get_form(request, obj, change, **kwargs)
        data = self._get_form_data(obj or AccessPolicy())
        form.declared_fields[AUTHORIZATION_CONDITION_CHILDREN].initial = json.dumps(data)
        return form

    def _get_form_data(self, access_policy: AccessPolicy):
        return pipe(access_policy,
                    AccessRulesGenerator.get_root_rule,
                    RulesSerializer,
                    extract_data)

已知需先保存模型实例再操作ManyToMany字段,但修改save()和_save_m2m()后问题仍存在,需明确修改位置。


解决方案

问题根源是Django 4.2对模型实例保存与关联关系绑定的校验更严格,且自定义的_save_m2m和save方法逻辑冲突,具体修改如下:

1. 重构save方法,确保新建实例先获取主键

创建新AccessPolicy时,必须先将实例保存到数据库拿到主键,再处理关联关系:

def save(self, commit=True):
    self.instance.version_hash = hash_dict(self.cleaned_data.get('rules'))
    
    # 新建对象场景:先保存实例获取主键,再处理关联
    if not self.instance.pk and commit:
        # 先保存实例(不触发m2m)
        super().save(commit=False)
        self.instance.save()
        # 手动触发关联关系保存
        self._save_m2m()
        return self.instance
    
    # 编辑对象场景:正常执行父类逻辑
    return super().save(commit)

2. 修改_save_m2m方法,移除重复的ManyToMany处理

Django的ModelForm会自动处理users字段的保存,自定义的_save_users方法重复操作会导致冲突,同时确保_save_condition执行时实例已有主键:

def _save_m2m(self):
    # 先清理并重新保存rules关联
    self.instance.rules.all().delete()
    self._save_condition()
    
    # 移除自定义的users处理,交给Django自动完成
    # self._save_users()  # 删除此行

3. 给_save_condition添加主键校验(可选)

增加容错性,避免因实例未保存导致的报错:

def _save_condition(self):
    if not self.instance.pk:
        raise ValueError("AccessPolicy实例必须先保存才能创建规则关联")
    root_condition = RulesSerializer().create(self.cleaned_data.get('rules'))
    root_condition.access_policy = self.instance
    root_condition.save()

4. Admin代码无需修改

get_form中创建的未保存实例仅用于初始化表单数据,不会触发关联操作,保持原代码即可。


内容的提问来源于stack exchange,提问作者zacniewski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 05:34:52