【问题标题】:Django Model Field for Abstract Base Class抽象基类的 Django 模型字段
【发布时间】:2011-08-05 01:46:34
【问题描述】:

我已经在堆栈溢出周围搜索了这个(可能很简单)问题的答案,但我看到的大多数解决方案似乎都过于复杂且难以理解。

我有一个模型“Post”,它是一个抽象基类。模型“公告”和“事件”继承自 Post。

现在我在其他模型中保留了事件和公告的相关列表。例如,我在另一个模型中有“removed_events”和“removed_announcements”字段。

但是,在我的项目中,“removed_events”和“removed_announcements”的处理方式完全相同。无需区分“已删除事件”和“已删除公告”。换句话说,跟踪“removed_posts”的字段就足够了。

我不知道如何(或者可能不能)创建一个字段“removed_posts”,因为 Post 是抽象的。然而,现在我觉得我在代码中重复自己(并且不得不做很多杂乱无章的检查来确定我正在查看的帖子是事件还是公告并将其添加到适当的删除字段)。

这里最好的选择是什么?我可以使 Posts 非抽象,但 Post 对象本身不应该被创建,而且我认为我不能对非抽象对象强制执行此操作。

我对数据库的理解很薄弱,但我的印象是,使 Post 非抽象会使数据库由于连接而变得复杂。这有什么大不了的吗?

最后,我想在其他模型中的其他字段中将相当于 event_list 和announcement_list 的内容压缩为 post_list,但这些字段确实需要消除歧义。我可以根据帖子类型过滤 post_list,但是对 filter() 的调用会比能够分别直接访问事件和公告列表要慢,不是吗?这里有什么建议吗?

非常感谢您阅读本文。

【问题讨论】:

    标签: django class abstract base django-queryset


    【解决方案1】:

    通用关系和外键是您成功之路上的朋友。定义一个中间模型,其中一侧是通用的,然后另一侧将获得多态模型的相关列表。它只是比标准的 m2m 连接模型稍微复杂一点,因为通用端有两列,一列到 ContentType(实际上是 FK),另一列到实际链接模型实例的 PK。您还可以使用标准 FK 参数限制要链接的模型。 你会很快适应它。

    (现在我得到了一个真正的键盘来写字,这里有例子:)

    class Post(models.Model):
        class Meta: abstract = True
        CONCRETE_CLASSES = ('announcement', 'event',)
        removed_from = generic.GenericRelation('OwnerRemovedPost',
            content_type_field='content_type',
            object_id_field='post_id',
        )
    
    class Announcement(Post): pass
    
    class Event(Post): pass
    
    class Owner(models.Model):
    
        # non-polymorphic m2m
        added_events = models.ManyToManyField(Event, null=True)
    
        # polymorphic m2m-like property
        def removed_posts(self):
            # can't use ManyToManyField with through.
            # can't return a QuerySet b/c it would be a union.
            return [i.post for i in self.removed_post_items.all()]
    
        def removed_events(self):
            # using Post's GenericRelation
            return Event.objects.filter(removed_from__owner=self)
    
    
    class OwnerRemovedPost(models.Model):
        content_type = models.ForeignKey(ContentType,
            limit_choices_to={'name__in': Post.CONCRETE_CLASSES},
        )
        post_id = models.PositiveIntegerField()
        post = generic.GenericForeignKey('content_type', 'post_id')
        owner = models.ForeignKey(Owner, related_name='removed_post_items')
    
        class Meta:
            unique_together = (('content_type', 'post_id'),)  # to fake FK constraint
    

    您不能像经典的多对多那样过滤到相关集合中,但是使用 Owner 中的正确方法,并巧妙地使用具体类的管理器,您可以随心所欲。

    【讨论】:

    • 使用这样的解决方案,您不妨只使用 GenericForeignKey 来在需要外键而不是使用中介的模型上的 Announcement/Event 中。
    • 取决于原始多态建模是否有 FK 而不是 m2m,是的。无论如何,在不再需要时很难删除外键,这比将 m2m 视为 FK 加上一对一的逆(软)键要多得多。如果要使模型足够通用,则可以使用 m2m 或猴子修补,如显式 field.contribute_to_class()...
    【解决方案2】:

    Django 中有两种模型子类化——抽象基类;和多表继承。

    抽象基类从不单独使用,也没有数据库表或任何形式的标识。它们只是一种缩短代码的方法,通过对代码中而不是数据库中的公共字段集进行分组。

    例如:

    class Address(models.Model):
        street = ...
        city = ...
    
        class Meta:
            abstract = True
    
    
    class Employee(Address):
        name = ...
    
    class Employer(Address):
        employees = ...
        company_name = ...
    

    这是一个人为的例子,但正如您所见,Employee 不是AddressEmployer 也不是。它们都包含与地址相关的字段。此示例中只有两个表; EmployeeEmployer - 它们都包含地址的所有字段。无法在数据库级别将雇主地址与员工地址进行比较 - 地址没有自己的密钥。

    现在,通过多表继承(从 Address 中删除 abstract=True),Address 确实拥有一个单独的表。这将产生 3 个不同的表; AddressEmployerEmployee。 Employer 和 Employee 都有一个唯一的外键 (OneToOneField) 返回到 Address。

    您现在可以引用地址,而不必担心它是什么类型的地址。

    for address in Address.objects.all():
        try:
            print address.employer
        except Employer.DoesNotExist: # must have been an employee
            print address.employee
    

    每个地址都有自己的主键,这意味着它可以单独保存在第四张表中:

    class FakeAddresses(models.Model):
        address = models.ForeignKey(Address)
        note = ...
    

    如果您需要使用 Post 类型的对象,而不用担心它是什么类型的 Post,那么多表继承就是您所追求的。如果从子类访问任何 Post 字段,将会产生连接开销;但开销将是最小的。这是一个唯一的索引连接,应该非常快。

    只要确保,如果您需要访问 Post,您在查询集上使用 select_related

    Events.objects.select_related(depth=1)
    

    这将避免额外的查询来获取父数据,但会导致连接发生。因此,如果您需要 Post,请仅使用 select related。

    最后两句;如果帖子既可以是公告也可以是事件,那么您需要做传统的事情,并通过 ForeignKey 链接到 Post。在这种情况下,子类化将不起作用。

    最后一件事是,如果连接对父子之间的性能至关重要,则应该使用抽象继承;并使用泛型关系来引用表中的抽象帖子,这对性能的要求要低得多。

    通用关系本质上是这样存储数据的:

    class GenericRelation(models.Model):
        model = ...
        model_key = ...
    
    
    DeletedPosts(models.Model):
        post = models.ForeignKey(GenericRelation)
    

    在 SQL 中加入要复杂得多(django 可以帮助你),但它的性能也比简单的 OneToOne 连接要低。如果 OneToOne 连接严重损害了您的应用程序的性能,您应该只需要沿着这条路线走,这可能不太可能。

    【讨论】:

    • 感谢您的帮助 - 您的第一个场景是对多表继承树基的 ForeignKey 引用,这正是我希望使用的。它给出了一个错误:“错误:一个或多个模型没有验证:ordering.orderinvoicelog:'合作伙伴'与模型合作伙伴有关系,它要么没有安装,要么是抽象的。”我通过更改外键关系声明以直接引用基类而不是通过字符串中的名称来修复它。即使用 ForeignKey(MyClass) 而不是 ForeignKey('MyClass')。后者适用于其他情况,但不适用于此处。
    • 如果我有 4 个子类会怎么样 try catch 块会是什么样子?
    • @iankit 寻找可以帮助您的东西,例如 github.com/carljm/django-model-utils
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-08-21
    • 2019-12-26
    • 2016-09-11
    • 1970-01-01
    • 1970-01-01
    • 2018-03-07
    • 1970-01-01
    相关资源
    最近更新 更多