How can I access a ManyToManyField in the save method of a model?

Viewed 452

I know that there's no way I'm the first to ask this question, but I cannot find the answer so please don't eat me.

If I have a model with a field that is a ManyToManyField; I know that I can get the selected objects prior to the save but this isn't helpful if the selected objects have changed.

models.py:

class AddOn(models.Model):
    name = models.CharField(max_length=100)


class Invoice(models.Model):
    additional_options = models.ManyToManyField(AddOn)

    def save(self, *args, **kwargs):
        super().save()
        # this prints the objects that were selected prior to the save.
        # this isn't helpful if the selected objects have changed.
        print(self.additional_options.all())
1 Answers

Based on the way you worded your question, I think you are confused about what is actually happening behind the scenes with a ManyToManyField (I will refer to this as M2M from now on), so I will attempt to explain it, with the following models:

class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

When we declare a M2M field, behind the scenes Django creates a through model which in this case looks like this:

class ThroughModel(models.Model):
    topping = models.ForeignKey(Topping)
    pizza models.ForeignKey(Pizza)

This is used to link the models together, and isn't stored in the database, unless you use the through keyword in the M2M field to create your own.

Before you can actually link the Pizza and Topping models in the M2M, you must already have instances saved:

>>> t1 = Topping(name='pepperoni')
>>> t2 = Topping(name='sausage')
>>> t3 = Topping(name='ham')
>>> Topping.objects.bulk_create([
>>> t1, t2, t3
>>> ])
>>>
>>> pizza = Pizza(name='meat lovers')

Now, how can we add data to the M2M field? Like this:

>>> pizza.toppings.add(t1, t2, t3)

But as is, this would raise:

ValueError: 'Pizza' instance needs to have a primary key value before a many-to-many relationship can be used.

Because the pizza instance wasn't saved.

>>> pizza.save()
>>> pizza.toppings.add(t1, t2, t3)
>>>
>>> print('no error thrown now!')

You can also add toppings by passing in a primary key instead of an instance:

>>> pk = Toppings.objects.first().pk
>>> pk
1
>>> pizza = Pizza.objects.create(name='pepperoni pizza')
>>> pizza.toppings.add(pk)

Finally, to get the toppings from a given pizza instance:

>>> pizza.toppings.all()
<QuerySet [<Toppings: Toppings object (1)>, <Toppings: Toppings object (2)>, <Toppings: Toppings object (3)>]>

It's important to note that fetching the values from a M2M require 2 database queries, and there really isn't a way around this.

Lastly, to actually answer your question, you can do something like this to access a M2M field inside of save:

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

    def save(self, *args): # args will be M2M data
        super().save() # now that the instance is saved, we can access toppings
        if args:
            self.toppings.add(*args)

        print(self.toppings.all())

Example usage:

>>> pizza = Pizza(name='sausage and ham')
>>> pizza.save(2, 3)
<QuerySet [<Toppings: Toppings object (2)>, <Toppings: Toppings object (3)>]>
# this is from the print inside of save

My example models come from the prefetch_related section of the docs, but I'd recommend reading through this section if you want to learn more about ManyToManyFields.

Related