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
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
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.
#
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.
#
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).
#
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.
#
This doesn't work for me when using
get()
to retrieve a single model. For example,Meal.objects.get(name='Caesar Salad')
would return aMeal
instead of aSalad
. Changing__iter__
toiterator
fixes this, though I'm not entirely clear on the consequences of such a change.#
Note that both instances of
__iter__
would need to be changed.#
niran, after you call
get(..)
, you end up with an instance of your base class. So you need to subsequently callas_leaf_class()
to get at the subclass.e.g.
Meal.objects.get(name='Ceasar Salad').as_leaf_class()
#
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.
#
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):
#
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.