Creating Django models and migrations
A Django model is essentially a Python class that holds the blueprint for creating a table in a database. The models.py
file can have many such models, and each model is transformed into a database table. The attributes of the class form the fields and relationships of the database table as per the model definitions.
For our reviews
application, we need to create the following models and their database tables consequently:
Book
: This should store information about booksContributor
: This should store information about the person(s) who contributed to writing the book, such as the author, co-author, or editorPublisher
: As the name implies, this refers to the book publisherReview
: This should store all the book reviews written by the users of the application
Every book in our application will need to have a publisher, so let’s create Publisher
as our first model. Enter the following code in reviews/models.py
:
from django.db import models 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.")
Note
You can take a look at the complete models.py
file for the bookr
app by clicking the following link: https://github.com/PacktPublishing/Web-Development-with-Django-Second-Edition/blob/main/Chapter02/final/bookr/reviews/models.py.
The first line of code imports the Django models
module. While this line will be autogenerated at the time of the creation of the Django app, do make sure you add it if it is not present. Following the import, the rest of the code defines a class named Publisher
, a subclass of Django’s models.Model
. Furthermore, this class will have attributes or fields such as name
, website
, and email
. The following are the field types used while creating this model.
As we can see, each of these fields is defined to have the following types:
CharField
: This field type stores shorter string fields, such asPackt Publishing
. For very large strings, we useTextField
.EmailField
: This is similar toCharField
but validates whether the string represents a valid email address, for example,[email protected]
.URLField
: Again, this is similar toCharField
, but validates whether the string represents a valid URL, for example, https://www.packtpub.com.
Next, we will look at the field options used when creating each of these fields.
Field options
Django provides a way to define field options for a model’s field. These field options are used to set a value or a constraint, and so on. For example, we can set a default value for a field using default=<value>
to ensure that every time a record is created in the database for the field, it is set to a default value specified by us. The following are the two field options that we used when defining the Publisher
model:
help_text
: This field option helps us add descriptive text for a field that gets automatically included for Django formsmax_length
: This option is provided toCharField
where it defines the maximum length of the field in terms of the number of characters
Django has many more field types and field options that can be explored from the extensive official Django documentation. As we go about developing our sample book review application, we will learn about the types and fields used for the project. Now, let’s migrate the Django models into the database by following these steps:
- Execute the following command in the shell or terminal to do that (run it from the folder where your
manage.py
file is stored):python manage.py makemigrations reviews
The output of the command looks like this:
Migrations for 'reviews': reviews/migrations/0001_initial.py - Create model Publisher
The makemigrations <appname>
command creates the migration scripts for the given app, in this case, for the reviews
app. Notice that after running makemigrations
, there is a new file created under the migrations
folder:
Figure 2.12: New file under the migrations folder
This is the migration script created by Django. When we run makemigrations
without the app name, the migration scripts will be created for all the apps in the project.
- Next, let’s list the project migration status. Remember that earlier, we applied migrations to Django’s installed apps, and now we have created a new app,
reviews
. Run the following command in the shell or terminal, and it will show the status of model migrations throughout the project (run it from the folder where yourmanage.py
file is stored):python manage.py showmigrations
The output for the preceding command is as follows:
admin [X] 0001_initial [X] 0002_logentry_remove_auto_add [X] 0003_logentry_add_action_flag_choices auth [X] 0001_initial [X] 0002_alter_permission_name_max_length [X] 0003_alter_user_email_max_length [X] 0004_alter_user_username_opts [X] 0005_alter_user_last_login_null [X] 0006_require_contenttypes_0002 [X] 0007_alter_validators_add_error_messages [X] 0008_alter_user_username_max_length [X] 0009_alter_user_last_name_max_length [X] 0010_alter_group_name_max_length [X] 0011_update_proxy_permissions contenttypes [X] 0001_initial [X] 0002_remove_content_type_name reviews [ ] 0001_initial sessions [X] 0001_initial
Here, the [X]
mark indicates that the migrations have been applied. Notice the difference that all the other apps’ migrations have been applied except that of reviews
. The showmigrations
command can be executed to understand the migration status, but this is not a mandatory step while performing model migrations.
- Next, let’s understand how Django transforms a model into an actual database table. To do that, run the
sqlmigrate
command as follows:python manage.py sqlmigrate reviews 0001_initial
We should see the following output:
BEGIN; -- -- Create model Publisher -- CREATE TABLE "reviews_publisher" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(50) NOT NULL, "website" varchar(200) NOT NULL, "email" varchar(254) NOT NULL); COMMIT;
The preceding snippet shows the SQL command equivalent used when Django migrates the database. In this case, we create the reviews_publisher
table with the name
, website
, and email
fields with defined field types. Furthermore, all these fields are defined to be NOT NULL
, implying that the entries for these fields cannot be null and should have a value. The sqlmigrate
command is not a mandatory step while doing the model migrations.
In the next section, we will learn about primary keys and their importance when storing data in a database.
Primary keys
Let’s assume that a database table called users
, as its name suggests, stores information about users. Let’s say it has more than 1,000 records and there are at least 3 users with the same name, Joe Burns. How do we uniquely identify these users from the application? The solution is to have a way to uniquely identify each record in the database. This is done using primary keys. A primary key is unique for a database table, and as a rule, a table cannot have two rows with the same primary key. In Django, when the primary key is not explicitly mentioned in the database models, Django automatically creates id
as the primary key (an integer type), which auto-increments as new records are created.
In the previous section, notice the output of the python manage.py sqlmigrate
command. When creating the Publisher
table, the SQL CREATE TABLE
command added one more field called id
to the table. An id
value is defined as PRIMARY KEY AUTOINCREMENT
. In relational databases, a primary key is used to uniquely identify an entry in the database. For example, the book
table has id
as the primary key, which has numbers starting from 1
. This value increments by one as new records are created. The integer value of id
is always unique across the book
table. Since the migration script has already been created by executing makemigrations
, let’s now migrate the newly created model in the reviews
app by executing the following command:
python manage.py migrate reviews
You should get the following output:
Operations to perform: Apply all migrations: reviews Running migrations: Applying reviews.0001_initial... OK
This operation creates the database table for the reviews
app. The following is a snippet from DB Browser indicating the new reviews_publisher
table has been created in the database:
Figure 2.13: The reviews_publisher table created after executing the migration command
So far, we have explored how to create a model and migrate it into the database. Let’s now work on creating the rest of the models for our book review application. As we’ve already seen, the application will have the following database tables:
Book
: This is the database table that holds the information about the book itself. We have already created aBook
model and have migrated this to the database.Publisher
: This table holds information about the book publisher.Contributor
: This table holds information about the contributor, that is, the author, co-author, or editor.Review
: This table holds information about the review comments posted by the reviewers.
Let’s add the Book
and Contributor
models, as shown in the following code snippet, into reviews/models.py
:
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.") 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.")
The code is self-explanatory. The Book
model has the title
, publication_date
, and isbn
fields. The Contributor
model has the first_names
and last_names
fields and the email
ID of the contributor. There are also some newly added models, apart from the ones we have seen in the Publisher
model. They have DateField
as a new field type, which, as the name suggests, is used to store a date. A new field option called verbose_name
is also used. It provides a descriptive name for the field. Next, we will see how relationships work in a relational database.
Relationships
One of the powers of relational databases is the ability to establish relationships between data stored across database tables. Relationships help maintain data integrity by establishing the correct references across tables, which in turn helps maintain the database. Relationship rules, on the other hand, ensure data consistency and prevent duplicates.
In a relational database, there can be the following types of relations:
- Many-to-one
- Many-to-many
- One-to-one
Let’s explore each relationship in detail in the following sections.
Many-to-one
In this relationship, many records (rows/entries) from one table can refer to one record (row/entry) in another table. For example, there can be many books produced by one publisher. This is an example of a many-to-one relationship. To establish this relationship, we need to use the database’s foreign keys. A foreign key in a relational database establishes the relationship between a field from one table and a primary key from a different table.
For example, say you have data about employees belonging to different departments stored in a table called employee_info
with their employee ID as the primary key alongside a column that stores their department name; this table also contains a column that stores that department’s department ID. Now, there’s another table called departments_info
, which has the department ID as the primary key. In this case, then, the department ID is a foreign key in the employee_info
table.
In our bookr
app, the Book
model can have a foreign key referring to the primary key of the Publisher
table. Since we have already created the models for Book
, Contributor
, and Publisher
, let’s now establish a many-to-one relationship across the Book
and Publisher
models. For the Book
model, add the last line:
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)
Now the newly added publisher
field establishes a many-to-one relationship between Book
and Publisher
using a foreign key. This relationship ensures the nature of a many-to-one relationship, which is that many books can have one publisher:
models.ForeignKey
: This is the field option to establish a many-to-one relationship.Publisher
: When we establish relationships with different tables in Django, we refer to the model that creates the table; in this case, thePublisher
table is created by thePublisher
model (or thePublisher
Python class).on_delete
: This is a field option that determines the action to be taken upon the deletion of the referenced object. In this case, theon_delete
option is set toCASCADE(models.CASCADE)
, which deletes the referenced objects.
For example, assume a publisher has published a set of books. For some reason, if the publisher has to be deleted from the application, the next action is CASCADE
, which means deleting all the referenced books from the application. There are many more on_delete
actions, such as the following:
PROTECT
: This prevents the record deletion unless all the referenced objects are deletedSET_NULL
: This sets a null value if the database field has been previously configured to store null valuesSET_DEFAULT
: Sets to a default value on the deletion of the referenced object
We will only use the CASCADE
option for our book review application.
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) may have written multiple books. So, this forms a many-to-many relationship between the Book
and Contributor
tables:
Figure 2.14: Many-to-many relationship between books and co-authors
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 here: https://github.com/PacktPublishing/Web-Development-with-Django-Second-Edition/blob/main/Chapter02/final/bookr/reviews/models.py.
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
: Again, this is 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 theroles
field to defineAuthor
,Co-Author
, andEditor
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 with a driver’s license could form a one-to-one relationship:
Figure 2.15: Example of a one-to-one relationship
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 here: https://github.com/PacktPublishing/Web-Development-with-Django-Second-Edition/blob/main/Chapter02/final/bookr/reviews/models.py.
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 will 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. Again, the field type isDateTimeField
.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 refers to theUser
model from Django’s built-in authentication module. It has anon_delete=models.CASCADE
field option. 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
, a foreign key to theBook
model. This is because reviews have to be written for a book review application, and a book can have many reviews, so this is a many-to-one relationship. This is also defined with anon_delete=models.CASCADE
field option 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.
In the next section, we will learn about and implement model methods.
Model methods
In Django, we can write methods inside a model class. These are called model methods and they can be custom 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; hence, self.name
is returned, with self
referring to the Publisher
object:
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
Similarly, the string representation of book is title, so the returned value is self.title
, with self
referring to the Book
object. The string representation of Contributor
is the first name of the contributor; hence self.first_names
is returned. Here, self
refers to the Contributor
object. Next, we will look at migrating the reviews
app.
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, it is important to create the migration scripts before running the migration. Migration scripts help in identifying any changes to the models and will propagate these changes into the database while running the migration. Follow these steps to create migration scripts and then migrate the models into the database:
- 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 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 on the Open Database button (Figure 2.16), and navigate to your project directory:
Figure 2.16: Click the Open Database button
- Select the
db.sqlite3
database file to open it (Figure 2.17).
Figure 2.17: Locating db.sqlite3 in the bookr directory
You should now be able to browse the new sets of tables created. The following screenshot shows the database tables defined in the reviews
app:
Figure 2.18: Database tables as defined in the reviews app
In this section, we learned more about Django models and migrations and how Python’s simple classes can transform themselves into database tables. We also learned about how various class attributes translate into appropriate database columns following the defined field types. Later, we learned about primary keys and different types of relationships that can exist in a database. We also created models for the book review application and migrated those models, translating them into database tables. In the next section, we will learn about how to perform database CRUD operations using Django’s ORM.