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

时间:2016-07-04 15:28:00

标签: python django inheritance model

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

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

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我有一系列ProductRow s:

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而不是相应的子类型返回。我找不到让孩子可以作为正确的孩子类型而不是父类型访问的方法,或者确定他们实际上是哪种孩子类型(我也试过catch ing AttributeError,没有帮助)。

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

2 个答案:

答案 0 :(得分:5)

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

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行(包括直接SQL查询)在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,
        })

答案 1 :(得分:1)

您的type()方法无效,因为您正在使用multi-table inheritanceProductRow个孩子中的每一个都是与{{{{}}连接的独立模型1}}使用自动生成的OneToOneField

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

    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' 的一个实例可以与ProductRowProductBanner和{{1}中的多个实例相关联}} 同时。您必须改为使用特定实例:

    ProductMagazineRow

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