【问题标题】:Generic many-to-many relationships通用多对多关系
【发布时间】:2010-10-30 07:19:37
【问题描述】:

我正在尝试创建一个消息系统,其中消息的发件人和收件人可以是通用实体。这对于发件人来说似乎很好,只有对象可以引用(GenericForeignKey),但我不知道如何为收件人解决这个问题(GenericManyToManyKey ??)

下面是一个简化的例子。 PersonClient 和 CompanyClient 从 Client 继承属性,但有自己的特定细节。最后一行是症结所在。如何让消息接收者成为一组 CompanyClients 和 PersonClients

  class Client(models.Model):
      city = models.CharField(max_length=16)

      class Meta:
          abstract = True

  class PersonClient(Client):
      first_name = models.CharField(max_length=16)
      last_name = models.CharField(max_length=16)
      gender = models.CharField(max_length=1)

  class CompanyClient(Client):
      name = models.CharField(max_length=32)
      tax_no = PositiveIntegerField()

  class Message(models.Model):
      msg_body = models.CharField(max_length=1024)
      sender = models.ForeignKey(ContentType)
      recipients = models.ManyToManyField(ContentType)

【问题讨论】:

    标签: python django generics django-models many-to-many


    【解决方案1】:

    您可以通过手动创建消息和收件人之间的联结表来使用通用关系来实现这一点:

    from django.db import models
    from django.contrib.contenttypes import generic
    from django.contrib.contenttypes.models import ContentType
    
    class Client(models.Model):
        city = models.CharField(max_length=16)
    
        # These aren't required, but they'll allow you do cool stuff
        # like "person.sent_messages.all()" to get all messages sent
        # by that person, and "person.received_messages.all()" to
        # get all messages sent to that person.
        # Well...sort of, since "received_messages.all()" will return
        # a queryset of "MessageRecipient" instances.
        sent_messages = generic.GenericRelation('Message',
            content_type_field='sender_content_type',
            object_id_field='sender_id'
        )
        received_messages = generic.GenericRelation('MessageRecipient',
            content_type_field='recipient_content_type',
            object_id_field='recipient_id'
        )
    
        class Meta:
            abstract = True
    
    class PersonClient(Client):
        first_name = models.CharField(max_length=16)
        last_name = models.CharField(max_length=16)
        gender = models.CharField(max_length=1)
    
        def __unicode__(self):
            return u'%s %s' % (self.last_name, self.first_name)
    
    class CompanyClient(Client):
        name = models.CharField(max_length=32)
        tax_no = models.PositiveIntegerField()
    
        def __unicode__(self):
            return self.name
    
    class Message(models.Model):
        sender_content_type = models.ForeignKey(ContentType)
        sender_id = models.PositiveIntegerField()
        sender = generic.GenericForeignKey('sender_content_type', 'sender_id')
        msg_body = models.CharField(max_length=1024)
    
        def __unicode__(self):
            return u'%s...' % self.msg_body[:25]
    
    class MessageRecipient(models.Model):
        message = models.ForeignKey(Message)
        recipient_content_type = models.ForeignKey(ContentType)
        recipient_id = models.PositiveIntegerField()
        recipient = generic.GenericForeignKey('recipient_content_type', 'recipient_id')
    
        def __unicode__(self):
            return u'%s sent to %s' % (self.message, self.recipient)
    

    你会像这样使用上述模型:

    >>> person1 = PersonClient.objects.create(first_name='Person', last_name='One', gender='M')
    >>> person2 = PersonClient.objects.create(first_name='Person', last_name='Two', gender='F')
    >>> company = CompanyClient.objects.create(name='FastCompany', tax_no='4220')
    >>> company_ct = ContentType.objects.get_for_model(CompanyClient)
    >>> person_ct = ContentType.objects.get_for_model(person1) # works for instances too.
    
    # now we create a message:
    
    >>> msg = Message.objects.create(sender_content_type=person_ct, sender_id=person1.pk, msg_body='Hey, did any of you move my cheese?')
    
    # and send it to a coupla recipients:
    
    >>> MessageRecipient.objects.create(message=msg, recipient_content_type=person_ct, recipient_id=person2.pk)
    >>> MessageRecipient.objects.create(message=msg, recipient_content_type=company_ct, recipient_id=company.pk)
    >>> MessageRecipient.objects.count()
    2
    

    如您所见,这是一个更加冗长(复杂?)的解决方案。我可能会保持简单,并在下面使用 Prariedogg 的解决方案。

    【讨论】:

    • Client 模型中,我不明白为什么MessageRecipientreceived_messages = generic.GenericRelation('MessageRecipient', ...) 中?必须是Message吗?
    • @user3595632 received_messagesClientMessage 之间的多对多关系。这就是为什么它必须在MessageRecipient 上,它明确地模拟了这种关系,因为没有GenericManyToManyField。这有意义吗?
    • 不错的解决方案。我刚刚将它用于我的用例。谢谢!
    • 这会创建一个连接表吗?
    • 为什么sent_messages 也需要是通用的? Message只有一种,能不能是正常关系?或者这个例子假设将添加多种类型的消息(即电子邮件、文本、邮政)?
    【解决方案2】:

    解决这个问题的绝对最佳方法是使用一个名为 django-gm2m 的库

    pip install django-gm2m
    

    如果我们有我们的模型

    >>> from django.db import models
    >>>
    >>> class Video(models.Model):
    >>>       class Meta:
    >>>           abstract = True
    >>>
    >>> class Movie(Video):
    >>>     pass
    >>>
    >>> class Documentary(Video):
    >>>     pass
    

    还有一个用户

    >>> from gm2m import GM2MField
    >>>
    >>> class User(models.Model):
    >>>     preferred_videos = GM2MField()
    

    我们可以的

    >>> user = User.objects.create()
    >>> movie = Movie.objects.create()
    >>> documentary = Documentary.objects.create()
    >>>
    >>> user.preferred_videos.add(movie)
    >>> user.preferred_videos.add(documentary)
    

    甜吗?

    欲了解更多信息,请访问:

    http://django-gm2m.readthedocs.org/en/stable/quick_start.html

    【讨论】:

    • 不幸的是,它不适用于管理员,并且似乎不再受支持。
    • 这个包与 django rest framework > 3.9 中断
    • 我现在认为,如果您需要使用它,那么您的系统建模不正确。看看我上面的例子 >>> user.preferred_videos.add(movie) >>> user.preferred_videos.add(documentary) 最好的办法就是有一个视频模型,选择字段为movie_type
    • 是的,gm2m 不再维护:github.com/tkhyn/django-gm2m/issues/48
    【解决方案3】:

    您可以通过简化架构以包含单个 Client 表来解决此问题,该表带有一个标志来指示它是什么类型的客户端,而不是使用两个单独的模型。

    from django.db import models
    from django.utils.translation import ugettext_lazy as _
    
    class Client(models.Model):
        PERSON, CORPORATION = range(2)
        CLIENT_TYPES = (
                        (PERSON, _('Person')),
                        (CORPORATION, _('Corporation')),
                       )
        type = models.PositiveIntegerField(choices=CLIENT_TYPES, default=PERSON)
        city = models.CharField(max_length=16)
        first_name = models.CharField(max_length=16, blank=True, null=True)
        last_name = models.CharField(max_length=16, blank=True, null=True)
        corporate_name = models.CharField(max_length=16, blank=True, null=True)
        tax_no = models.PositiveIntegerField(blank=True, null=True)
    
        def save(self, *args, **kwargs):
            """
            Does some validation ensuring that the person specific fields are
            filled in when self.type == self.PERSON, and corporation specific
            fields are filled in when self.type == self.CORPORATION ...
    
            """
            # conditional save logic goes here
            super(Client, self).save(*args, **kwargs)
    

    如果您以这种方式做事,您可能根本不必使用通用外键。作为额外的便利,您还可以为客户端模型编写自定义管理器,例如 Client.corporate.all()Client.person.all(),以返回仅包含您想要的客户端类型的预过滤查询集。

    这也可能不是解决您的问题的最佳方式。我只是把它作为一种潜在的可能性扔在那里。我不知道是否有将两个相似模型组合在一起并使用保存覆盖来确保数据完整性的传统智慧。看起来它可能有潜在的问题......我会让社区在这个方面向我学习。

    【讨论】:

    • 谢谢@Prairiedogg。同意你所说的一切。我仍然有兴趣看看是否有使用泛型关系的解决方案......
    • 老实说,我开始更喜欢在我的用例中使用“类型”列的想法,即使它是设计模式/执行多态性的“错误”方式,因为它非常简单并且不会对通过尝试执行通用关系魔术而引入的 Django ORM 施加限制。例如 Django ORM can't handle filtering a generic relation。在管理中查询和期望显示方面感觉除了麻烦之外什么都没有。
    猜你喜欢
    • 1970-01-01
    • 2018-10-03
    • 2016-04-18
    • 2015-02-15
    • 1970-01-01
    • 1970-01-01
    • 2018-05-20
    • 2015-10-30
    • 1970-01-01
    相关资源
    最近更新 更多