ManyToMany关系的问题在保存后不会立即更新

问题描述 投票:17回答:6

当我保存它时(通过管理员)并且尝试在附加到post_save信号的函数内或相关save_modelAdminModel内使用新值时,我遇到了ManytoMany关系的问题,这些关系没有在模型中更新。我试图通过使用带有id的get函数重新加载这些函数中的对象,但它仍然具有旧值。

这是交易问题吗?交易结束时是否抛出信号?

谢谢,

django django-admin django-orm django-signals
6个回答
27
投票

当您通过管理表单保存模型时,它不是原子事务。首先保存主对象(以确保它具有PK),然后清除M2M并将新值设置为表单中出现的任何值。因此,如果您在主对象的save()中,那么您将处于尚未更新M2M的机会窗口中。实际上,如果您尝试对M2M执行某些操作,则clear()将会消除此更改。我大约一年前遇到过这个问题。

代码在ORM之前的重构日有所改变,但归结为django.db.models.fields.ManyRelatedObjectsDescriptorReverseManyRelatedObjectsDescriptor中的代码。查看他们的__set __()方法,你会看到manager.clear(); manager.add(*value) clear()完成清除该表中当前主对象的任何M2M引用。然后add()设置新值。

所以回答你的问题:是的,这是一个交易问题。

交易结束时是否抛出信号?没有官方的,但请继续阅读:

有一个related thread a few months ago和MonkeyPatching是一种方法提出。 Grégoire posted a MonkeyPatch为此。我没试过,但它看起来应该有用。


8
投票

当您尝试访问模型的post_save信号中的ManyToMany字段时,相关对象已被删除,并且在信号完成之前不会再次添加。

要访问此数据,您必须绑定到ModelAdmin中的save_related方法。不幸的是,您还必须在post_save信号中包含需要自定义的非管理员请求的代码。

见:https://docs.djangoproject.com/en/1.7/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_related

例:

# admin.py
Class GroupAdmin(admin.ModelAdmin):
    ...
    def save_related(self, request, form, formsets, change):
        super(GroupAdmin, self).save_related(request, form, formsets, change)
        # do something with the manytomany data from the admin
        form.instance.users.add(some_user)

然后在您的信号中,您可以进行与保存时要执行的相同更改:

# signals.py
@receiver(post_save, sender=Group)
def group_post_save(sender, instance, created, **kwargs):
    # do somethign with the manytomany data from non-admin
    instance.users.add(some_user)
    # note that instance.users.all() will be empty from the admin: []

5
投票

我有一个通用的解决方案似乎比猴子修补核心甚至使用芹菜更清洁(虽然我确信有人可以找到它失败的区域)。基本上我在admin中为具有m2m关系的表单添加了一个clean()方法,并将实例关系设置为cleaning_data版本。这使得实例的保存方法可以使用正确的数据,即使它还没有“在书上”。试一试,看看它是怎么回事:

def clean(self, *args, **kwargs):
    # ... actual cleaning here
    # then find the m2m fields and copy from cleaned_data to the instance
    for f in self.instance._meta.get_all_field_names():
        if f in self.cleaned_data:
            field = self.instance._meta.get_field_by_name(f)[0]
            if isinstance(field, ManyToManyField):
                setattr(self.instance,f,self.cleaned_data[f])

3
投票

http://gterzian.github.io/Django-Cookbook/signals/2013/09/07/manipulating-m2m-with-signals.html

问题:当您在post或pre_save信号接收器中操作模型的m2m时,您的更改将在Django随后的“清除”m2m中消失。

解决方案:在post或pre_save信号处理程序中,将另一个处理程序注册到要更新其m2m的模型的m2m中间模型上的m2m_changed信号。

请注意,第二个处理程序将接收几个m2m_changed信号,并且测试与它们一起传递的“action”参数的值是关键。

在第二个处理程序中,检查'post_clear'操作。当您收到带有post_clear动作的信号时,m2m已被Django清除,您有机会成功操作它。

一个例子:

def save_handler(sender, instance, *args, **kwargs):
    m2m_changed.connect(m2m_handler, sender=sender.m2mfield.through, weak=False)


def m2m_handler(sender, instance, action, *args, **kwargs):
    if action =='post_clear':
        succesfully_manipulate_m2m(instance)


pre_save.connect(save_handler, sender=YouModel, weak=False)

https://docs.djangoproject.com/en/1.5/ref/signals/#m2m-changed


0
投票

您可以在此主题中找到更多信息:qazxsw poi


0
投票

更新m2m的解决方案之一,以及更新您的一个模型。

Django manytomany signals?

首先,通过管理面板的所有请求都是原子的。你可以看看ModelAdmin:

Django 1.11 and higher

您在更新过程中可以观察到的行为,当您使用m2m记录进行的更改未保存时,即使您在保存方法中使用其中一个模型或信号进行更改,也只是因为m2m形式重写了主要文件之后的所有记录对象已更新。

这就是为什么,一步一步:

  1. 主要对象已更新。
  2. 您的代码(在保存方法或信号中)进行了更改(您可以查看它们,只需在ModelAdmin中放置一个断点):
@csrf_protect_m
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
    with transaction.atomic(using=router.db_for_write(self.model)):
        return self._changeform_view(request, object_id, form_url, extra_context)

@csrf_protect_m
def delete_view(self, request, object_id, extra_context=None):
    with transaction.atomic(using=router.db_for_write(self.model)):
        return self._delete_view(request, object_id, extra_context)
  1. form.save_m2m()获取放置在页面上的所有m2m值(粗略地说)并通过相关管理器替换所有m2m记录。这就是为什么你不能在交易结束时看到你的变化。

有一个解决方案:通过 def save_related(self, request, form, formsets, change): breakpoint() form.save_m2m() for formset in formsets: self.save_formset(request, form, formset, change=change) 使用m2m进行更改。 transaction.on_commit将在提交事务时在form.save_m2m()之后进行更改。

不幸的是,这个解决方案的缺点 - 您对m2m的更改将在单独的事务中执行。

© www.soinside.com 2019 - 2024. All rights reserved.