Sunday, July 4, 2021

Django ManyToMany Field

When should you use a ManyToManyField instead of a regular ForeignKey? To remember that, let's think about pizza. A pizza can have many toppings (a Hawaiian pizza usually has Canadian bacon and pineapple), and a topping can go on many pizzas (Canadian bacon also appears on meat lovers' pizzas). Since a pizza can have more than one topping, and a topping can go on more than one pizza, this is a great place to use a ManyToManyField.


from django.db import models 



class Pizza(models.Model):


    name = models.CharField(max_length=30)

    toppings = models.ManyToManyField('Topping')


    def __str__(self):

        return self.name



class Topping(models.Model):


    name = models.CharField(max_length=30)


    def __str__(self):

        return self.name




Both objects must exist in the database

You have to save a Topping in the database before you can add it to a Pizza, and vice versa. This is because a ManyToManyField creates an invisible "through" model that relates the source model (in this case Pizza, which contains the ManyToManyField) to the target model (Topping). In order to create the connection between a pizza and a topping, they both have to be added to this invisible "through" table



Below is what Django Doc says about through relationships 


" [T]here is … an implicit through model class you can use to directly access the table created to hold the association. It has three fields to link the models. If the source and target models differ, the following fields are generated:

  • id: the primary key of the relation.
  • <containing_model>_id: the id of the model that declares the ManyToManyField.
  • <other_model>_id: the id of the model that the ManyToManyField points to."



The invisible "through" model that Django uses to make many-to-many relationships work requires the primary keys for the source model and the target model. A primary key doesn't exist until a model instance is saved, so that's why both instances have to exist before they can be related. (You can't add spinach to your pizza if you haven't bought spinach yet, and you can't add spinach to your pizza if you haven't even started rolling out the crust yet either.)


i.e. below will give error because Topping is not yet saved. 


>> from pizzas.models import Pizza, Topping

>> hawaiian_pizza = Pizza.objects.create(name='Hawaiian')

>> pineapple = Topping(name='pineapple')

>> hawaiian_pizza.toppings.add(pineapple)

Traceback (most recent call last):

...

ValueError: Cannot add "<Topping: pineapple>": instance is on database "default", 

value is on database "None"

>> 



Below will correct this issue 


>> pineapple.save() 

>> hawaiian_pizza.toppings.add(pineapple)

>> hawaiian_pizza.toppings.all()

<QuerySet [<Topping: pineapple>]>



The reverse doesn't work either: I can't create a topping in the database, and then add it to a pizza that hasn't been saved.


>> pepperoni = Topping.objects.create(name='pepperoni')

>> pepperoni_pizza = Pizza(name='Pepperoni')

>> pepperoni_pizza.toppings.add(pepperoni)

Traceback (most recent call last):

...

ValueError: "<Pizza: Pepperoni>" needs to have a value for field "id" before this many-to-many 

relationship can be used.



To retrieve the stuff in a ManyToManyField, you have to use *_set ...

Since the field toppings is already on the Pizza model, getting all the toppings on a specific pizza is pretty straightforward.


That's because Django automatically refers to the target ManyToManyField objects as "sets." The pizzas that use specific toppings are in their own "set":


>> canadian_bacon.pizza_set.all()

<QuerySet [<Pizza: Hawaiian>]>


This can be mitigated by adding a related_name 


Adding the related_name option to a ManyToManyField will let you choose a more intuitive name to use when you want to retrieve the stuff in that field.


class Pizza(models.Model):

    ...

    toppings = models.ManyToManyField('Topping', related_name='pizzas')



The related_name should usually be the lowercase, plural form of your model name. This is confusing for some people because shouldn't the related_name for toppings just be… toppings?


No; the related_name isn't referring to how you want to retrieve the stuff in this field; it specifies the term you want to use instead of *_set when you're on the target object (which in this case is a topping) and want to see which source objects point to that target (what pizzas use a specific topping).

Without a related_name, we would retrieve all the pizzas that use a specific topping with pizza_set:



>> canadian_bacon.pizza_set.all()

<QuerySet [<Pizza: Hawaiian>]>




References:

https://www.revsys.com/tidbits/tips-using-djangos-manytomanyfield/#:~:text=The%20example%20used%20in%20the,ManyToManyField%20that%20points%20to%20Person%20.

No comments:

Post a Comment