Django аннотирует объект M2M полем из сквозной модели

Допустим, у меня есть следующие модели:

class Color(models.Model):
    name = models.CharField(max_length=255, unique=True)
    users = models.ManyToManyField(User, through="UserColor", related_name="colors")


class UserColor(models.Model):
    class Meta:
        unique_together = (("user", "color"), ("user", "rank"))

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    color = models.ForeignKey(Color, on_delete=models.CASCADE)
    rank = models.PositiveSmallIntegerField()

Я хочу получить всех пользователей из базы данных с их соответствующими цветами и рангами цветов. Я знаю, что могу сделать это, обратившись к сквозной модели, которая делает в общей сложности 3 обращения к БД:

users = User.objects.prefetch_related(
    Prefetch(
        "usercolor_set",
        queryset=UserColor.objects.order_by("rank").prefetch_related(
            Prefetch("color", queryset=Color.objects.only("name"))
        ),
    )
)

for user in users:
    for usercolor in user.usercolor_set.all():
        print(user, usercolor.color.name, usercolor.rank)

Я обнаружил другой способ сделать это, аннотировав ранг на объекты Color, что имеет смысл, поскольку у нас есть уникальное ограничение на пользователя и цвет.

users = User.objects.prefetch_related(
    Prefetch(
        "colors",
        queryset=(
            Color.objects.annotate(rank=F("usercolor__rank"))
            .order_by("rank")
            .distinct()
        ),
    )
)

for user in users:
    for color in user.colors.all():
        print(user, color, color.rank)

Такой подход дает несколько преимуществ:

  • Делает только 2 DB удара вместо 3.
  • Не нужно иметь дело со сквозным объектом, что, на мой взгляд, более интуитивно понятно.

Однако, это работает только если я выстраиваю цепочку distinct() (иначе я получаю дубликаты объектов), и я беспокоюсь, что это не совсем законный подход (возможно, я просто придумал хак, который может работать не во всех случаях).

Итак, законно ли второе решение? Есть ли лучший способ? Или мне лучше придерживаться первого?

Вернуться на верх