Django:具有多个子模型类型的父模型

Posted

技术标签:

【中文标题】Django:具有多个子模型类型的父模型【英文标题】:Django: Parent Model with multiple child model types 【发布时间】:2016-11-06 09:19:18 【问题描述】:

我为 CMS 创建了一组 Django 模型,以显示一系列 Products。

每个页面包含一系列行,所以我有一个通用的

class ProductRow(models.Model):
  slug = models.SlugField(max_length=100, null=False, blank=False, unique=True, primary_key=True)
  name = models.CharField(max_length=200,null=False,blank=False,unique=True)
  active = models.BooleanField(default=True, null=False, blank=False)

然后我有这个模型的一系列子模型,用于不同类型的行:

class ProductBanner(ProductRow):
  wide_image = models.ImageField(upload_to='product_images/banners/', max_length=100, null=False, blank=False)
  top_heading_text = models.CharField(max_length=100, null=False, blank=False)
  main_heading_text = models.CharField(max_length=200, null=False, blank=False)
  ...

class ProductMagazineRow(ProductRow):
  title = models.CharField(max_length=50, null=False, blank=False)
  show_descriptions = models.BooleanField(null=False, blank=False, default=False)
  panel_1_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  panel_2_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  panel_3_product = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  ...

class ProductTextGridRow(ProductRow):
  title = models.CharField(max_length=50, null=False, blank=False)
  col1_title = models.CharField(max_length=50, null=False, blank=False)
  col1_product_1 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  col1_product_2 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  col1_product_3 = models.ForeignKey(Product, related_name='+', null=False, blank=False)
  ...

等等。

然后在我的ProductPage我有一系列ProductRows:

class ProductPage(models.Model):
  slug = models.SlugField(max_length=100, null=False, blank=False, unique=True, primary_key=True)
  name = models.CharField(max_length=200, null=False, blank=False, unique=True)
  title = models.CharField(max_length=80, null=False, blank=False)
  description = models.CharField(max_length=80, null=False, blank=False)
  row_1 = models.ForeignKey(ProductRow, related_name='+', null=False, blank=False)
  row_2 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
  row_3 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
  row_4 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)
  row_5 = models.ForeignKey(ProductRow, related_name='+', null=True, blank=True)

我遇到的问题是,我想让ProductPage 中的这 5 行成为ProductRow 的任何不同子类型。但是,当我遍历它们时,例如

views.py

product_page_rows = [product_page.row_1,product_page.row_2,product_page.row_3,product_page.row_4,product_page.row_5]

然后在模板中:

% for row in product_page_rows %
  <pre> row.XXXX </pre>
% endfor %

我不能将任何子字段引用为XXXX

我尝试为父子节点添加一个“type()”方法,以尝试区分每一行是哪个类:

class ProductRow(models.Model):

  ...

  @classmethod
  def type(cls):
      return "generic"

class ProductTextGridRow(TourRow):

  ...

  @classmethod
  def type(cls):
      return "text-grid"

但是如果我在模板中将XXXX 更改为.type(),那么它会为列表中的每个项目显示"generic"(我在数据中定义了各种行类型),所以我想一切都会回来作为ProductRow 而不是适当的子类型。我找不到任何办法让孩子们可以作为正确的孩子类型而不是父母类型来访问,或者确定他们实际上是哪种孩子类型(我也试过catching AttributeError,这没有帮助)。

有人可以建议我如何正确处理各种模型类型的列表,所有这些模型类型都包含一个共同的父模型,并且能够访问相应子模型类型的字段吗?

【问题讨论】:

这里看起来有问题...您确定要在所有这些模型中使用多个 ForeignKeys 吗?每个ForeignKey 代表一个1-N 关系,因此,例如,您的模型ProductRow 有5 个不同的关系,用ProductPage 代表不同的事物。只是确认这是否是您想要的。 是的,每个产品页面将有 5 个不同的行,每个行都是各种 ProductRow 类型之一 .. 并且每个产品行可能用于多个不同的页面 如果它更简单,考虑一个更标准的面向对象示例:我想存储Vehicles 的列表。每个Vehicle 可以是CarBikeVanBoatVehiclePage 包含 5 个Vehicles 的列表,每个CarBikeVanBoat。我需要能够在Vehicles 中循环并说出类似“如果它是Car,获取它的车牌......如果它是一个Van,获取它的立方存储容量,等等...... .`" 我只能说,在阅读了更多关于数据库规范化、设计等方面的内容后,您需要从头开始进行数据库设计 【参考方案1】:

这通常(读作“总是”)是一个糟糕的设计,有这样的东西:

class MyModel(models.Model):
    ...
    row_1 = models.ForeignKey(...)
    row_2 = models.ForeignKey(...)
    row_3 = models.ForeignKey(...)
    row_4 = models.ForeignKey(...)
    row_5 = models.ForeignKey(...)

它不可扩展。如果您想在某一天(谁知道?)允许 6 行或 4 行而不是 5 行,您将不得不添加/删除一个新行并更改您的数据库方案(并处理具有 5 行的现有对象)。而且它不是 DRY,您的代码量取决于您处理的行数,并且涉及大量复制粘贴。

很明显,如果您想知道如果必须处理 100 行而不是 5 行该怎么做,这是一个糟糕的设计。

您必须使用ManyToManyField() 和一些自定义逻辑来确保至少有一行,最多有五行。

class ProductPage(models.Model):
    ...
    rows = models.ManyToManyField(ProductRow)

如果您希望您的行被排序,您可以使用这样的显式中间模型:

class ProductPageRow(models.Model):

    class Meta:
        order_with_respect_to = 'page'

    row = models.ForeignKey(ProductRow)
    page = models.ForeignKey(ProductPage)

class ProductPage(models.Model):
    ...
    rows = model.ManyToManyField(ProductRow, through=ProductPageRow)

我只想允许N 行(比如说5),你可以实现自己的order_with_respect_to 逻辑:

from django.core.validators import MaxValueValidator

class ProductPageRow(models.Model):

    class Meta:
        unique_together = ('row', 'page', 'ordering')

    MAX_ROWS = 5

    row = models.ForeignKey(ProductRow)
    page = models.ForeignKey(ProductPage)
    ordering = models.PositiveSmallIntegerField(
        validators=[
            MaxValueValidator(MAX_ROWS - 1),
        ],
    )

元组 ('row', 'page', 'ordering') 唯一性被强制执行,并且排序限制为五个值(从 0 到 4),('row', 'page') 对的出现次数不能超过 5 次。

但是, 除非您有充分的理由 100% 确保无法以任何方式在数据库中添加超过 N 行(包括直接在您的 DBMS 控制台),无需将其“锁定”到此级别。

很可能所有“不受信任”的用户只能通过 html 表单输入来更新您的数据库。并且您可以使用 formsets 来强制填写表格时的最小和最大行数。

注意:这也适用于您的其他型号。任何一堆名为 foobar_N,其中N 是一个递增的整数,背叛了一个非常糟糕的 数据库设计。


但是,这并不能解决您的问题。

从父模型实例中获取子模型实例的最简单(阅读“想到的第一个”)方法是遍历每个可能的子模型,直到获得匹配的实例。

class ProductRow(models.Model):
    ...
    def get_actual_instance(self):
        if type(self) != ProductRow:
            # If it's not a ProductRow, its a child
            return self
        attr_name = '_ptr'.format(ProductRow._meta.model_name)
        for possible_class in self.__subclasses__():
            field_name = possible_class._meta.get_field(attr_name).related_query_name()
            try:
                return getattr(self, field_name)
            except possible_class.DoesNotExist:
                pass
         # If no child found, it was a ProductRow
         return self

但是每次尝试都要访问数据库。它仍然不是很干燥。获得它的最有效方法是添加一个可以告诉您孩子类型的字段:

from django.contrib.contenttypes.models import ContentType

class ProductRow(models.Model):
    ...
    actual_type = models.ForeignKey(ContentType, editable=False)

    def save(self, *args, **kwargs):
        if self._state.adding:
            self.actual_type = ContentType.objects.get_for_model(type(self))
         super().save(*args, **kwargs)

    def get_actual_instance(self):
        my_info = (self._meta.app_label, self._meta.model_name)
        actual_info = (self.actual_type.app_label, self.actual_type.model)
        if type(self) != ProductRow or my_info == actual_info:
            # If this is already the actual instance
            return self
        # Otherwise
        attr_name = '_ptr_id'.format(ProductRow._meta.model_name)
        return self.actual_type.get_object_for_this_type(**
            attr_name: self.pk,
        )

【讨论】:

感谢您抽出宝贵时间对数据库规范化和链接表进行很好的解释。碰巧在这种情况下,我有一些非常具体的理由来选择固定的、有限数量的非规范化字段。感谢self.actual_type 解决方案。这非常聪明,正是我想要实现的目标!!!【参考方案2】:

您的type() 方法不起作用,因为您使用的是multi-table inheritance:ProductRow 的每个子模型都是使用自动生成的OneToOneField 连接到ProductRow 的单独模型。

如果您确保每个 ProductRow 实例只有一种类型的孩子(在三种可能的孩子中),有一种简单的方法可以确定该孩子是 ProductBannerProductMagazineRow 还是ProductTextGridRow,然后使用相应的字段:

class ProductRow(models.Model):
    ...
    def get_type(self):
        try:
            self.productbanner
            return 'product-banner'
        except ProductBanner.DoesNotExist:
            pass

        try:
            self.productmagazinerow
            return 'product-magazine'
        except ProductMagazineRow.DoesNotExist:
            pass

        try:
            self.producttextgridrow
            return 'product-text-grid'
        except ProductTextGridRow.DoesNotExist:
            pass

        return 'generic'

但是,如果您不强制执行,则ProductRow 的一个实例可以同时链接到ProductBannerProductMagazineRowProductTextGridRow 中的多个。您将不得不使用特定的实例:

class ProductRow(models.Model):
    ...
    def get_productbanner(self):
        try:
            return self.productbanner
        except ProductBanner.DoesNotExist:
            return None

    def get_productmagazinerow(self):
        try:
            return self.productmagazinerow
        except ProductMagazineRow.DoesNotExist:
            return None

    def get_producttextgridrow(self)
        try:
            return self.producttextgridrow
        except ProductTextGridRow.DoesNotExist:
            return None

将此与 Antonio Pinsard 的答案结合起来,以改进您的数据库设计。

【讨论】:

“如果子类型重叠”是什么意思?孩子的关系是一对一的,父母不可能有多个孩子。 有点清楚:ProductRow 的“父”实例每个可以有一个“子”,即ProductBannerProductMagazineRowProductTextGridRow 的实例。我将更改第二点的语言以包含此 谢谢@YashTewari。当你tryself.productmagazinerow 和类似的,我认为这些只是给定子类型独有的字段?所以本质上方法是..尝试访问特定子类型独有的字段...然后如果错误不存在则捕获错误。我喜欢它,聪明。 PS 数据库设计是故意非规范化的,在这种情况下,我有特定的理由选择固定的、有限数量的字段。 如果我正确理解您的问题,您是在问self.productmagazinerow 是否是模型ProductMagazineRow 独有的字段。不,self.productmagazinerowProductRow 中自动创建的字段。它是在您从ProductRow 派生模型ProductMagazineRow 时创建的。参考我的答案顶部,并点击链接到multi-table inheritance

以上是关于Django:具有多个子模型类型的父模型的主要内容,如果未能解决你的问题,请参考以下文章

Django - 在管理面板中显示具有内容类型的模型

7.Django模型类的定义和管理

Laravel 自己模型上的父/子关系

Python/Django:数据库模型中字段“类型”的同义词(保留的内置符号)

如何创建具有选择的多个用户模型?

如何表示同一类型的父属性和多个子属性之间的关系?