【问题标题】:How to use Django model inheritance with signals?如何使用带有信号的 Django 模型继承?
【发布时间】:2011-12-09 04:31:12
【问题描述】:

我在 Django 中有几个模型继承级别:

class WorkAttachment(models.Model):
    """ Abstract class that holds all fields that are required in each attachment """
    work            = models.ForeignKey(Work)
    added           = models.DateTimeField(default=datetime.datetime.now)
    views           = models.IntegerField(default=0)

    class Meta:
        abstract = True


class WorkAttachmentFileBased(WorkAttachment):
    """ Another base class, but for file based attachments """
    description     = models.CharField(max_length=500, blank=True)
    size            = models.IntegerField(verbose_name=_('size in bytes'))

    class Meta:
        abstract = True


class WorkAttachmentPicture(WorkAttachmentFileBased):
    """ Picture attached to work """
    image           = models.ImageField(upload_to='works/images', width_field='width', height_field='height')
    width           = models.IntegerField()
    height          = models.IntegerField()

WorkAttachmentFileBasedWorkAttachment 继承了许多不同的模型。我想创建一个信号,它会在创建附件时更新父工作的attachment_count 字段。认为为父发送者 (WorkAttachment) 发出的信号也适用于所有继承的模型是合乎逻辑的,但事实并非如此。这是我的代码:

@receiver(post_save, sender=WorkAttachment, dispatch_uid="att_post_save")
def update_attachment_count_on_save(sender, instance, **kwargs):
    """ Update file count for work when attachment was saved."""
    instance.work.attachment_count += 1
    instance.work.save()

有没有办法让这个信号适用于从WorkAttachment继承的所有模型?

Python 2.7、Django 1.4 pre-alpha

附:我试过one of the solutions I found on the net,但它对我不起作用。

【问题讨论】:

标签: python django django-signals


【解决方案1】:

除了@clwainwright 答案之外,我还配置了他的答案,改为适用于 m2m_changed 信号。我必须将其发布为代码格式的答案才有意义:

@classmethod
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for m2m_field in cls._meta.many_to_many:
            if hasattr(cls, m2m_field.attname) and hasattr(getattr(cls, m2m_field.attname), 'through'):
                models.signals.m2m_changed.connect(m2m_changed_receiver, weak=False, sender=getattr(cls, m2m_field.attname).through)

它会进行一些检查以确保在未来的 Django 版本中发生任何变化时它不会中断。

【讨论】:

    【解决方案2】:

    我只是使用 python 的(相对)新的__init_subclass__ method

    from django.db import models
    
    def perform_on_save(*args, **kw):
        print("Doing something important after saving.")
    
    class ParentClass(models.Model):
        class Meta:
            abstract = True
    
        @classmethod
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            models.signals.post_save.connect(perform_on_save, sender=cls)
    
    class MySubclass(ParentClass):
        pass  # signal automatically gets connected.
    

    这需要 django 2.1 和 python 3.6 或更高版本。请注意,使用 django 模型和相关元类时似乎需要 @classmethod 行,即使根据官方 python 文档不需要它。

    【讨论】:

    • 我是__init_subclass__
    • 非常好的解决方案
    • 这是一个很好的解决方案!我将其配置为连接到 m2m_changed 信号。我将在下面添加我的答案。
    • 这是完美的,因为它避免了多次触发 save()
    【解决方案3】:

    您可以在不指定sender 的情况下注册连接处理程序。并过滤其中需要的模型。

    from django.db.models.signals import post_save
    from django.dispatch import receiver
    
    
    @receiver(post_save)
    def my_handler(sender, **kwargs):
        # Returns false if 'sender' is NOT a subclass of AbstractModel
        if not issubclass(sender, AbstractModel):
           return
        ...
    

    参考:https://groups.google.com/d/msg/django-users/E_u9pHIkiI0/YgzA1p8XaSMJ

    【讨论】:

    • 这可行,但有一个小缺点,即每个调用 save() 的模型都会调用接收器函数。
    【解决方案4】:

    Michael Herrmann 的解决方案无疑是最符合 Django 的方法。 是的,它适用于所有子类,因为它们是在 ready() 调用中加载的。

    我想贡献文档参考:

    实际上,信号处理程序通常定义在与其相关的应用程序的信号子模块中。信号接收器连接在应用程序配置类的 ready() 方法中。如果您使用的是 receiver() 装饰器,只需在 ready() 中导入信号子模块。

    https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions

    并添加警告:

    在测试期间可能会多次执行 ready() 方法,因此您可能希望防止信号重复,尤其是当您计划在测试中发送它们时。

    https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions

    因此,您可能希望在连接函数上使用 dispatch_uid 参数来防止重复信号。

    post_save.connect(my_callback, dispatch_uid="my_unique_identifier")
    

    在这种情况下,我会这样做:

    for subclass in get_subclasses(WorkAttachment):
        post_save.connect(update_attachment_count_on_save, subclass, dispatch_uid=subclass.__name__)
    

    https://docs.djangoproject.com/en/dev/topics/signals/#preventing-duplicate-signals

    【讨论】:

      【解决方案5】:

      最简单的解决方案是不限制sender,而是在信号处理程序中检查各个实例是否是子类:

      @receiver(post_save)
      def update_attachment_count_on_save(sender, instance, **kwargs):
          if isinstance(instance, WorkAttachment):
              ...
      

      但是,这可能会导致显着的性能开销,因为每次保存任何模型时,都会调用上述函数。

      我想我找到了最符合 Django 的方法:最新版本的 Django 建议在名为 signals.py 的文件中连接信号处理程序。这是必要的接线代码:

      your_app/__init__.py:

      default_app_config = 'your_app.apps.YourAppConfig'
      

      your_app/apps.py:

      import django.apps
      
      class YourAppConfig(django.apps.AppConfig):
          name = 'your_app'
          def ready(self):
              import your_app.signals
      

      your_app/signals.py:

      def get_subclasses(cls):
          result = [cls]
          classes_to_inspect = [cls]
          while classes_to_inspect:
              class_to_inspect = classes_to_inspect.pop()
              for subclass in class_to_inspect.__subclasses__():
                  if subclass not in result:
                      result.append(subclass)
                      classes_to_inspect.append(subclass)
          return result
      
      def update_attachment_count_on_save(sender, instance, **kwargs):
          instance.work.attachment_count += 1
          instance.work.save()
      
      for subclass in get_subclasses(WorkAttachment):
          post_save.connect(update_attachment_count_on_save, subclass)
      

      认为这适用于所有子类,因为它们都将在调用 YourAppConfig.ready 时加载(因此导入 signals)。

      【讨论】:

      • 好答案。请注意,get_subclassed 中的 result 包含与此问题匹配的父类。如果你的父类是一个抽象模型,你会希望 result 最初是一个空列表。
      【解决方案6】:

      此解决方案解决了并非所有模块都导入内存时的问题。

      def inherited_receiver(signal, sender, **kwargs):
          """
          Decorator connect receivers and all receiver's subclasses to signals.
      
              @inherited_receiver(post_save, sender=MyModel)
              def signal_receiver(sender, **kwargs):
                  ...
      
          """
          parent_cls = sender
      
          def wrapper(func):
              def childs_receiver(sender, **kw):
                  """
                  the receiver detect that func will execute for child 
                  (and same parent) classes only.
                  """
                  child_cls = sender
                  if issubclass(child_cls, parent_cls):
                      func(sender=child_cls, **kw)
      
              signal.connect(childs_receiver, **kwargs)
              return childs_receiver
          return wrapper
      

      【讨论】:

      【解决方案7】:
      post_save.connect(my_handler, ParentClass)
      # connect all subclasses of base content item too
      for subclass in ParentClass.__subclasses__():
          post_save.connect(my_handler, subclass)
      

      祝你有美好的一天!

      【讨论】:

      • 您需要确保在定义了所有可能的子类之后运行它,否则它们将被跳过(不过,我还没有测试过这个断言,我认为这是会发生的)。
      【解决方案8】:

      还可以使用内容类型来发现子类 - 假设您将基类和子类打包在同一个应用程序中。这样的事情会起作用:

      from django.contrib.contenttypes.models import ContentType
      content_types = ContentType.objects.filter(app_label="your_app")
      for content_type in content_types:
          model = content_type.model_class()
          post_save.connect(update_attachment_count_on_save, sender=model)
      

      【讨论】:

        【解决方案9】:

        你可以试试这样的:

        model_classes = [WorkAttachment, WorkAttachmentFileBased, WorkAttachmentPicture, ...]
        
        def update_attachment_count_on_save(sender, instance, **kwargs):
            instance.work.attachment_count += 1
            instance.work.save()
        
        for model_class in model_classes:
            post_save.connect(update_attachment_count_on_save, 
                              sender=model_class, 
                              dispatch_uid="att_post_save_"+model_class.__name__)
        

        (免责声明:我没有测试过以上)

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-10-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-10-19
        相关资源
        最近更新 更多