Django: упорядочить модель по полю "многие ко многим"

Я пишу приложение Django, у которого есть модель для People, и я попал в ловушку. Я назначаю объекты Role для людей, использующих отношения "Много-ко-многим", где роли имеют имя и вес. Я хочу заказать свой список людей по самому тяжелому весу. Если я делаю People.objects.order_by ('- role__weight'), то я получаю дубликаты, когда у людей есть несколько назначенных им ролей.

Моя первоначальная идея заключалась в том, чтобы добавить денормализованное поле, называемое тяжелее-ролевым весом, и сортировать по нему. Затем это можно было обновить каждый раз, когда новая роль была добавлена ​​или удалена пользователем. Однако выясняется, что нет никакого способа выполнить пользовательское действие каждый раз, когда ManyToManyField обновляется в Django (пока, в любом случае).

Итак, я думал, что смогу тогда полностью переборщить и написать собственное поле, дескриптор и менеджер, чтобы справиться с этим, но это кажется чрезвычайно сложным, когда ManyRelatedManager создается динамически для ManyToManyField.

Я пытаюсь придумать какой-нибудь умный SQL, который мог бы сделать это для меня - я уверен, что это возможно с подзапросом (или несколькими), но я был бы обеспокоен тем, что он не был совместимым. базы данных поддерживает Django.

Кто-нибудь сделал это раньше - или есть идеи, как это можно было бы достичь?

Ответ 1

Django 1.1 (в настоящее время бета) добавляет поддержку aggregation. Ваш запрос может быть выполнен с помощью:

from django.db.models import Max
People.objects.annotate(max_weight=Max('roles__weight')).order_by('-max_weight')

Это сортирует людей по их самым тяжелым ролям, не возвращая дубликатов.

Сгенерированный запрос:

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

Ответ 2

Здесь можно сделать это без аннотации:

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

Это позволяет вам сказать что-то вроде:

>>> person = Person.objects.get(id=1)
>>> roles = person.ordered_roles()

Ответ 3

Что-то вроде этого в 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

Примечание: группа по p. * может быть запрещена вашим диалектом SQL. Если это так, просто перечислите все столбцы таблицы p, которые вы собираетесь использовать в предложении select.

Примечание. Если вы просто группируете p.ID, вы не сможете вызвать другие столбцы в p в предложении select.

Я не знаю, как это взаимодействует с Django.