Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Single Table Inheritance in Django

Is there explicit support for Single Table Inheritance in Django? Last I heard, the feature was still under development and debate.

Are there libraries/hacks I can use in the meantime to capture the basic behavior? I have a hierarchy that mixes different objects. The canonical example of a corporation structure with an Employee class, subclasses for types of employees, and a manager_id (parent_id) would be a good approximation of the problem I am solving.

In my case, I would like to represent the idea that an employee can manage other employees while being managed by a different employee. There are not separate classes for Manager and Worker, which makes this hard to spread across tables. Sub-classes would represent types of employees-programmers, accountants, sales, etc and would be independent of who supervises who (OK, I guess it's no longer a typical corporation in some respect).

like image 800
sutee Avatar asked Oct 27 '08 20:10

sutee


People also ask

How does the single-table inheritance work?

In Single-Table Inheritance (STI), many subclasses inherit from one superclass with all the data in the same table in the database. The superclass has a “type” column to determine which subclass an object belongs to. In a polymorphic association, one model “belongs to” several other models using a single association.

What is polymorphic Django?

What Is Polymorphism? Polymorphism is the ability of an object to take on many forms. Common examples of polymorphic objects include event streams, different types of users, and products in an e-commerce website. A polymorphic model is used when a single entity requires different functionality or information.

What are the inheritance properties in Django?

Multi-table inheritance: Use this when the parent class has common fields, but the parent class table also exists in the database all by itself. Proxy models: Use this when you want to modify the behavior of the parent class, like by changing orderby or a new model manager.

What is multi-table inheritance in Django?

In multi-table inheritance, each model corresponds to a database table. Django creates a OneToOneField field for the relationship in the child's model to its parent. To use multi-table inheritance, you have to subclass an existing model. Django will create a database table for both the original model and the sub-model.


1 Answers

Summary

Django's proxy models provide the basis for Single Table Inheritance.

However, some effort is required to make it work.

Skip to the end for a re-usable example.

Background

Martin Fowler describes Single Table Inheritance (STI) as follows:

Single Table Inheritance maps all fields of all classes of an inheritance structure into a single table.

This is precisely what Django's proxy model inheritance does.

Note, that, according to this blog post from 2010, proxy models have been around since Django 1.1.

A "normal" Django model is a concrete model, i.e. it has a dedicated table in the database. There are two types of Django model that do not have dedicated database tables, viz. abstract models and proxy models:

  • Abstract models act as superclasses for concrete models. An abstract model can define fields, but it does not have a database table. The fields are only added to the database tables for its concrete subclasses.

  • Proxy models act as subclasses for concrete models. A proxy model cannot define new fields. Instead, it operates on the database table associated with its concrete superclass. In other words, a Django concrete model and its proxies all share a single table.

Django's proxy models provide the basis for Single Table Inheritance, viz. they allow different models to share a single table, and they allow us to define proxy-specific behavior on the Python side. However, Django's default object-relational mapping (ORM) does not provide all the behavior that would be expected, so a little customization is required. How much, that depends on your needs.

Let's build a minimal example, step by step, based on the simple data-model in the figure below:

simple party data model

Step 1: basic "proxy model inheritance"

Here's the content of models.py for a basic proxy inheritance implementation:

from django.db import models   class Party(models.Model):     name = models.CharField(max_length=20)     person_attribute = models.CharField(max_length=20)     organization_attribute = models.CharField(max_length=20)   class Person(Party):     class Meta:         proxy = True   class Organization(Party):     class Meta:         proxy = True 

Person and Organization are two types of parties.

Only the Party model has a database table, so all the fields are defined on this model, including any fields that are specific either to Person or to Organization.

Because Party, Person, and Organization all use the Party database table, we can define a single ForeignKey field to Party, and assign instances of any of the three models to that field, as implied by the inheritance relation in the figure. Note, that, without inheritance, we would need a separate ForeignKey field for each model.

For example, suppose we define an Address model as follows:

class Address(models.Model):     party = models.ForeignKey(to=Party, on_delete=models.CASCADE) 

We can then initialize an Address object using e.g. Address(party=person_instance) or Address(party=organization_instance).

So far, so good.

However, if we try to get a list of objects corresponding to a proxy model, using e.g. Person.objects.all(), we get a list of all Party objects instead, i.e. both Person objects and Organization objects. This is because the proxy models still use the model manager from the superclass (i.e. Party).

Step 2: add proxy model managers

To make sure that Person.objects.all() only returns Person objects, we need to assign a separate model manager that filters the Party queryset. To enable this filtering, we need a field that indicates which proxy model should be used for the object.

To be clear: creating a Person object implies adding a row to the Party table. The same goes for Organization. To distinguish between the two, we need a column to indicate if a row represents a Person or an Organization. For convenience and clarity, we add a field (i.e. column) called proxy_name, and use that to store the name of the proxy class.

So, enter the ProxyManager model manager and the proxy_name field:

from django.db import models   class ProxyManager(models.Manager):     def get_queryset(self):         return super().get_queryset().filter(proxy_name=self.model.__name__)   class Party(models.Model):     proxy_name = models.CharField(max_length=20)     name = models.CharField(max_length=20)     person_attribute = models.CharField(max_length=20)     organization_attribute = models.CharField(max_length=20)      def save(self, *args, **kwargs):         self.proxy_name = type(self).__name__         super().save(*args, **kwargs)   class Person(Party):     class Meta:         proxy = True      objects = ProxyManager()   class Organization(Party):     class Meta:         proxy = True      objects = ProxyManager() 

Now the queryset returned by Person.objects.all() will only contain Person objects (and the same for Organization).

However, this does not work in the case of a ForeignKey relation to Party, as in Address.party above, because that will always return a Party instance, regardless of the value of the proxy_name field (also see docs). For example, suppose we create an address = Address(party=person_instance), then address.party will return a Party instance, instead of a Person instance.

Step 3: extend the Party constructor

One way to deal with the related-field issue is to extend the Party.__new__ method, so it returns an instance of the class specified in the 'proxy_name' field. The end result looks like this:

class Party(models.Model):     PROXY_FIELD_NAME = 'proxy_name'          proxy_name = models.CharField(max_length=20)     name = models.CharField(max_length=20)     person_attribute = models.CharField(max_length=20)     organization_attribute = models.CharField(max_length=20)      def save(self, *args, **kwargs):         """ automatically store the proxy class name in the database """         self.proxy_name = type(self).__name__         super().save(*args, **kwargs)      def __new__(cls, *args, **kwargs):         party_class = cls         try:             # get proxy name, either from kwargs or from args             proxy_name = kwargs.get(cls.PROXY_FIELD_NAME)             if proxy_name is None:                 proxy_name_field_index = cls._meta.fields.index(                     cls._meta.get_field(cls.PROXY_FIELD_NAME))                 proxy_name = args[proxy_name_field_index]             # get proxy class, by name, from current module             party_class = getattr(sys.modules[__name__], proxy_name)         finally:             return super().__new__(party_class) 

Now address.party will actually return a Person instance if the proxy_name field is Person.

As a last step, we can make the whole thing re-usable:

Step 4: make it re-usable

To make our rudimentary Single-Table Inheritance implementation re-usable, we can use Django's abstract inheritance:

inheritance/models.py:

import sys from django.db import models   class ProxySuper(models.Model):     class Meta:         abstract = True      proxy_name = models.CharField(max_length=20)      def save(self, *args, **kwargs):         """ automatically store the proxy class name in the database """         self.proxy_name = type(self).__name__         super().save(*args, **kwargs)      def __new__(cls, *args, **kwargs):         """ create an instance corresponding to the proxy_name """         proxy_class = cls         try:             field_name = ProxySuper._meta.get_fields()[0].name             proxy_name = kwargs.get(field_name)             if proxy_name is None:                 proxy_name_field_index = cls._meta.fields.index(                     cls._meta.get_field(field_name))                 proxy_name = args[proxy_name_field_index]             proxy_class = getattr(sys.modules[cls.__module__], proxy_name)         finally:             return super().__new__(proxy_class)   class ProxyManager(models.Manager):     def get_queryset(self):         """ only include objects in queryset matching current proxy class """         return super().get_queryset().filter(proxy_name=self.model.__name__) 

Then we can implement our inheritance structure as follows:

parties/models.py:

from django.db import models from inheritance.models import ProxySuper, ProxyManager   class Party(ProxySuper):     name = models.CharField(max_length=20)     person_attribute = models.CharField(max_length=20)     organization_attribute = models.CharField(max_length=20)   class Person(Party):     class Meta:         proxy = True      objects = ProxyManager()   class Organization(Party):     class Meta:         proxy = True      objects = ProxyManager()   class Placement(models.Model):     party = models.ForeignKey(to=Party, on_delete=models.CASCADE) 

More work may be required, depending on your needs, but I believe this covers some of the basics.

like image 172
djvg Avatar answered Oct 05 '22 22:10

djvg