17👍
There are two kinds of model subclassing in Django – Abstract Base Classes; and Multi-Table inheritance.
Abstract Base Classes aren’t ever used by themselves, and do not have a database table or any form of identification. They are simply a way of shortening code, by grouping sets of common fields in code, not in the database.
For example:
class Address(models.Model):
street = ...
city = ...
class Meta:
abstract = True
class Employee(Address):
name = ...
class Employer(Address):
employees = ...
company_name = ...
This is a contrived example, but as you can see, an Employee
isn’t an Address
, and neither is an Employer
. They just both contain fields relating to an address. There are only two tables in this example; Employee
, and Employer
– and both of them contain all the fields of Address. An employer address can not be compared to an employee address at the database level – an address doesn’t have a key of its own.
Now, with multi-table inheritance, (remove the abstract=True from Address), Address does have a table all to itself. This will result in 3 distinct tables; Address
, Employer
, and Employee
. Both Employer and Employee will have a unique foreign key (OneToOneField) back to Address.
You can now refer to an Address without worrying about what type of address it is.
for address in Address.objects.all():
try:
print address.employer
except Employer.DoesNotExist: # must have been an employee
print address.employee
Each address will have its own primary key, which means it can be saved in a fourth table on its own:
class FakeAddresses(models.Model):
address = models.ForeignKey(Address)
note = ...
Multi-table Inheritance is what you’re after, if you need to work with objects of type Post
without worrying about what type of Post it is. There will be an overhead of a join if accessing any of the Post fields from the subclass; but the overhead will be minimal. It is a unique index join, which should be incredibly quick.
Just make sure, that if you need access to the Post
, that you use select_related
on the queryset.
Events.objects.select_related(depth=1)
That will avoid additional queries to fetch the parent data, but will result in the join occurring. So only use select related if you need the Post.
Two final notes; if a Post can be both an Announcement AND an Event, then you need to do the traditional thing, and link to Post via a ForeignKey. No subclassing will work in this case.
The last thing is that if the joins are performance critical between the parent and the children, you should use abstract inheritance; and use Generic Relations to refer to the abstract Posts from a table that is much less performance critical.
Generic Relations essentially store data like this:
class GenericRelation(models.Model):
model = ...
model_key = ...
DeletedPosts(models.Model):
post = models.ForeignKey(GenericRelation)
That will be a lot more complicated to join in SQL (django helps you with that), but it will also be less performant than a simple OneToOne join. You should only need to go down this route if the OneToOne joins are severely harming performance of your application which is probably unlikely.
2👍
Generic relationships and foreign keys are your friend in your path to succeed. Define an intermediate model where one side is generic, then the other side will get a related list of polymorphic models. It’s just a little more complicated than a standard m2m join model, in that the generic side has two columns, one to ContentType (actually a FK) and the other to the PK of the actual linked model instance. You can also restrict the models to be linked with using standard FK parameters.
You’ll get used with it quickly.
(now that I get an actual keyboard to write with, here there is the example:)
class Post(models.Model):
class Meta: abstract = True
CONCRETE_CLASSES = ('announcement', 'event',)
removed_from = generic.GenericRelation('OwnerRemovedPost',
content_type_field='content_type',
object_id_field='post_id',
)
class Announcement(Post): pass
class Event(Post): pass
class Owner(models.Model):
# non-polymorphic m2m
added_events = models.ManyToManyField(Event, null=True)
# polymorphic m2m-like property
def removed_posts(self):
# can't use ManyToManyField with through.
# can't return a QuerySet b/c it would be a union.
return [i.post for i in self.removed_post_items.all()]
def removed_events(self):
# using Post's GenericRelation
return Event.objects.filter(removed_from__owner=self)
class OwnerRemovedPost(models.Model):
content_type = models.ForeignKey(ContentType,
limit_choices_to={'name__in': Post.CONCRETE_CLASSES},
)
post_id = models.PositiveIntegerField()
post = generic.GenericForeignKey('content_type', 'post_id')
owner = models.ForeignKey(Owner, related_name='removed_post_items')
class Meta:
unique_together = (('content_type', 'post_id'),) # to fake FK constraint
You can’t filter into the related collection like a classic many-to-many, but with the proper methods in Owner
, and using the concrete classes’ managers smartly, you get everywhere you want.