简体   繁体   中英

Polymorphic Model Inheritance in Django

This question is about model inheritance in Django.

Almost everything I have read (including the Django documentation itself) strongly recommends doing 'abstract base class' inheritance rather than 'multi-table' inheritance. I agree with the reasoning and as a result am fully behind the recommendation. However, Django does not appear to have support for:

  • polymorphic querying, or
  • model linking (ie I cannot create a ForeignKey field to the abstract base class from another model).

Situation

For example, I have some models that implement the 'abstract base class' inheritance pattern:

class Tool(models.Model):
    name = models.CharField(max_length=30)
    group = models.ManyToManyField(ToolGroup, blank=True) # Link to 'ToolGroup' MUST be placed on abstract class
    attributes = models.ManyToManyField(ToolAttributeValue, blank=True)  # Link to 'ToolAttributeValue' MUST be placed on abstract class

    class Meta:
        abstract = True # Almost everything I read strongly recommends against making this its own table


class HandheldTool(Tool):
    electrical_safe = models.BooleanField(default=False)


class PowerTool(Tool):
    compliance_expiry_date = models.DateTimeField()


class ConsumableTool(Tool):
    combustible = models.BooleanField(default=False)
    best_before = models.DateTimeField(null=True)

I also have some grouping and information classes related to tools in general:

# Grouping related structures
#
# ToolHierarchy  >       ToolGroup (n times)       > Tool
# 
#   "Tool Boxes" > "Day Shift"   > "Builders"      > HandheldTool[Hammer]
#                                                  > HandheldTool[Screwdriver - SAFE]
#                                                  > PowerTool[Drill]
#
#                                > "Demo Team"     > HandheldTool[Sledgehammer 1]
#                                                  > PowerTool[Jackhammer]
#                                                  > ConsumableTool[Dynamite]
#
#                > "Night Shift" > "Rock Breakers" > HandheldTool[Hammer]
#                                                  > HandheldTool[Sledgehammer 2]
#                                                  > PowerTool[Rocksaw]

class ToolHierarchy(models.Model):
    name = models.CharField(blank=True, max_length=30)


class ToolGroup(models.Model):
    name = models.CharField(blank=True, max_length=30)
    parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
    hierarchy = models.ForeignKey(ToolHierarchy, null=True, blank=True, related_name='top_level_tools')
    # tools = models.ManyToManyField(Tool) # CANNOT MAKE LINK, as 'Tool' is abstract


# 'Extra-info' structures
#
# ToolAttribute > ToolAttributeValue > Tool
# 
#  'Brand'      > 'Stanley'          > HandheldTool[Hammer]
#                                    > HandheldTool[Sledgehammer 1]
#               > 'ACME'             > HandheldTool[Sledgehammer 2]
#                                    > ConsumableTool[Dynamite]
#
#  'Supplier'   > 'Bash Brothers'    > HandheldTool[Hammer]
#                                    > HandheldTool[Sledgehammer 1]
#                                    > HandheldTool[Sledgehammer 2]
class ToolAttribute(models.Model):
    name = models.CharField(max_length=30)
    data_type = models.CharField(max_length=30) # e.g. "STRING", "INT", "DATE" "FLOAT" -- Actually done with enum
    unit_of_measure = models.CharField(max_length=30, blank=True)


class ToolAttributeValue(models.Model):
    attribute = models.ForeignKey(ToolAttribute)
    value = models.CharField(blank=True, max_length=30)
    # tool = models.ForeignKey(Tool)  # CANNOT MAKE LINK, as 'Tool' is abstract

Problem

Ideally this inheritance model would be implemented via a polymorphic relationship, however the Django ORM does not support it. This is possible with SQLAlchemy and other ORMs like Hibernate.

With the Django ORM, because the Tool class is abstract I cannot create the links like:

  • ToolAttributeValue.tool -> tool_obj or
  • ToolGroup.tools -> [tool_obj_1, tool_obj_2] .

Instead I am forced to create the inverse link on the abstract class, despite it modelling a slightly different thing! This then results in all sorts of ugliness on the ToolAttributeValue and ToolGroup objects, which then no longer have a .tools attribute but instead have RelatedManager fields for each subtype. ie:

tool_group_obj.handheldtool_set.all()
tool_group_obj.powertool_set.all()
...etc, for every subtype of Tool

Which pretty much destroys the usefulness of the abstract class.

Questions

So, with this in mind, my questions are:

  1. Is this a good case for 'multi-table' inheritance?
  2. Am I trying too hard to force the type inheritance? Should I just get rid of Tool ? If yes, then do I have to create a *ToolGroup model for each subclass?
  3. What is the current (Django 1.8) accepted way around this? Surely I am not the first person to build a relational system in Django ;-) And the fact that other ORMs have considered this problem suggest it is a common design choice.
  4. Is the polymorphic solution (possibly via SQLAlchemy) an option? Is this being considered for Django 1.9+?

NB: I have read the docs for and tested https://github.com/chrisglass/django_polymorphic , but it does not appear to work for 'abstract base class' inheritance (ie it is only for multi-table). If I did choose to do multi-table inheritance then django-polymorphic would be good for the query side of my problem and I guess my model-linking problem would disappear.

NB: This is a similar question to this one , but provides more detail.

Ok, so I think I'm going to answer my own questions...

  1. Is this a good case for 'multi-table' inheritance?

    It appears so. Although there are a few places that recommend against 'multi-table' inheritance ( listed here for example ), some counterpoints are:

    • @Bruno Desthuilliers points out that these opinions are not from the 'official' documentation, and by extension, he implies that 'multi-table' is a perfectly good feature available for one to use.

    • My reading of @dhke's links and comments is that you have to choose one option, and that the 'multi-table' option is the only way databases truly support inheritance. Ie Even with the polymorphic tricks of tools like Hibernate or SQLAlchemy, you are still choosing whether to JOIN tables ('multi-table' option) for object lookup or to UNION tables ('abstract base' / 'polymorphic' options) for set creation.

    • @dhke also points out that it is probably better to use the 'multi-table' option and tell a library like django-polymorphic not to do subclass resolution when looking up the 'whole set' rather than have the database do a UNION over all of the tables (as would be required for 'whole set' lookup with the 'abstract base class' option).

  2. Am I trying too hard to force the type inheritance? Should I just get rid of Tool ? If yes, then do I have to create a *ToolGroup model for each subclass?

    No, it doesn't seem that way. The two uses of the Tool interface that I presented have different needs:

    • The ToolGroup / hierarchical-grouping use case is a good one for retaining the inherited Tool class. This would get very ugly if you had to create a type-specific set of classes for every type of tool

    • The ToolAttribute also makes a good case for the super class, except if you are able to use things like the HSTORE field type (provided by Postgres, I'm not sure about other backends). This link gives a good rundown, and it is probably what I will do here (Thanks to @nigel222 for the research that went into the question!).

  3. What is the current (Django 1.8) accepted way around this? Surely I am not the first person to build a relational system in Django ;-) And the fact that other ORMs have considered this problem suggest it is a common design choice.

    This is now an irrelevant question. Basically they don't worry about it.

  4. Is the polymorphic solution (possibly via SQLAlchemy) an option? Is this being considered for Django 1.9+?

    Not that I can tell.

The case that led me to this question is a model like this:

class PurchasableItem(models.Model):

    class Meta:
        abstract = True


class Cheesecake(PurchasableItem):
    pass


class Coffee(PurchasableItem):
    pass

The workaround I used is turning the parent class into an attribute:

class PurchasableItem(models.Model):

    class Meta:
        abstract = False


class Cheesecake(models.Model):
    purchasable_item = models.OneToOneField(PurchasableItem, on_delete=models.CASCADE)


class Coffee(models.Model):
    purchasable_item = models.OneToOneField(PurchasableItem, on_delete=models.CASCADE)

This way, I can get both the behavior and querying functionality.

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