防止 m2m_changed 在创建对象时触发

Prevent m2m_changed from firing when creating an object

当使用像 post_save 这样的 Django 信号时,您可以通过执行以下操作来防止它在首次创建对象时触发:

@receiver(post_save,sender=MyModel)
def my_signal(sender, instance, created,**kwargs):
    if not created:
        pass # Do nothing, as the item is new.
    else:
        logger.INFO("The item changed - %s"%(instance) )

但是,多对多关系是在项目最初创建后应用的,so no such argument is passed in,因此在这些情况下很难抑制。

@receiver(m2m_changed,sender=MyModel.somerelation.though)
def my_signal(sender, instance, created,**kwargs):
    if __something__: # What goes here?
        pass # Do nothing, as the item is new.
    else:
        logger.INFO("The item changed - %s"%(instance) )

有没有一种简单的方法可以在对刚创建的对象执行 m2m_changed 信号时抑制它?

我认为没有简单的方法可以做到这一点。

正如 Django doc 所说,在项目被保存之前,您不能将其与关系相关联。文档中的示例:

>>> a1 = Article(headline='...')
>>> a1.publications.add(p1)
Traceback (most recent call last):
...
ValueError: 'Article' instance needs to have a primary key value before a many-to-many relationship can be used.

# should save Article first
>>> a1.save()
# the below statement never know it's just following a creation or not
>>> a1.publications.add(p1)

在没有外部信息的情况下,逻辑上不可能知道关系记录是添加到 "a just created item" 还是 "an item that already exists for some time"。

我想到的一些解决方法:

解决方法 1.在MyModel中添加一个DatetimeField来表示创建时间。 m2m_changed 处理程序使用创建时间来检查项目的创建时间。在某些情况下确实有效,但不能保证正确性

解决方案 2. 在 MyModel 中添加一个 'created' 属性,在 post_save 处理程序或其他代码中。示例:

@receiver(post_save, sender=Pizza)
def pizza_listener(sender, instance, created, **kwargs):
    instance.created = created

@receiver(m2m_changed, sender=Pizza.toppings.through)
def topping_listener(sender, instance, action, **kwargs):
    if action != 'post_add':
        # as example, only handle post_add action here
        return
    if getattr(instance, 'created', False):
        print 'toppings added to freshly created Pizza'
    else:
        print 'toppings added to modified Pizza'
    instance.created = False

演示:

p1 = Pizza.objects.create(name='Pizza1')
p1.toppings.add(Topping.objects.create())
>>> toppings added to freshly created Pizza
p1.toppings.add(Topping.objects.create())
>>> toppings added to modified Pizza

p2 = Pizza.objects.create(name='Pizza2')
p2.name = 'Pizza2-1'
p2.save()
p2.toppings.add(Topping.objects.create())
>>> toppings added to modified Pizza

但请谨慎使用此解决方案。由于 'created' 属性已分配给 Python 实例,未保存在数据库中,因此可能会出错:

p3 = Pizza.objects.create(name='Pizza3')
p3_1 = Pizza.objects.get(name='Pizza3')
p3_1.toppings.add(Topping.objects.create())
>>> toppings added to modified Pizza
p3.toppings.add(Topping.objects.create())
>>> toppings added to freshly created Pizza

这就是答案。然后,在这里抓住你!我是 github django-notifications 组的 zhang-z :)

@ZZY 的回答基本上帮助我意识到如果不存储额外的字段这是不可能的。幸运的是,我使用的是 django-model-utils which includes a TimeStampedModel,其中包含一个 created 字段。

提供足够小的增量,在捕获信号时检查创建时间相对容易。

@receiver(m2m_changed,sender=MyModel.somerelation.though)
def my_signal(sender, instance, created,**kwargs):
    if action in ['post_add','post_remove','post_clear']:
        created = instance.created >= timezone.now() - datetime.timedelta(seconds=5)
        if created:
            logger.INFO("The item changed - %s"%(instance) )

使用 _state.adding 属性可以更简单快捷地检查对象是否已创建:

def m2m_change_method(sender, **kwargs):
    instance = kwargs.pop('instance', None)
    if instance:
       if instance.adding: #created object
          pk_set = list(kwargs.pop('pk_set')) #ids of object added to m2m relation
       else:
          # do something if the instance not newly created or changed
    
  # if you want to check if the m2m objects is new use pk_set query if exists()
m2m_change.connect(m2m_change_method, sender=YourModel.many_to_many_field.through)