darkhorse
darkhorse

Reputation: 8722

Checking if an M2M field is empty or not in Django

I have the following models:

class Pizza(models.Model):
    toppings = models.ManyToManyField(Topping)
    has_toppings = models.BooleanField(default=False)

    def check_if_there_are_toppings(self):
         if len(self.toppings.all()) > 0:
             self.has_toppings = True

@receiver(m2m_changed, sender=Pizza.toppings.through)
def toppings_changed(sender, instance, **kwargs):
    instance.check_if_there_are_toppings()
    instance.save()

What I want to do is update the has_toppings field whenever the toppings length is more than 0. What would be the correct way to do this? Thanks for any help.

Upvotes: 1

Views: 718

Answers (2)

willeM_ Van Onsem
willeM_ Van Onsem

Reputation: 476547

I think it is probably not a good idea to do that. There can be a lot of reasons how a many-to-many relation changes. It will be very hard, or even impossible to cover all of these. For example a Topping object itself can be removed, and therefore trigger a change in all the many-to-many relations where the topping was used. Furthermore the Django ORM has some functions like .bulk_create(…) [Django-doc] and .update(…) [Django-doc] that circumvent Django's signal mechanism, and thus can render the database in an inconsistent state. Therefore it might make more sense to just remove the has_toppings field:

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

It might make more sense to just annotate your Pizza querysets. For example with:

from django.db.models import Exists, OuterRef

Pizza.objects.annotate(
    has_toppings=Exists(
        Pizza.toppings.through.objects.filter(pizza_id=OuterRef('pk'))
    )
)

This will generate a query that looks like:

SELECT pizza.id,
       EXISTS(
           SELECT U0.id, U0.pizza_id, U0.topping_id
           FROM pizza_toppings U0
           WHERE U0.pizza_id = pizza.id
       ) AS has_toppings
FROM pizza

You can use this queryset when you access Pizza.objects, by setting a manager:

from django.db.models import Exists, OuterRef

class PizzaManager(models.Manager):
    def get_queryset(self):
        return Pizza.objects.annotate(
            has_toppings=Exists(
                Pizza.toppings.through.objects.filter(pizza_id=OuterRef('pk'))
            )
        )

class Topping(models.Model):
    pass

class Pizza(models.Model):
    toppings = models.ManyToManyField(Topping)
    objects = PizzaManager()

So we can now for example retrieve all Pizzas with toppings with:

Pizza.objects.filter(has_toppings=True)

Upvotes: 4

Giorgi Jambazishvili
Giorgi Jambazishvili

Reputation: 743

You can to it simply checking count of the toppings, by calling:

is_empty = instance.toppings.all().count() == 0

is_empty would have False if there are more than 0 toppings, and True if there are no toppings.

Hope, it helps.

Upvotes: 1

Related Questions