Introduction
In the previous tutorial, “How To Create a Django App and Connect it to a Database,” we covered how to create a MySQL database, how to create and start a Django application, and how to connect it to a MySQL database.
In this tutorial, we will create the Django models that define the fields and behaviors of the Blog application data that we will be storing. These models map the data from your Django application to the database. It’s what Django uses to generate the database tables via their object relational mapping (ORM) API, referred to as “models.”
Prerequisites
This tutorial is part of the Django Development series and is a continuation of that series.
If you have not followed along with this series, we are making the following assumptions:
You have Django version 4 or higher installed.
You have connected your Django app to a database. We are using MySQL, and you can achieve this connection by following part two of the Django series, “How To Create a Django App and Connect it to a Database.”
You are working with a Unix-based operating system, preferably an Ubuntu 22.04 cloud server as this is the system we have tested on. If you would like to set up Django on a similar environment, please refer to our tutorial, “How To Install Django and Set Up a Development Environment on Ubuntu 22.04.”
As this tutorial is largely dealing with Django models, you may be able to follow along even if you have a somewhat different setup.
Step 1 — Create Django Application
To be consistent with the Django philosophy of modularity, we will create a Django app within our project that contains all of the files necessary for creating the blog website.
Whenever we begin doing work in Python and Django, we should activate our Python virtual environment and move into our app’s root directory. If you followed along with the series, you can achieve this by typing the following.
cd ~/my_blog_app
. env/bin/activate
cd blog
From there, let’s run this command:
python manage.py startapp blogsite
This will create our app along with a blogsite
directory.
At this point in the tutorial series, you’ll have the following directory structure for your project:
my_blog_app/
└── blog
├── blog
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-38.pyc
│ │ ├── settings.cpython-38.pyc
│ │ ├── urls.cpython-38.pyc
│ │ └── wsgi.cpython-38.pyc
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── blogsite
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── manage.py
The file we will focus on for this tutorial, will be the models.py
file, which is in the blogsite
directory.
Step 2 — Add the Posts Model
First we need to open and edit the models.py
file so that it contains the code for generating a Post
model. A Post
model contains the following database fields:
title
— The title of the blog post.
slug
— Where valid URLs are stored and generated for web pages.
content
— The textual content of the blog post.
created_on
— The date on which the post was created.
author
— The person who has written the post.
Now, move into the directory where the models.py
file is contained.
cd ~/my_blog_app/blog/blogsite
Use the cat
command to show the contents of the file in your terminal.
cat models.py
The file should have the following code, which imports models, along with a comment describing what is to be placed into this models.py
file.
models.py
from django.db import models
# Create your models here.
Using your favorite text editor, add the following code to the models.py
file. We’ll use nano
as our text editor, but you are welcome to use whatever you prefer.
nano models.py
Within this file, the code to import the models API is already added, we can go ahead and delete the comment that follows. Then we’ll import slugify
for generating slugs from strings, Django’s User
for authentication, and reverse
from django.urls
to give us greater flexibility with creating URLs.
models.py
from django.db import models
from django.template.defaultfilters import slugify
from django.contrib.auth.models import User
from django.urls import reverse
Then, add the class method on the model class we will be calling Post
, with the following database fields, title
, slug
, content
, created_on
and author
. Add these below your import statements.
models.py
...
class Post(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=255)
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
author = models.TextField()
Next, we will add functionality for the generation of the URL and the function for saving the post. This is crucial, because this creates a unique link to match our unique post.
models.py
...
def get_absolute_url(self):
return reverse('blog_post_detail', args=[self.slug])
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
Now, we need to tell the model how the posts should be ordered, and displayed on the web page. The logic for this will be added to a nested inner Meta
class. The Meta
class generally contains other important model logic that isn’t related to database field definition.
models.py
...
class Meta:
ordering = ['created_on']
def __unicode__(self):
return self.title
Finally, we will add the Comment
model to this file. This involves adding another class named Comment
with models.Models
in its signature and the following database fields defined:
name
— The name of the person posting the comment.
email
— The email address of the person posting the comment.
text
— The text of the comment itself.
post
— The post with which the comment was made.
created_on
— The time the comment was created.
models.py
...
class Comment(models.Model):
name = models.CharField(max_length=42)
email = models.EmailField(max_length=75)
website = models.URLField(max_length=200, null=True, blank=True)
content = models.TextField()
post = models.ForeignKey(Post, on_delete=models.CASCADE)
created_on = models.DateTimeField(auto_now_add=True)
At this point models.py
will be complete. Ensure that your models.py
file matches the following:
models.py
from django.db import models
from django.template.defaultfilters import slugify
from django.contrib.auth.models import User
from django.urls import reverse
class Post(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True, max_length=255)
content = models.TextField()
created_on = models.DateTimeField(auto_now_add=True)
author = models.TextField()
def get_absolute_url(self):
return reverse('blog_post_detail', args=[self.slug])
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super(Post, self).save(*args, **kwargs)
class Meta:
ordering = ['created_on']
def __unicode__(self):
return self.title
class Comment(models.Model):
name = models.CharField(max_length=42)
email = models.EmailField(max_length=75)
website = models.URLField(max_length=200, null=True, blank=True)
content = models.TextField()
post = models.ForeignKey(Post, on_delete=models.CASCADE)
created_on = models.DateTimeField(auto_now_add=True)
Be sure to save and close the file. If you’re using nano, you can do so by typing CTRL
and X
, then Y
, then ENTER
.
With the models.py
file set up, we can go on to update our settings.py
file.
Step 3 — Update Settings
Now that we’ve added models to our application, we must inform our project of the existence of the blogsite
app that we’ve just added. We do this by adding it to the INSTALLED_APPS
section in settings.py
.
Navigate to the directory where your settings.py
lives.
cd ~/my_blog_app/blog/blog
From here, open up your settings.py
file, with nano, for instance.
nano settings.py
With the file open, add your blogsite
app to the INSTALLED_APPS
section of the file, as indicated below.
settings.py
# Application definition
INSTALLED_APPS = [
'blogsite',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
With the blogsite
app added, you can save and exit the file.
At this point, we are ready to move on to apply these changes.
Step 4 — Make Migrations
With our models Post
and Comment
added, the next step is to apply these changes so that our MySQL
database schema recognizes them and creates the necessary tables.
First, we must package up our model changes into individual migration files using the command makemigrations
. These files are similar to that of commits
in a version control system like Git.
Now, if you navigate to ~/my_blog_app/blog/blogsite/migrations
and run ls
, you’ll notice that there is only an __init__.py
file. This will change once we add the migrations.
Change to the blog directory using cd
, like so:
cd ~/my_blog_app/blog
Then run the makemigrations
command on manage.py
.
python manage.py makemigrations
You should then receive the following output in your terminal window:
Migrations for 'blogsite':
blogsite/migrations/0001_initial.py
- Create model Post
- Create model Comment
Remember, when we navigated to /~/my_blog_app/blog/blogsite/migrations
and it only had the __init__.py
file? If we now cd
back to that directory we’ll notice that two items have been added: __pycache__
and 0001_initial.py
. The 0001_initial.py
file was automatically generated when you ran makemigrations
. A similar file will be generated every time you run makemigrations
.
Run less 0001_initial.py
from the directory it’s in if you’d like to read over what the file contains.
Now navigate to ~/my_blog_app/blog
:
cd ~/my_blog_app/blog
Since we have made a migration file, we must apply the changes these files describe to the database using the command migrate
. But first let’s check which migrations currently exist, using the showmigrations
command.
python manage.py showmigrations
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
blogsite
[ ] 0001_initial
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
sessions
[X] 0001_initial
You’ll notice that all migrations are checked except for the one for 0001_initial
which we just created with the models Post
and Comment
.
Now let’s check which SQL
statements will be executed once we make the migrations, using the following command. It takes in the migration and the migration’s title as an argument:
python manage.py sqlmigrate blogsite 0001_initial
Revealed below is the actual SQL query being made behind the scenes.
--
-- Create model Post
--
CREATE TABLE `blogsite_post` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `title` varchar(255) NOT NULL, `slug` varchar(255) NOT NULL UNIQUE, `content` longtext NOT NULL, `created_on` datetime(6) NOT NULL, `author` longtext NOT NULL);
--
-- Create model Comment
--
CREATE TABLE `blogsite_comment` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(42) NOT NULL, `email` varchar(75) NOT NULL, `website` varchar(200) NULL, `content` longtext NOT NULL, `created_on` datetime(6) NOT NULL, `post_id` integer NOT NULL);
ALTER TABLE `blogsite_comment` ADD CONSTRAINT `blogsite_comment_post_id_de248bfe_fk_blogsite_post_id` FOREIGN KEY (`post_id`) REFERENCES `blogsite_post` (`id`);
Let’s now perform the migrations so that they get applied to our MySQL database.
python manage.py migrate
We will receive the following output:
Operations to perform:
Apply all migrations: admin, auth, blogsite, contenttypes, sessions
Running migrations:
Applying blogsite.0001_initial... OK
You have now successfully applied your migrations.
It is important to keep in mind that there are three caveats to Django migrations with MySQL as your backend, as stated in the Django documentation.
Lack of support for transactions around schema alteration operations. In other words, if a migration fails to apply successfully, you will have to manually unpick the changes you’ve made in order to attempt another migration. It is not possible to rollback, to an earlier point, before any changes were made in the failed migration.
For most schema alteration operations, MySQL will fully rewrite tables. In the worst case, the time complexity will be proportional to the number of rows in the table to add or remove columns. According to the Django documentation, this could be as slow as one minute per million rows.
In MySQL, there are small limits on name lengths for columns, tables and indices. There is also a limit on the combined size of all columns and index covers. While some other backends can support higher limits created in Django, the same indices will fail to be created with a MySQL backend in place.
For each database you consider for use with Django, be sure to weigh the advantages and disadvantages of each.
Step 5 — Verify Database Schema
With migrations complete, we should verify the successful generation of the MySQL tables that we’ve created via our Django models.
To do this, run the following command in the terminal to log into MySQL. We’ll use the djangouser
we created in the previous tutorial.
mysql blog_data -u djangouser
Now, select our database blog_data
. If you don’t know the database you are using, you can show all databases withSHOW DATABASES;
in SQL.
USE blog_data;
Then type the following command to view the tables.
SHOW TABLES;
This SQL query should reveal the following:
+----------------------------+
| Tables_in_blog_data |
+----------------------------+
| auth_group |
| auth_group_permissions |
| auth_permission |
| auth_user |
| auth_user_groups |
| auth_user_user_permissions |
| blogsite_comment |
| blogsite_post |
| django_admin_log |
| django_content_type |
| django_migrations |
| django_session |
+----------------------------+
12 rows in set (0.01 sec)
Among the tables are blogsite_comment
and blogsite_post
. These are the models that we’ve just made ourselves. Let’s validate that they contain the fields we’ve defined.
DESCRIBE blogsite_comment;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(42) | NO | | NULL | |
| email | varchar(75) | NO | | NULL | |
| website | varchar(200) | YES | | NULL | |
| content | longtext | NO | | NULL | |
| created_on | datetime(6) | NO | | NULL | |
| post_id | int | NO | MUL | NULL | |
+------------+--------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
DESCRIBE blogsite_post;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| title | varchar(255) | NO | | NULL | |
| slug | varchar(255) | NO | UNI | NULL | |
| content | longtext | NO | | NULL | |
| created_on | datetime(6) | NO | | NULL | |
| author | longtext | NO | | NULL | |
+------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
We have verified that the database tables were successfully generated from our Django model migrations.
You can close out of MySQL with CTRL
+ D
and when you are ready to leave your Python environment, you can run the deactivate
command:
deactivate
Deactivating your programming environment will put you back to the terminal command prompt.
Conclusion
In this tutorial, we’ve successfully added models for basic functionality in a blog web application. You’ve learned how to code models
, how migrations
work and the process of translating Django models
to actual MySQL
database tables.