简体   繁体   中英

Django: Parent Model with multiple child model types

I've created a set of Django model for a CMS to show a series of Product s.

Each page contains a series of rows, so I have a generic

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)

then I have a series of children of this model, for different types of row:

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)
  ...

and so on.

Then in my ProductPage I have a series of 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)

The problem I have got, is that I want to allow those 5 rows in the ProductPage to be any of the different child types of ProductRow . However when I iterate over them such as

in 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]

and then in the template:

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

I cannot reference any child field as XXXX .

I tried adding a " type ()" method to both the parent and children, to try and distinguish which class each row is:

class ProductRow(models.Model):

  ...

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

and

class ProductTextGridRow(TourRow):

  ...

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

but if i change XXXX for .type() in the template then it shows "generic" for every item in the list (I had defined a variety of row types in the data), so I guess everything is coming back as a ProductRow rather than the appropriate child type. I can find no way to get the children to be accessible as the correct child type rather than the parent type, or to determine which child type they actually are (I tried catch ing AttributeError as well, that didn't help).

Can someone advise how I can properly handle a list of varied model types all of which contain a common parent, and be able to access the fields of the appropriate child model type?

This is generally (read "always") a bad design to have something like this:

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

It is not scalable. If ever you want to allow 6 rows or 4 rows instead of 5, one day (who knows?), you will have to add/delete a new row and alter your database scheme (and handle existing objects that had 5 rows). And it's not DRY, your amount of code depends on the number of rows you handle and it involves a lot of copy-pasting.

This become clear that it is a bad design if you wonder how you would do it if you had to handle 100 rows instead of 5.

You have to use a ManyToManyField() and some custom logic to ensure there is at least one row, and at most five rows.

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

If you want your rows to be ordered, you can use an explicit intermediate model like this:

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)

I want to allow only N rows (let's say 5), you could implement your own order_with_respect_to logic:

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),
        ],
    )

The tuple ('row', 'page', 'ordering') uniqueness being enforced, and ordering being limited to five values (from 0 to 4), there can't be more than 5 occurrences of the couple ('row', 'page') .

However, unless you have a very good reason to make 100% sure that there is no way to add more than N rows in the database by any mean (including direct SQL query input on your DBMS console), there is no need to "lock" it a this level.

It is very likely that all "untrusted" user will only be able to update your database through HTML form inputs. And you can use formsets to force both a minimum and a maximum number of rows when filling a form.

Note: This also applies to your other models. Any bunch of fields named foobar_N , where N is an incrementing integer, betrays a very bad database design.


Yet, this does not fix your issue.

The easiest (read "the first that comes to mind") way to get your child model instance back from the parent model instance is to loop over each possible child model until you get an instance that matches.

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

But it involves to hit the database for each try. And it is still not very DRY. The most efficient way to get it is to add a field that will tell you the type of the child:

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,
        })

Your type() method doesn't work because you're using multi-table inheritance : each of ProductRow 's children is a separate model connected to ProductRow using an automatically generated OneToOneField .

  • If you ensure that each ProductRow instance has only one type of child (out of the three possible ones), there is a simple way to find out whether that child is a ProductBanner , a ProductMagazineRow or a ProductTextGridRow , and then to use the appropriate fields:

     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' 
  • However, if you don't enforce otherwise, then one instance of ProductRow can be linked to more than one of ProductBanner , ProductMagazineRow and ProductTextGridRow at the same time. You'll have to work with the specific instances instead:

     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 

Combine this with with Antonio Pinsard's answer to improve your database design.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM