如何在可重用的Django应用程序中建模外键?

时间:2021-09-07 18:14:43

In my django site I have two apps, blog and links. blog has a model blogpost, and links has a model link. There should be a one to many relationship between these two things. There are many links per blogpost, but each link has one and only one blog post. The simple answer is to put a ForeignKey to blogpost in the link model.

在我的Django网站上,我有两个应用程序,博客和链接。博客有博客模型,链接有模型链接。这两件事之间应该存在一对多的关系。每个博客帖子有很多链接,但每个链接只有一篇博文。简单的答案是在链接模型中将ForeignKey放到blogpost中。

That's all well and good, however there is a problem. I want to make the links app reusable. I don't want it to depend upon the blog app. I want to be able to use it again in other sites and perhaps associate links with other non-blogpost apps and models.

这一切都很好,但有一个问题。我想让链接应用程序可重用。我不希望它依赖于博客应用程序。我希望能够在其他网站中再次使用它,并且可能将链接与其他非博客应用和模型相关联。

A generic foreign key seems like it might be the answer, but not really. I don't want links to be able to associate with any model in my site. Just the one that I explicitly specify. And I know from prior experience that there can be issues using generic foreign keys in terms of database usage because you can't do a select_related over a generic foreign key the way you can with a regular foreign key.

一般的外键似乎可能是答案,但不是真的。我不希望链接能够与我的网站中的任何模型相关联。只是我明确指定的那个。我从之前的经验中知道,在数据库使用方面可能存在使用通用外键的问题,因为您不能像使用常规外键那样对通用外键执行select_related。

What is the "correct" way to model this relationship?

建立这种关系的“正确”方法是什么?

6 个解决方案

#1


22  

If you think the link app will always point to a single app then one approach would be to pass the name of the foreign model as a string containing the application label instead of a class reference (Django docs explanation).

如果您认为链接应用程序将始终指向单个应用程序,那么一种方法是将外部模型的名称作为包含应用程序标签而不是类引用的字符串传递(Django docs说明)。

In other words, instead of:

换句话说,而不是:

class Link(models.Model):
    blog_post = models.ForeignKey(BlogPost)

do:

做:

from django.conf import setings
class Link(models.Model):
    link_model = models.ForeignKey(settings.LINK_MODEL)

and in your settings.py:

并在您的settings.py中:

LINK_MODEL = 'someproject.somemodel'

#2


1  

I think TokenMacGuy is on the right track. I would look at how django-tagging handles a similar generic relationship using the content type, generic object_id, and generic.py. From models.py

我认为TokenMacGuy走在正确的轨道上。我将看看django-tagging如何使用内容类型,泛型object_id和generic.py处理类似的泛型关系。来自models.py

class TaggedItem(models.Model):
    """
    Holds the relationship between a tag and the item being tagged.
    """
    tag          = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
    content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
    object_id    = models.PositiveIntegerField(_('object id'), db_index=True)
    object       = generic.GenericForeignKey('content_type', 'object_id')

    objects = TaggedItemManager()

    class Meta:
        # Enforce unique tag association per object
        unique_together = (('tag', 'content_type', 'object_id'),)
        verbose_name = _('tagged item')
        verbose_name_plural = _('tagged items')

#3


1  

Anoher way to solve this is how django-mptt does this: define only an abstract model in a reusable app(MPTTModel), and require to inherit it with defining some fields (parent=ForeignKey to self, or whatever your app usecase will require)

解决这个问题的另一种方法是django-mptt这样做:在可重用的应用程序(MPTTModel)中只定义一个抽象模型,并且需要通过定义一些字段来继承它(parent = ForeignKey to self,或者你的应用程序用例需要的任何东西)

#4


0  

Probably you need to use the content types app to link to a model. You might then arrange for your app to check the settings to do some additional checking to limit which content types it will accept or suggest.

您可能需要使用内容类型应用程序链接到模型。然后,您可以安排您的应用检查设置以进行一些额外检查,以限制它将接受或建议的内容类型。

#5


0  

I'd go with generic relations. You can do something like select_related, it just require some extra work. But I think it's worth it.

我会选择通用关系。您可以执行select_related之类的操作,只需要一些额外的工作。但我认为这是值得的。

One possible solution for generic select_related-like functionality:

通用select_related类功能的一种可能解决方案:

http://bitbucket.org/kmike/django-generic-images/src/tip/generic_utils/managers.py

http://bitbucket.org/kmike/django-generic-images/src/tip/generic_utils/managers.py

(look at GenericInjector manager and it's inject_to method)

(查看GenericInjector管理器,它是inject_to方法)

#6


0  

This question and Van Gale's answer lead me to the question, how it could be possible, to limit contenttypes for GFK without the need of defining it via Q objects in the model, so it could be completly reuseable

这个问题和Van Gale的回答引出了一个问题,即如何可能限制GFK的内容类型,而不需要通过模型中的Q对象来定义它,因此它可以完全重复使用

the solution is based on

解决方案基于

  • django.db.models.get_model
  • django.db.models.get_model
  • and the eval built-in, that evaluates a Q-Object from settings.TAGGING_ALLOWED. This is necessary for usage in the admin-interface
  • 和eval内置的,用于评估来自settings.TAGGING_ALLOWED的Q-Object。这是在admin-interface中使用所必需的

My code is quite rough and not fully tested

我的代码很粗糙,没有经过全面测试

settings.py

settings.py

TAGGING_ALLOWED=('myapp.modela', 'myapp.modelb')

models.py:

models.py:

from django.db import models
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db.models import get_model
from django.conf import settings as s
from django.db import IntegrityError

TAGABLE = [get_model(i.split('.')[0],i.split('.')[1]) 
        for i in s.TAGGING_ALLOWED if type(i) is type('')]
print TAGABLE

TAGABLE_Q = eval( '|'.join(
    ["Q(name='%s', app_label='%s')"%(
        i.split('.')[1],i.split('.')[0]) for i in s.TAGGING_ALLOWED
    ]
))

class TaggedItem(models.Model):
    content_type = models.ForeignKey(ContentType, 
                    limit_choices_to = TAGABLE_Q)                               
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey('content_type', 'object_id')

    def save(self, force_insert=False, force_update=False):
        if self.content_object and not type(
            self.content_object) in TAGABLE:
            raise IntegrityError(
               'ContentType %s not allowed'%(
                type(kwargs['instance'].content_object)))
        super(TaggedItem,self).save(force_insert, force_update)

from django.db.models.signals import post_init
def post_init_action(sender, **kwargs):
    if kwargs['instance'].content_object and not type(
        kwargs['instance'].content_object) in TAGABLE:
        raise IntegrityError(
           'ContentType %s not allowed'%(
            type(kwargs['instance'].content_object)))

post_init.connect(post_init_action, sender= TaggedItem)

Of course the limitations of the contenttype-framework affect this solution

当然,contenttype-framework的局限性会影响这个解决方案

# This will fail
>>> TaggedItem.objects.filter(content_object=a)
# This will also fail
>>> TaggedItem.objects.get(content_object=a)

#1


22  

If you think the link app will always point to a single app then one approach would be to pass the name of the foreign model as a string containing the application label instead of a class reference (Django docs explanation).

如果您认为链接应用程序将始终指向单个应用程序,那么一种方法是将外部模型的名称作为包含应用程序标签而不是类引用的字符串传递(Django docs说明)。

In other words, instead of:

换句话说,而不是:

class Link(models.Model):
    blog_post = models.ForeignKey(BlogPost)

do:

做:

from django.conf import setings
class Link(models.Model):
    link_model = models.ForeignKey(settings.LINK_MODEL)

and in your settings.py:

并在您的settings.py中:

LINK_MODEL = 'someproject.somemodel'

#2


1  

I think TokenMacGuy is on the right track. I would look at how django-tagging handles a similar generic relationship using the content type, generic object_id, and generic.py. From models.py

我认为TokenMacGuy走在正确的轨道上。我将看看django-tagging如何使用内容类型,泛型object_id和generic.py处理类似的泛型关系。来自models.py

class TaggedItem(models.Model):
    """
    Holds the relationship between a tag and the item being tagged.
    """
    tag          = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
    content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
    object_id    = models.PositiveIntegerField(_('object id'), db_index=True)
    object       = generic.GenericForeignKey('content_type', 'object_id')

    objects = TaggedItemManager()

    class Meta:
        # Enforce unique tag association per object
        unique_together = (('tag', 'content_type', 'object_id'),)
        verbose_name = _('tagged item')
        verbose_name_plural = _('tagged items')

#3


1  

Anoher way to solve this is how django-mptt does this: define only an abstract model in a reusable app(MPTTModel), and require to inherit it with defining some fields (parent=ForeignKey to self, or whatever your app usecase will require)

解决这个问题的另一种方法是django-mptt这样做:在可重用的应用程序(MPTTModel)中只定义一个抽象模型,并且需要通过定义一些字段来继承它(parent = ForeignKey to self,或者你的应用程序用例需要的任何东西)

#4


0  

Probably you need to use the content types app to link to a model. You might then arrange for your app to check the settings to do some additional checking to limit which content types it will accept or suggest.

您可能需要使用内容类型应用程序链接到模型。然后,您可以安排您的应用检查设置以进行一些额外检查,以限制它将接受或建议的内容类型。

#5


0  

I'd go with generic relations. You can do something like select_related, it just require some extra work. But I think it's worth it.

我会选择通用关系。您可以执行select_related之类的操作,只需要一些额外的工作。但我认为这是值得的。

One possible solution for generic select_related-like functionality:

通用select_related类功能的一种可能解决方案:

http://bitbucket.org/kmike/django-generic-images/src/tip/generic_utils/managers.py

http://bitbucket.org/kmike/django-generic-images/src/tip/generic_utils/managers.py

(look at GenericInjector manager and it's inject_to method)

(查看GenericInjector管理器,它是inject_to方法)

#6


0  

This question and Van Gale's answer lead me to the question, how it could be possible, to limit contenttypes for GFK without the need of defining it via Q objects in the model, so it could be completly reuseable

这个问题和Van Gale的回答引出了一个问题,即如何可能限制GFK的内容类型,而不需要通过模型中的Q对象来定义它,因此它可以完全重复使用

the solution is based on

解决方案基于

  • django.db.models.get_model
  • django.db.models.get_model
  • and the eval built-in, that evaluates a Q-Object from settings.TAGGING_ALLOWED. This is necessary for usage in the admin-interface
  • 和eval内置的,用于评估来自settings.TAGGING_ALLOWED的Q-Object。这是在admin-interface中使用所必需的

My code is quite rough and not fully tested

我的代码很粗糙,没有经过全面测试

settings.py

settings.py

TAGGING_ALLOWED=('myapp.modela', 'myapp.modelb')

models.py:

models.py:

from django.db import models
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.db.models import get_model
from django.conf import settings as s
from django.db import IntegrityError

TAGABLE = [get_model(i.split('.')[0],i.split('.')[1]) 
        for i in s.TAGGING_ALLOWED if type(i) is type('')]
print TAGABLE

TAGABLE_Q = eval( '|'.join(
    ["Q(name='%s', app_label='%s')"%(
        i.split('.')[1],i.split('.')[0]) for i in s.TAGGING_ALLOWED
    ]
))

class TaggedItem(models.Model):
    content_type = models.ForeignKey(ContentType, 
                    limit_choices_to = TAGABLE_Q)                               
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey('content_type', 'object_id')

    def save(self, force_insert=False, force_update=False):
        if self.content_object and not type(
            self.content_object) in TAGABLE:
            raise IntegrityError(
               'ContentType %s not allowed'%(
                type(kwargs['instance'].content_object)))
        super(TaggedItem,self).save(force_insert, force_update)

from django.db.models.signals import post_init
def post_init_action(sender, **kwargs):
    if kwargs['instance'].content_object and not type(
        kwargs['instance'].content_object) in TAGABLE:
        raise IntegrityError(
           'ContentType %s not allowed'%(
            type(kwargs['instance'].content_object)))

post_init.connect(post_init_action, sender= TaggedItem)

Of course the limitations of the contenttype-framework affect this solution

当然,contenttype-framework的局限性会影响这个解决方案

# This will fail
>>> TaggedItem.objects.filter(content_object=a)
# This will also fail
>>> TaggedItem.objects.get(content_object=a)