[Django] C. R. U. D - ManyToManyField

Hailey Park·2021년 11월 17일
0

Django

목록 보기
6/10
post-thumbnail

Many to Many Relationship in database

To maintain a many-to-many relationship between two tables in a database, the only way is to have a third table which has references to both of those tables. This table is called a “through” table and each entry in this table will connect the source table (movies in this case) and the target table (actors in this case).

When you use a ManyToManyField, it creates a through model which is not visible to the ORM user and whenever one needs to fetch all of the movies that have a particular cast(actor) given only the name of the actor, the above 3 tables are joined.

Joining 3 tables may not be very efficient, so if you query this information using the actor ID instead of name, Django internally Joins only 2 tables (movies_actor and movies). These join operations are invisible to the user but it helps to know what’s going on in the database so that the queries can be made as efficient as possible.

Let’s look at how the ManyToManyField helps abstract out all of this complexity and provides a simple interface to the person writing code.

1) using related_name

2)using _set

[Result]

Using ManyToManyField

Running a database migration on these models will create a Movie table, a Actor table and a through table connecting the two. Let us now look at how we can access this information using the Django ORM.

>>> Emma_movie=Movie.objects.get(id=5)
>>> Emma_movie2=Movie.objects.get(id=6)
>>> Emma = Actor.objects.get(first_name="Emma")
>>> Emma_movie.actor.add(Emma)
>>> Emma_movie2.actor.add(Emma)

>>> Harry_potter = Movie.objects.get(id=5)
>>> Harry_potter.actor.all()
<QuerySet [<Actor: Actor object (1)>, <Actor: Actor object (4)>]>

Running movie.actor.all() gives us all the actors in the movie but if we want to perform a reverse action, i.e get all the movies that an actor performed in, this can be done by performing the same operation on the target object by using a _set.

>>> Angelina = Actor.objects.get(first_name="Angelina")
>>> Angelina.movie.all()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Actor' object has no attribute 'movie'
>>>
>>> Angelina.movie_set.all()
<QuerySet [<Movie: Movie object (1)>, <Movie: Movie object (2)>]>
>>>

As you can see, trying to perform a Angelina.movie.all() threw a AttributeError but Angelina.movie_set.all() worked. This is because Django internally references a target ManyToManyField as a set. This behaviour can be overridden by providing a related_name option to the target field as below.

Now we can perform the previous query using movies instead of movie_set.

>>> Angelina=Actor.objects.get(first_name="Angelina")
>>> Angelina.movies.all()
<QuerySet [<Movie: Movie object (1)>, <Movie: Movie object (2)>]>
>>>

Fetching specific movies and actors using each other

>>> Movie.objects.filter(actor__id=1)
<QuerySet [<Movie: Movie object (5)>, <Movie: Movie object (6)>]>
>>> Movie.objects.filter(actor__first_name="Angelina")
<QuerySet [<Movie: Movie object (1)>, <Movie: Movie object (2)>]>


>>> Actor.objects.filter(movies__id=1)
<QuerySet [<Actor: Actor object (3)>, <Actor: Actor object (5)>]>
>>> Actor.objects.filter(movies__title="Harry Potter")
<QuerySet [<Actor: Actor object (1)>, <Actor: Actor object (4)>]>

Using a custom “through” model

Even though Django takes care of creating the through model on its own and keeps this invisible to a user, sometimes it becomes necessary to use a custom through model in order to add some additional fields to that model.

Even though Django takes care of creating the through model on its own and keeps this invisible to a user, sometimes it becomes necessary to use a custom through model in order to add some additional fields to that model.

For instance consider the relationship between a student and a teacher. A teacher can teach multiple students and a student can be taught by multiple teachers thereby qualifying this for a many to many relationship.

However in this case just having a table that connects these two entities won’t suffice because we would require extra information such as:

  • The date on which a teacher started teaching a student.
  • The subject that is taught by a teacher to a student.
  • Duration of the course.

To sum this up, we require a “course” table that not only connects a student and a teacher but also holds this extra information.

To make this happen, one must override the default though table that Django creates and use a custom through table instead.

Creating a custom through model:

from django.db import models

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

    def __str__(self):
        return self.name

class Sandwich(models.Model):
    name = models.CharField(max_length=100)
    sauces = models.ManyToManyField(Sauce, through='SauceQuantity')

    def __str__(self):
        return self.name

class SauceQuantity(models.Model):
    sauce = models.ForeignKey(Sauce, on_delete=models.CASCADE)
    sandwich = models.ForeignKey(Sandwich, on_delete=models.CASCADE)
    extra_sauce = models.BooleanField(default=False)

    def __str__(self):
        return "{}_{}".format(self.sandwich.__str__(), self.sauce.__str__())

With a custom through model you will not be able to add sauces to a Sandwich like you did before. Instead you would have to create entries of the SauceQuantity model explicitly as shown below.

>>> from sandwiches.models import *
>>> 
>>> 
>>> chicken_teriyaki_sandwich = Sandwich.objects.create(name="Chicken Teriyaki with mayo and extra bbq sauce")
>>> 
>>> 
>>> bbq_sauce = Sauce.objects.create(name="Barbeque")
>>> 
>>> SauceQuantity.objects.create(sandwich=chicken_teriyaki_sandwich, sauce=bbq_sauce, extra_sauce=True)
<SauceQuantity: Chicken Teriyaki with mayo and extra bbq sauce_Barbeque>
>>> 
>>> SauceQuantity.objects.create(sandwich=chicken_teriyaki_sandwich, sauce=mayo_sauce, extra_sauce=False)
<SauceQuantity: Chicken Teriyaki with mayo and extra bbq sauce_Mayonnaise>
>>>

You can still access a sauce from a sandwich and a sandwich from a sauce just like you previously did.


>>> 
>>> chicken_teriyaki_sandwich.sauces.all()
<QuerySet [<Sauce: Barbeque>, <Sauce: Mayonnaise>]>
>>> 
>>> bbq_sauce.sandwich_set.all()
<QuerySet [<Sandwich: Chicken Teriyaki with mayo and extra bbq sauce>]>
>>> 
>>> 

In order to know what all sauces are being used on a sandwich and in what quantities, we can iterate through the sauces of a Sandwich and retrieve information from the SauceQuantity model for each of the sauces as shown below.

>>> 
>>> 
>>> for sauce in chicken_teriyaki_sandwich.sauces.all():
...  saucequantity = SauceQuantity.objects.get(sauce=sauce, sandwich=chicken_teriyaki_sandwich)
...  print("{}{}".format("Extra " if saucequantity.extra_sauce else "", sauce))
... 
Extra Barbeque
Mayonnaise
>>> 

The SauceQuantity model can also be extended further to include stuff like whether or not the sandwich is cut in half, type of bread used, etc.

The advantages of using ManyToManyField instead of using ForeignKey

1) We don't have to create the junction table. Django automatically creates the table.

2) We can add the connected value right away from the Movie and Actor objects while we should add the value through the junction table when we use ForeighKey.

3) The process of getting the information from the junction table is way more simple than the case of using ForeignKey.

Resource
https://www.sankalpjonna.com/learn-django/the-right-way-to-use-a-manytomanyfield-in-django

Should read
https://docs.djangoproject.com/en/3.2/topics/db/examples/many_to_many/

profile
I'm a deeply superficial person.

0개의 댓글