Login

Model inheritance with content type and inheritance-aware manager

Author:
dan90
Posted:
September 7, 2008
Language:
Python
Version:
1.0
Score:
8 (after 8 ratings)

inspired by crucialfelix's inheritance hack, which was a far better method of fetching a model's subclassed version from an instance than my own, i decided to bake his snippet in to my own inheritance hack, which i think benefits both. the result is a query set that returns subclassed instances per default. So - in the snippet's example, querying Meal.objects.all()[0] will return a salad object, if that instance of Meal happens to be the parent of a Salad instance.

To my mind this is closer to the 'intuitive' way for a query set on an inheriting model to behave.

Updated: added subclassing behaviour for the iterator access as well.

Updated: incorporated carljm's feedback on inheritance

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet

class SubclassingQuerySet(QuerySet):
    def __getitem__(self, k):
        result = super(SubclassingQuerySet, self).__getitem__(k)
        if isinstance(result, models.Model) :
            return result.as_leaf_class()
        else :
            return result
    def __iter__(self):
        for item in super(SubclassingQuerySet, self).__iter__():
            yield item.as_leaf_class()

class MealManager(models.Manager):
    def get_query_set(self):
        return SubclassingQuerySet(self.model)

class Meal (models.Model) :
    name = models.TextField(max_length=100)
    content_type = models.ForeignKey(ContentType,editable=False,null=True)
    objects = MealManager()
    
    def save(self, *args, **kwargs):
        if(not self.content_type):
            self.content_type = ContentType.objects.get_for_model(self.__class__)
            super(Meal, self).save(*args, **kwargs)

    def as_leaf_class(self):
        content_type = self.content_type
        model = content_type.model_class()
        if (model == Meal):
            return self
        return model.objects.get(id=self.id)
    
class Salad (Meal) :
    too_leafy = models.BooleanField(default=False)
    objects = MealManager()

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 2 weeks ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 1 week ago
  5. Help text hyperlinks by sa2812 11 months ago

Comments

carljm (on February 5, 2009):

Good snippet. Might want to be careful with its use, though; I wouldn't make it the only Manager available, as there are likely to be times you only need the parent class fields, and doesn't using SubclassingQueryset mean n+1 queries anytime you retrieve n objects? Not very scale-friendly.

My only other quibble is that it's bad practice to call self.save_base() directly; better to use super(Meal, self).save(args, *kwargs). Because of some of the oddities of how super() works in Python (it doesn't actually call the parent class method, it calls the next method in the chain, which in certain diamond inheritance structures may not always be the parent class method), failing to use it can cause highly tricky bugs.

If those two issues are resolved in some way, I'd give this a +1.

#

dan90 (on February 5, 2009):

good call on the save_base method, carljm - that's pure laziness when i copied the snippet rather than a dive into the dire world of inheritance-based errors, and is now fixed.

The other point is interesting. I think that making a non-subclassing query set available should be optional. the models I have using this code include alternate managers, but I'm not sure this snippet in general should include them. the functionality case for being able to get at parent objects is weak. (the child object already includes all parent fields and the django ORM enforces naming conventions to keep it that way) I can imagine that you'd want to perhaps call a parent's object's methods without dealing with the subclass's inherited versions, but this use case seem rare, and it's not something that one can normally do in python so why in django?

Your performance point though i think is a very good one - yes, this can be a expensive query, esp. if there are long inheritance chains, since it takes a time proportional to the depth of inheritance (although I wouldn't advise more than one level of inheritance without a very strong reason, personally, for other reasons...). Definitely there are cases when you want to get at just the fields of the parent object as a performance optimisation... I think in that case the developer needs to be aware that this manager is an "expensive" one and they may wish to provide as an alternative the django-default "cheap" one.

#

carljm (on February 16, 2009):

Looks good. On the performance issue, I was more just thinking of a warning in a docstring (or even the snippet description). And since the snippet includes usage demonstration, maybe demonstrating the inclusion of a "standard" queryset.

What would be interesting is to explore writing a version of this that could pull down N objects of K different leaf types using only K+1 queries instead of N+1 (one query for the parent table and one for each leaf table, instead of one leaf query per object).

#

tug (on June 16, 2009):

I found a funny bug related to this snippet.

If u use this snippet and try to «dumpdata», you'll did not find any data from parent model.

#

niran (on December 14, 2009):

This doesn't work for me when using get() to retrieve a single model. For example, Meal.objects.get(name='Caesar Salad') would return a Meal instead of a Salad. Changing __iter__ to iterator fixes this, though I'm not entirely clear on the consequences of such a change.

#

niran (on December 14, 2009):

Note that both instances of __iter__ would need to be changed.

#

Nagyman (on May 18, 2010):

niran, after you call get(..), you end up with an instance of your base class. So you need to subsequently call as_leaf_class() to get at the subclass.

e.g. Meal.objects.get(name='Ceasar Salad').as_leaf_class()

#

joejaz (on September 15, 2010):

Correct me if I am wrong, but I think the super in the save method for Meal is indented too far. I had problems editing my models in the admin until I "un-dented" this line.

#

redglyph (on April 8, 2012):

I confirm what joejaz said, the save is obviously indented one step too far (it is correct in crucialfelix's original snippet).

So this should be like this (sorry for the bad code font used here):

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet

class SubclassingQuerySet(QuerySet):
    def __getitem__(self, k):
        result = super(SubclassingQuerySet, self).__getitem__(k)
        if isinstance(result, models.Model) :
            return result.as_leaf_class()
        else :
            return result
    def __iter__(self):
        for item in super(SubclassingQuerySet, self).__iter__():
            yield item.as_leaf_class()

class SubClassManager(models.Manager):
    def get_query_set(self):
        return SubclassingQuerySet(self.model)

class InheritableModel(models.Model):
    '''
    Abstract base class for inherited models, more efficient than standard scheme.
    Iterating through a base class inherited from InheritableModel directly yields the subclass elements.
    '''
    content_type = models.ForeignKey(ContentType, editable=False, null=True)
    objects = SubClassManager()

    def save(self, *args, **kwargs):
        if(not self.content_type):
            self.content_type = ContentType.objects.get_for_model(self.__class__)
        super(InheritableModel, self).save(*args, **kwargs)

    def as_leaf_class(self):
        content_type = self.content_type
        model = content_type.model_class()
        if (model == InheritableModel):
            return self
        return model.objects.get(id=self.id)

    class Meta:
        abstract = True

#

vdboor (on May 19, 2013):

Instead of using this snippet nowadays, I'd recommend looking at django-polymorphic. It supports model inheritance in a similar way and includes many other features including Django-admin integration :)

#

Please login first before commenting.