[Fixed]-Django: Order a model by a many-to-many field


Django 1.1 (currently beta) adds aggregation support. Your query can be done with something like:

from django.db.models import Max

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):

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.

Leave a comment