I am writing a Django application that has a model for People, and I have hit a snag. I am assigning Role objects to people using a Many-To-Many relationship – where Roles have a name and a weight. I wish to order my list of people by their heaviest role’s weight. If I do People.objects.order_by(‘-roles__weight’), then I get duplicates when people have multiple roles assigned to them.
My initial idea was to add a denormalized field called heaviest-role-weight – and sort by that. This could then be updated every time a new role was added or removed from a user. However, it turns out that there is no way to perform a custom action every time a ManyToManyField is updated in Django (yet, anyway).
So, I thought I could then go completely overboard and write a custom field, descriptor and manager to handle this – but that seems extremely difficult when the ManyRelatedManager is created dynamically for a ManyToManyField.
I have been trying to come up with some clever SQL that could do this for me – I’m sure it’s possible with a subquery (or a few), but I’d be worried about it not being compatible will all the database backends Django supports.
Has anyone done this before – or have any ideas how it could be achieved?
Django 1.1 (currently beta) adds aggregation support. Your query can be done with something like:
from django.db.models import Max People.objects.annotate(max_weight=Max('roles__weight')).order_by('-max_weight')
This sorts people by their heaviest roles, without returning duplicates.
The generated query is:
SELECT people.id, people.name, MAX(role.weight) AS max_weight FROM people LEFT OUTER JOIN people_roles ON (people.id = people_roles.people_id) LEFT OUTER JOIN role ON (people_roles.role_id = role.id) GROUP BY people.id, people.name ORDER BY max_weight DESC
Here’s a way to do it without an annotation:
class Role(models.Model): pass class PersonRole(models.Model): weight = models.IntegerField() person = models.ForeignKey('Person') role = models.ForeignKey(Role) class Meta: # if you have an inline configured in the admin, this will # make the roles order properly ordering = ['weight'] class Person(models.Model): roles = models.ManyToManyField('Role', through='PersonRole') def ordered_roles(self): "Return a properly ordered set of roles" return self.roles.all().order_by('personrole__weight')
This lets you say something like:
>>> person = Person.objects.get(id=1) >>> roles = person.ordered_roles()
Something like this in SQL:
select p.*, max (r.Weight) as HeaviestWeight from persons p inner join RolePersons rp on p.id = rp.PersonID innerjoin Roles r on rp.RoleID = r.id group by p.* order by HeaviestWeight desc
Note: group by p.* may be disallowed by your dialect of SQL. If so, just list all the columns in table p that you intend to use in the select clause.
Note: if you just group by p.ID, you won’t be able to call for the other columns in p in your select clause.
I don’t know how this interacts with Django.