Tom Brock
Tom Brock

Reputation: 950

How to get the count of the many-to-many keys through a reverse relationship?

I hope I can get your advice on this.

If I have a Pizza model with a many-to-many relationship to topping, I can easily get the count of the toppings on the pizza.

# models.py
class Pizza(models.Model):
    toppings = models.ManyToManyField(Topping)

class Topping(models.Model):
    name = models.CharField(max_length=255, null=True, blank=True)

# admin.py
@admin.register(Pizza)
class PizzaAdmin(admin.ModelAdmin):
    list_display = ['topping_count']

    def topping_count(self, pizza):
        return pizza.toppings.count()

However, if I move the many-to-many forward relationship to Topping, this all breaks.

# models.py
class Pizza(models.Model):
    pass

class Topping(models.Model):
    pizzas = models.ManyToManyField(Pizza)

How can I update topping_count in PizzaAdmin so I'm able to get the toppings count via the reverse relationship?

Thank you.

Upvotes: 0

Views: 79

Answers (3)

The way you count your toppings is inefficient in that it does a separate DB query for each topping. I would suggest making use of annotate:

@admin.register(Pizza)
class PizzaAdmin(admin.ModelAdmin):
    list_display = ['topping_count']

    def get_queryset(self, request):
        qs = super(PizzaAdmin, self).get_queryset(request)
        return qs.annotate(topping_count=Count('topping_set'))            

    def topping_count(self, pizza):
        return pizza.topping_count

Explanation: annotate will set topping_count for each Pizza in admin. Then your PizzaAdmin.topping_count will just retrieve the value of topping_count from the instance.

That way it should touch your database only once regarless of the number of pizzas.

Upvotes: 1

Amyth
Amyth

Reputation: 32949

Get the count of the reverse relationship as pizza.topping_set.count()

Upvotes: 1

knbk
knbk

Reputation: 53659

The reverse relation uses the lowercased model name + _set:

@admin.register(Pizza)
class PizzaAdmin(admin.ModelAdmin):
    list_display = ['topping_count']

    def topping_count(self, pizza):
        return pizza.topping_set.count()

Alternatively, you can keep the current admin code and change the related name:

class Topping(models.Model):
    pizzas = models.ManyToManyField(Pizza, related_name='toppings')

Upvotes: 2

Related Questions