Django now supports Model Inheritance, and one of the coolest new opportunities that model inheritance brings is the possibility of the creation of mixins, so in this post I'll walk through the steps I went through to create some simple examples. This is just an excercise (although it could be modified to be more robust)--and right now there are better ways to achieve all of the effects of the following mixins. (See django-mptt, for example).
Model and Field Setup
First let's just set up two basic models. The first will be our mixin, NaiveHierarchy, which has a single field, parent, which is a reference to itself. Using this, we can traverse the tree and find all sorts of fun hierarchical information. Also, we'll create the canonical example model: the blog post. Our models start out looking something like this:
from django.db import models
class NaiveHierarchy(models.Model):
parent = models.ForeignKey('self', null=True)
class Meta:
abstract = True
class BlogPost(NaiveHierarchy):
title = models.CharField(max_length = 128)
body = models.TextField()
def __unicode__(self):
return self.title
Now let's test to make sure that worked. We'll create some data and test that parent exists on the instances.
>>> from mixins.models import BlogPost
>>> bp = BlogPost.objects.create(title="post1", body="First post!")
>>> bp2 = BlogPost.objects.create(title="post2", body="Second post!", parent=bp)
>>> bp3 = BlogPost.objects.create(title="post3", body="Third post!", parent=bp2)
>>> bp.parent
>>> bp2.parent
<BlogPost: post1>
Inherited Class-Level Methods
So as you can see, everything is working correctly! But that really doesn't save us much time yet, as it's fairly easy to copy and paste fields onto new models, and we still have to write methods which take advantage of those new fields. In this case, I already know that I'm going to want to get the related children and descendants of my blogposts. So why not write those methods on the abstract model? Thanks to inheritance, those methods will apply to the new model as well.
class NaiveHierarchy(models.Model):
parent = models.ForeignKey('self', null=True)
def get_children(self):
return self._default_manager.filter(parent=self)
def get_descendants(self):
descs = set(self.get_children())
for node in list(descs):
descs.update(node.get_descendants())
return descs
class Meta:
abstract = True
Now, getting all the children or descendents of a particular node is easy:
>>> bp.get_children()
[<BlogPost: post2>]
>>> bp.get_descendants()
set([<BlogPost: post2>, <BlogPost: post3>])
Now this NaiveHierarchy mixin is starting to become quite useful! But what happens if I want to get all of the BlogPosts that have no parents? It's really manager-level functionality. So let's write a manager which defines a get_roots function. Unfortunately, using abstract managers doesn't quite work yet (it works for non-abstract inheritance), but it probably will in future versions of Django. In fact, by applying the latest patch on either Django ticket 7252 or 7154, it will work today. Let's see how this would look:
class NaiveHierarchyManager(models.Manager):
def get_roots(self):
return self.get_query_set().filter(parent__isnull=True)
class NaiveHierarchy(models.Model):
parent = models.ForeignKey('self', null=True)
tree = NaiveHierarchyManager()
def get_children(self):
return self._default_manager.filter(parent=self)
def get_descendants(self):
descs = set(self.get_children())
for node in list(descs):
descs.update(node.get_descendants())
return descs
class Meta:
abstract = True
class BlogPost(NaiveHierarchy):
title = models.CharField(max_length = 128)
body = models.TextField()
objects = models.Manager()
def __unicode__(self):
return self.title
Note that we needed to explicitly define objects as the basic manager, because once a parent class specifies a manager, it gets set as the default manager on all inherited subclasses. This would play out exactly how you would expect:
>>> BlogPost.tree.get_roots()
[<BlogPost: post1>]
>>> BlogPost.tree.all()
[<BlogPost: post1>, <BlogPost: post2>, <BlogPost: post3>]
Advanced Stuff
So now I really wanted to push the limit, and write a mixin which would enhance one of the basic methods of all Model classes: save(). This would be a DateMixin which would contain date_added and date_modified, where date_modified was updated on each save. To my surprise, this Just Worked. Let's see the final result:
import datetime
from django.db import models
class DateMixin(models.Model):
date_added = models.DateTimeField(default=datetime.datetime.now)
date_modified = models.DateTimeField()
def save(self):
self.date_modified = datetime.datetime.now()
super(DateMixin, self).save()
class NaiveHierarchyManager(models.Manager):
def get_roots(self):
return self.get_query_set().filter(parent__isnull=True)
class NaiveHierarchy(models.Model):
parent = models.ForeignKey('self', null=True)
tree = NaiveHierarchyManager()
def get_children(self):
return self._default_manager.filter(parent=self)
def get_descendants(self):
descs = set(self.get_children())
for node in list(descs):
descs.update(node.get_descendants())
return descs
class Meta:
abstract = True
class BlogPost(NaiveHierarchy, DateMixin):
title = models.CharField(max_length = 128)
body = models.TextField()
objects = models.Manager()
def __unicode__(self):
return self.title
Conclusions
Mixins can be powerful tools, but there are some hazards in using mixins, which all boil down to the same basic problem: unexpected consequences. In the case of the DateMixin, if any other class has defined a save() method, our custom save() method simply won't be called unless called explicitly. Perhaps this is a documentation problem, but perhaps it's a fault in the idea of a date mixin altogether.
So all that being said, I'm not suggesting to go off and start using any of the mixins that I have provided here, but rather to illustrate how a mixin can be constructed with Django's new Model Inheritance. I do hope that a reusable app emerges with some great mixins that are useful for a large variety of tasks. Because mixins are powerful, and new shiny things that Django can do, and new shiny things are worth being explored!
All Content


You might want to split up the double Meta: abstract = True in the last example.
Also, the DateAddedMixin is referenced in the first example, but it's not defined yet.
Good ideas overall! Model inheritance looks very good, I can see lots of "plugins" being created, in the :acts_as_* fashion that Rails has...
Whoops! Good catches, thanks. Fixing now.
I came from a Symfony background and am glad to see this kind of functionality in Django :)
It is quite similar in purpose to propel behaviors for Symfony :)
model inheritance would really help me a lot in designing models!
what about adding another method datemixin_save that is called by default? clients can override the save method and only need to call the mixin save method of the parent class... easy enough, but not tested.
You know, that's a really great idea. This could be done automatically with a small patch to Model.save(). Maybe I'll write up a patch later!
eric, great post. looking forward to some interesting implementations in the future.
also, we're talking model inheritance this week at django-nyc meetup, and we now have another great reference/discussion point.
cheers!
Could someone show how to use the DateMixin alone?
You just subclass it alone, the behavior is self contained.
You don't give the DateMixin enough credit. I've been using something very much like that in production ever since qs-rf merged. Anytime any model overrides save() it should use super() to call the default save() from models.Model. If that model is now a subclass of DateMixin instead of Model, the identical super() call will call DateMixin.save(), which will in turn call Model.save() - in other words, it all just works. The subclass doesn't need to do anything different than it would if subclassing Model normally.
Nice example! An excellent way to stay DRYer in the model.
BlogPost.tree.get_roots() generates an attribute error for me on django revision 7538, anybody experiencing this...
AttributeError: 'Options' object has no attribute '_join_cache' happens in
'''
/usr/local/lib/python2.5/site-packages/django/db/models/sql/query.py in setup_joins(self, names, opts, alias, dupe_multis, allow_many, allow_explicit_fk, can_reuse)
1121 opts.pk.column), exclusions=joins)
1122 joins.append(alias)
-> 1123 cached_data = opts._join_cache.get(name)
1124 orig_opts = opts
1125
'''
Yes, this a result of the manager which doesn't yet work with Django trunk. I still suggest checking out
http://code.djangoproject.com/ticket/7252
and http://code.djangoproject.com/ticket/7154
BlogPost.tree.get_roots() does work when tree = NaiveHierarchyManager() is defined under BlogPost
undersleep piaffe scotomata methine anisotropism seres antihysteric unpoisonable
<a href= http://freebizadsweb.com/tablemaker/tablemaker.asp >Button Maker</a>
http://www.cleversley.com/
undersleep piaffe scotomata methine anisotropism seres antihysteric unpoisonable
<a href= http://www.theoxfordgroup.com/ >The Oxford Group</a>
http://www.winspec.com/
undersleep piaffe scotomata methine anisotropism seres antihysteric unpoisonable
<a href= http://www.creative.com.cy/ >Creative Tours</a>
http://www.thisisthenortheast.co.uk/the_north_east/newton_aycliffe