0

Let's say I have the following models:

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

I want to fetch all users from the database with their respective colors and color ranks. I know I can do this by traversing across the through model, which makes a total of 3 DB hits:

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)

I discovered another way to do this by annotating the rank onto the Color objects, which makes sense because we have a unique constraint on user and 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)

This approach comes with several benefits:

  • Makes only 2 DB hits instead of 3.
  • Don't have to deal with the through object, which I think is more intuitive.

However, it only works if I chain distinct() (otherwise I get duplicate objects) and I'm worried this may not be a legit approach (maybe I just came up with a hack that may not work in all cases).

So is the second solution legit? Is there a better way to it? Or should I stick to the first one?

Johnny Metz
  • 5,977
  • 18
  • 82
  • 146
  • Related question: https://stackoverflow.com/questions/63247172/django-annotate-queryset-by-field-from-m2m-through-model – Johnny Metz Sep 09 '22 at 21:20

0 Answers0