Many to Many
In this relationship, multiple records in a table can have a relationship with multiple records in a different table. For example, a book can have multiple co-authors and each author (contributor) could have written multiple books. So, this forms a many-to-many relationship between the Book
and Contributor
tables:
In models.py
, for the Book model, add the last line as shown here:
class Book(models.Model): """A published book.""" title = models.CharField\ (max_length=70, \ help_text="The title of the book.") publication_date = models.DateField\ (verbose_name=\ "Date the book was published.") isbn = models.CharField\ (max_length=20, \ verbose_name="ISBN number of the book.") publisher = models.ForeignKey\ (Publisher, on_delete=models.CASCADE) contributors = models.ManyToManyField\ ('Contributor', through="BookContributor")
The newly added contributors field establishes a many-to-many relationship with Book and Contributor using the ManyToManyField field type:
models.ManyToManyField
: This is the field type to establish a many-to-many relationship.through
: This is a special field option for many-to-many relationships. When we have a many-to-many relationship across two tables, if we want to store some extra information about the relationship, then we can use this to establish the relationship via an intermediary table.
For example, we have two tables, namely Book
and Contributor
, where we need to store the information on the type of contributor for the book, such as Author, Co-author, or Editor. Then the type of contributor is stored in an intermediary table called BookContributor
. Here is how the BookContributor
table/model looks. Make sure you include this model in reviews/models.py
:
class BookContributor(models.Model): class ContributionRole(models.TextChoices): AUTHOR = "AUTHOR", "Author" CO_AUTHOR = "CO_AUTHOR", "Co-Author" EDITOR = "EDITOR", "Editor" book = models.ForeignKey\ (Book, on_delete=models.CASCADE) contributor = models.ForeignKey\ (Contributor, \ on_delete=models.CASCADE) role = models.CharField\ (verbose_name=\ "The role this contributor had in the book.", \ choices=ContributionRole.choices, max_length=20)
Note
The complete models.py
file can be viewed at this link: http://packt.live/3hmFQxn.
An intermediary table such as BookContributor
establishes relationships by using foreign keys to both the Book
and Contributor
tables. It can also have extra fields that can store information about the relationship the BookContributor
model has with the following fields:
book
: This is a foreign key to theBook
model. As we saw previously,on_delete=models.CASCADE
will delete an entry from the relationship table when the relevant book is deleted from the application.Contributor
: This is again a foreign key to theContributor
model/table. This is also defined asCASCADE
upon deletion.role
: This is the field of the intermediary model, which stores the extra information about the relationship betweenBook
andContributor
.class ContributionRole(models.TextChoices)
: This can be used to define a set of choices by creating a subclass ofmodels.TextChoices
. For example,ContributionRole
is a subclass created out ofTextChoices
, which is used by the roles field to define Author, Co-Author, and Editor as a set of choices.choices
: This refers to a set of choices defined in the models, and they are useful when creating DjangoForms
using the models.Note
When the through field option is not provided while establishing a many-to-many relationship, Django automatically creates an intermediary table to manage the relationship.
One-to-One Relationships
In this relationship, one record in a table will have a reference to only one record in a different table. For example, a person can have only one driver's license, so a person to their driver's license could form a one-to-one relationship:
The OneToOneField can be used to establish a one-to-one relationship, as shown here:
class DriverLicence(models.Model): person = models.OneToOneField\ (Person, on_delete=models.CASCADE) licence_number = models.CharField(max_length=50)
Now that we have explored database relationships, let's come back to our bookr application and add one more model there.
Adding the Review Model
We've already added the Book
and Publisher
models to the reviews/models.py
file. The last model that we are going to add is the Review
model. The following code snippet should help us do this:
from django.contrib import auth class Review(models.Model): content = models.TextField\ (help_text="The Review text.") rating = models.IntegerField\ (help_text="The rating the reviewer has given.") date_created = models.DateTimeField\ (auto_now_add=True, \ help_text=\ "The date and time the review was created.") date_edited = models.DateTimeField\ (null=True, \ help_text=\ "The date and time the review was last edited.") creator = models.ForeignKey\ (auth.get_user_model(), on_delete=models.CASCADE) book = models.ForeignKey\ (Book, on_delete=models.CASCADE, \ help_text="The Book that this review is for.")
Note
The complete models.py
file can be viewed at this link: http://packt.live/3hmFQxn.
The review
model/table will be used to store user-provided review comments and ratings for books. It has the following fields:
content
: This field stores the text for a book review, hence the field type used isTextField
as this can store a large amount of text.rating
: This field stores the review rating of a book. Since the rating is going to be an integer, the field type used isIntegerField
.date_created
: This field stores the time and date when the review was written, hence the field type isDateTimeField
.date_edited
: This field stores the date and time whenever a review is edited. The field type is againDateTimeField
.Creator
: This field specifies the review creator or the person who writes the book review. Notice that this is a foreign key toauth.get_user_model()
, which is referring to theUser
model from Django's built-in authentication module. It has a field optionon_delete=models.CASCADE
. This explains that when a user is deleted from the database, all the reviews written by that user will be deleted.Book
: Reviews have a field calledbook
, which is a foreign key to theBook
model. This is because for a book review application, reviews have to be written, and a book can have many reviews, so this is a many-to-one relationship. This is also defined with a field option,on_delete=models.CASCADE
, because once the book is deleted, there is no point in retaining the reviews in the application. So, when a book is deleted, all the reviews referring to the book will also get deleted.
Model Methods
In Django, we can write methods inside a model class. These are called model methods and they can be custom methods or special methods that override the default methods of Django models. One such method is __str__()
. This method returns the string representation of the Model
instances and can be especially useful while using the Django shell. In the following example, where the __str__()
method is added to the Publisher
model, the string representation of the Publisher
object will be the publisher's name:
class Publisher(models.Model): """A company that publishes books.""" name = models.CharField\ (max_length=50, \ help_text="The name of the Publisher.") website = models.URLField\ (help_text="The Publisher's website.") email = models.EmailField\ (help_text="The Publisher's email address.") def __str__(self): return self.name
Add the _str_()
methods to Contributor
and Book
as well, as follows:
class Book(models.Model): """A published book.""" title = models.CharField\ (max_length=70, \ help_text="The title of the book.") publication_date = models.DateField\ (verbose_name=\ "Date the book was published.") isbn = models.CharField\ (max_length=20, \ verbose_name="ISBN number of the book.") publisher = models.ForeignKey\ (Publisher, \ on_delete=models.CASCADE) contributors = models.ManyToManyField\ ('Contributor', through="BookContributor") def __str__(self): return self.title class Contributor(models.Model): """ A contributor to a Book, e.g. author, editor, \ co-author. """ first_names = models.CharField\ (max_length=50, \ help_text=\ "The contributor's first name or names.") last_names = models.CharField\ (max_length=50, \ help_text=\ "The contributor's last name or names.") email = models.EmailField\ (help_text=\ "The contact email for the contributor.") def __str__(self): return self.first_names
Migrating the Reviews App
Since we have the entire model file ready, let's now migrate the models into the database, similar to what we did before with the installed apps. Since the reviews app has a set of models created by us, before running the migration, it is important to create the migration scripts. Migration scripts help in identifying any changes to the models and will propagate these changes into the database while running the migration. Execute the following command to create the migration scripts:
python manage.py makemigrations reviews
You should get an output similar to this:
reviews/migrations/0002_auto_20191007_0112.py - Create model Book - Create model Contributor - Create model Review - Create model BookContributor - Add field contributors to book - Add field publisher to book
Migration scripts will be created in a folder named migrations
in the application folder. Next, migrate all the models into the database using the migrate
command:
python manage.py migrate reviews
You should see the following output:
Operations to perform: Apply all migrations: reviews Running migrations: Applying reviews.0002_auto_20191007_0112... OK
After executing this command, we have successfully created the database tables defined in the reviews
app. You may use DB Browser for SQLite to explore the tables you have just created after the migration. To do so, open DB Browser for SQLite, click the Open
Database
button (Figure 2.17), and navigate to your project directory:
Select the database file named db.sqlite3
to open it (Figure 2.18).
You should now be able to browse the new sets of tables created. The following figure shows the database tables defined in the reviews
app: