创建一个支持多种类型帖子的博客

时间:2018-10-29 06:41:19

标签: python django oop

我是Django的新用户,我试图弄清楚如何创建一个可以支持多种(类型)元素的模型。

这是情节:我想在我的应用程序上创建一个Blog模块。 为此,我创建了一个模型 Page ,该模型描述了Blog页面。还有一个模型 PageElement ,它描述了博客上的帖子。每个 Page 可以包含许多 PageElement

PageElement 可以有多种类型,因为我希望用户可以像发布短文本,视频,图片一样发布信息。我还希望(例如)用户可以发布对另一个模型的引用(例如对用户的引用)。根据用户发布的内容类型,HTML页面将以不同的方式显示每个 PageElement

但是我不知道为支持所有这些情况而声明PageElement类的正确方法是什么:(

这是我的Page模型:

class Page(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    # Basical informations
    title = models.CharField(max_length=150)
    description = models.TextField(blank=True)

    # Foreign links
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name='pages_as_user'
    )

    created_at = models.DateTimeField(default=timezone.now)

    # Other fields ....

    class Meta:
        indexes = [
            models.Index(fields=['uuid']),
            models.Index(fields=['user', 'artist'])
        ]

目前,我有两个解决方案,第一个使用继承:在博客上创建新帖子时,您将创建一个从PageElement模型继承的Element。这是我针对每种情况的不同模型:

class PageElement(models.Model):
    page = models.ForeignKey(
        Page,
        on_delete=models.CASCADE,
        related_name='%(class)s_elements'
    )

    updated_at = models.DateTimeField(default=timezone.now)
    created_at = models.DateTimeField(default=timezone.now)

class PageImageElement(PageElement):
    image = models.ImageField(null=True)
    image_url = models.URLField(null=True)

class PageVideoElement(PageElement):
    video = models.FileField(null=True)
    video_url = models.URLField(null=True)

class PageTextElement(PageElement):
    text = models.TextField(null=True)

class PageUserElement(PageElement):
    user = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
        related_name='elements'
    )

如果必须使用“纯” Python,则应该选择该解决方案。因为我可以将每个PageElement存储在字典中,然后按类对其进行过滤。而且,这种解决方案可以通过新型内容轻松地扩展。

但是使用Django模型。看来这不是最好的解决方案。因为从数据库中获取所有PageElement子代确实很困难(我不能只写“ page.elements”来获取所有类型的所有元素,所以我需要获取所有%(class)s_elements 手动将元素连接起来:/)。我已经考虑过类似下面的解决方案(我还没有尝试过),但是对于这个问题(对于必须处理大量请求的数据库而言)似乎显得过分了:

class Page(models.Model):
    # ...
    def get_elements(self):
        # Retrieve all PageElements children linked to the current Page
        R = []
        fields = self._meta.get_fields(include_hidden=True)
        for f in fields:
            try:
                if '_elements' in f.name:
                    R += getattr(self, f.name)
            except TypeError as e:
                continue

        return R

第二个“解决方案”使用一个唯一的类,其中包含我需要的所有字段。根据我要创建的 PageElement 的类型,我将 type 字段设置为正确的值,将值放在相应的字段中,然后设置为 NULL 所有其他未使用的字段:

class PageElement(models.Model):
    page = models.OneToOneField(
        Page,
        on_delete=models.CASCADE,
        related_name='elements'
    )

    updated_at = models.DateTimeField(default=timezone.now)
    created_at = models.DateTimeField(default=timezone.now)

    TYPES_CHOICE = (
        ('img', 'Image'),
        ('vid', 'Video'),
        ('txt', 'Text'),
        ('usr', 'User'),
    )
    type = models.CharField(max_length=60, choices=TYPES_CHOICE)

    # For type Image
    image = models.ImageField(null=True)
    image_url = models.URLField(null=True)

    # For type Video
    video = models.FileField(null=True)
    video_url = models.URLField(null=True)

    # For type Text
    text = models.TextField(null=True)

    # For type User
    user = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
        related_name='elements',
        null=True
    )

使用此解决方案,我可以使用“ page.elements”在单个请求中检索所有元素。但这比以前的扩展性差(我需要修改整个表结构以添加新的字段或新的Element)。

坦白地说,我绝对不知道哪种解决方案是最好的。而且我敢肯定还有其他(更好的)解决方案,但是我糟糕的定向对象技能使我无法考虑它们(:()...

我想要一个将来可以轻松修改的解决方案(例如,如果我想在Blog上添加一个引用DateTime的新Type“ calendar”)。如果我想检索与页面相关的所有元素,那么在我的应用程序中将很容易使用它。

感谢您的关注:)

3 个答案:

答案 0 :(得分:1)

我不确定是否适合您的问题,但是在这种情况下使用GenericForeignKeys/ContentType framework可能合适。掌握了这个概念,它就非常强大。

示例构造:

class Page(models.Model):
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    page_element = GenericForeignKey('content_type', 'object_id')
    ...

您现在可以通过GenericFK将任何模型对象连接到Page模型。因此,在以后的阶段添加新类型(作为新模型)并不具有干扰性。

更新

正如评论所指出的那样,此结构不能很好地支持Page的许多PageElements。

详细说明,一种解决该问题的方法,仍然利用GenericFK ...

class PageElement(models.Model):
    class Meta:
        unique_together=('page', 'content_type', 'object_id') # Solve the unique per page

     page = models.ForeignKey(Page, related_name='page_elements')
     content_type = models.ForeignKey(ContentType)
     object_id = models.PositiveIntegerField()
     content_object = GenericForeignKey('content_type', 'object_id')

一个页面可以有许多“抽象”页面元素,而content_object是“具体的页面元素模型/实现”。 易于检索特定页面的所有元素,并允许检查ContentType来检查元素的类型等。

解决这个特定问题的一种方法。

答案 1 :(得分:0)

要在Django中建立PagePageElement之间的关系,您宁愿使用外键关系,而不是继承。

class PageImageElement(PageElement):
    page = models.ForeignKey(Page,
                             on_delete=models.CASCADE,
                             related_name='images')
    image = models.ImageField(null=True)
    image_url = models.URLField(null=True)

每个用户的帖子都会创建Page的实例。在页面上每次添加图像都会创建一个PageImageElement的实例,您可以使用相关名称来查询它们。这样,访问单个Page的所有视频,图像,文本模块将非常容易。

在相关说明中,我想说PageElement类可以是抽象的see the docs,如果您声明字段可能包含null中的video = models.FileField(null=True)值,那么它可能值得声明blank=True,否则在创建这些字段未定义的对象时会出错。例如,在这里讨论:differentiate null=True, blank=True in django

答案 2 :(得分:0)

  

我不能只写“ page.elements”来获取所有类型的所有元素

实际上,如果您使用multi-table inheritance,则可以。问题在于返回的所有记录都是PageElement的实例,这意味着您丢失了所有子类类型的信息以及这些子对象可能包含的其他数据。
有很多解决这个多态性问题的软件包: django packages: Model inheritance