通用多对多关系

52

我正在尝试创建一个消息系统,其中消息的发送者和接收者可以是通用实体。对于发送者来说,这似乎很好处理,只需要参考一个对象(GenericForeignKey),但是我无法弄清如何在接收者方面处理(GenericManyToManyKey??)

下面是一个简化的示例。 PersonClient和CompanyClient继承了Client的属性,但具有自己特定的详细信息。最后一行是关键所在。如何允许消息的接收者是由公司客户和个人客户组成的集合?

  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)
3个回答

66

您可以使用通用关系通过手动创建消息和收件人之间的连接表来实现此功能:

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”模型中,我不理解为什么“MessageRecipient”位于“received_messages = generic.GenericRelation('MessageRecipient', ...)”中?它必须是“Message”吗? - user3595632
2
@user3595632 received_messagesClientMessage 之间的多对多关系。这就是为什么它必须在 MessageRecipient 上,因为没有 GenericManyToManyField。这样说通了吗? - elo80ka
不错的解决方案。我刚刚在我的用例中使用了它。谢谢! - Jorge Arévalo
这会创建一个联接表吗? - Jango
为什么 sent_messages 也需要是泛型?因为 Message 只有一种类型,所以它可以是一个普通关系吗?或者这个例子假设会添加多种类型的消息(比如电子邮件、短信、邮寄信件)? - JHS
@JHS 在客户和他们发送的消息之间没有直接的ForeignKey关系。不同类型的客户(如CompanyPerson等)可以发送消息,因此sender是一个GenericForeignKeysent_messages模型反映了这种关系的反向关系,因此它本身是一个GenericRelation - elo80ka

10

处理这个问题的绝佳方法是使用一个叫做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


1
不幸的是,它不能与管理员一起使用,而且似乎已经不再受支持了。 - Ted Klein Bergman
此包与 Django Rest Framework > 3.9 不兼容。 - nickswiss
我现在的观点是,如果你需要使用这个,那么你的系统建模可能存在问题。请看上面的例子:
user.preferred_videos.add(movie) user.preferred_videos.add(documentary) 最好的方法是只使用一个Video模型,并具有电影类型的选择字段。
- Dr Manhattan
1
是的,gm2m已经不再维护了:https://github.com/tkhyn/django-gm2m/issues/48 - steffres

7
您可以通过简化模式只包含一个 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模型编写自定义管理器,例如Client.corporate.all()Client.person.all(),返回预过滤的查询集,仅包含您想要的客户类型。
这也可能不是解决您问题的最佳方式。我只是提出其中一个可能性。我不知道是否有常规智慧将两个相似的模型组合在一起并使用save override来确保数据完整性。这似乎可能会有潜在问题... 我会让社区告诉我。

感谢 @Prairiedogg。同意你所说的一切。我仍然很想看看是否有使用通用关系的解决方案... - Noel Evans
说实话,尽管在设计模式上这是错误的,使用“类型”列对于我的用例来说变得更喜欢了,因为它更简单,并且不会对 Django ORM 强加任何限制,而这些限制是通过尝试进行通用关系魔法引入的。例如,Django ORM 无法处理通用关系过滤器。对于查询和期望在管理员中显示,这种方式感觉像一堆麻烦。 - JHS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接