Declaring relationships with the models
Make sure you quit the Django's development server. Remember that you just need to press Ctrl + C in the terminal or command-prompt window in which it is running. Now, we will create the models that we are going to use to represent and persist the game categories, games, players and scores, and their relationships. Open the games/models.py
file and replace its contents with the following code. The lines that declare fields related to other models are highlighted in the code listing. The code file for the sample is included in the restful_python_chapter_02_03
folder.
from django.db import models class GameCategory(models.Model): name = models.CharField(max_length=200) class Meta: ordering = ('name',) def __str__(self): return self.name class Game(models.Model): created = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=200) game_category = models.ForeignKey( GameCategory, related_name='games', on_delete=models.CASCADE) release_date = models.DateTimeField() played = models.BooleanField(default=False) class Meta: ordering = ('name',) def __str__(self): return self.name class Player(models.Model): MALE = 'M' FEMALE = 'F' GENDER_CHOICES = ( (MALE, 'Male'), (FEMALE, 'Female'), ) created = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=50, blank=False, default='') gender = models.CharField( max_length=2, choices=GENDER_CHOICES, default=MALE, ) class Meta: ordering = ('name',) def __str__(self): return self.name class PlayerScore(models.Model): player = models.ForeignKey( Player, related_name='scores', on_delete=models.CASCADE) game = models.ForeignKey( Game, on_delete=models.CASCADE) score = models.IntegerField() score_date = models.DateTimeField() class Meta: # Order by score descending ordering = ('-score',)
The preceding code declares the following four models, specifically four classes as subclasses of the django.db.models.Model
class:
GameCategory
Game
Player
PlayerScore
Django automatically adds an auto-increment integer primary key column named id
when it creates the database table related to each model. We specified the field types, maximum lengths, and defaults for many attributes. Each class declares a Meta
inner class that declares an ordering attribute. The Meta
inner class declared within the PlayerScore
class specifies '-score'
as the value of the ordering
tuple, with a dash as a prefix of the field name and ordered by score
in descending order, instead of the default ascending order.
The GameCategory
, Game
, and Player
classes declare the __str__
method that returns the contents of the name
attribute that provides the name or title for each of these models. So, Django will call this method whenever it has to provide a human-readable representation for the model.
The Game
model declares the game_category
field with the following line:
game_category = models.ForeignKey( GameCategory, related_name='games', on_delete=models.CASCADE)
The preceding line uses the django.db.models.ForeignKey class to provide a many-to-one relationship to the GameCategory model. The 'games' value specified for the related_name argument creates a backwards relation from the GameCategory model to the Game model. This value indicates the name to be used for the relation from the related GameCategory object back to a Game object. Now, we will be able to access all the games that belong to a specific game category. Whenever we delete a game category, we want all the games that belong to this category to be deleted too, and therefore, we specified the models.CASCADE value for the on_delete argument.
The PlayerScore
model declares the player
field with the following line:
player = models.ForeignKey( Player, related_name='scores', on_delete=models.CASCADE)
The preceding line uses the django.db.models.ForeignKey class to provide a many-to-one relationship to the Player model. The 'scores' value specified for the related_name argument creates a backwards relation from the Player model to the PlayerScore model. This value indicates the name to be used for the relation from the related Player object back to a PlayerScore object. Now, we will be able to access all the scores archive by a specific player. Whenever we delete a player, we want all the scores achieved by this player to be deleted too, and therefore, we specified the models.CASCADE value for the on_delete argument.
The PlayerScore
model declares the game
field with the following line:
game = models.ForeignKey( Game, on_delete=models.CASCADE)
The preceding line uses the django.db.models.ForeignKey class to provide a many-to-one relationship to the Game model. In this case, we don't create a backwards relation because we don't need it. Thus, we don't specify a value for the related_name argument. Whenever we delete a game, we want all the registered scores for this game to be deleted too, and therefore, we specified the models.CASCADE value for the on_delete argument.
In case you created a new virtual environment to work with this example or you downloaded the sample code for the book, you don't need to delete any existing database. However, in case you are making changes to the code for our previous API example, you have to delete the gamesapi/db.sqlite3 file and the games/migrations folder.
Then, it is necessary to create the initial migration for the new models we recently coded. We just need to run the following Python scripts and we will also synchronize the database for the first time. As we learned from our previous example API, by default, Django uses an SQLite database. In this example, we will be working with a PostgreSQL database. However, in case you want to use SQLite, you can skip the steps related to PostgreSQL, its configuration in Django, and jump to the migrations generation command.
You will have to download and install a PostgreSQL database in case you aren't already running it in your computer or in a development server. You can download and install this database management system from its web page-http://www.postgresql.org. In case you are working with macOS, Postgres.app
provides an easy way to install and use PostgreSQL on this operating system-http://postgresapp.com.
Tip
You have to make sure that the PostgreSQL bin folder is included in the PATH
environmental variable. You should be able to execute the psql
command-line utility from your current terminal or command prompt. In case the folder isn't included in the PATH, you will receive an error indicating that the pg_config
file cannot be found when trying to install the psycopg2
package. In addition, you will have to use the full path to each of the PostgreSQL command-line tools we will use in the subsequent steps.
We will use the PostgreSQL command-line tools to create a new database named games
. In case you already have a PostgreSQL database with this name, make sure that you use another name in all the commands and configurations. You can perform the same task with any PostgreSQL GUI tool. In case you are developing on Linux, it is necessary to run the commands as the postgres
user. Run the following command in macOS or Windows to create a new database named games
. Note that the command won't produce any output:
createdb games
In Linux, run the following command to use the postgres
user:
sudo -u postgres createdb games
Now, we will use the psql
command-line tool to run some SQL statements to create a specific user that we will use in Django and assign the necessary roles for it. In macOS or Windows, run the following command to launch psql
:
psql
In macOS, you might need to run the following command to launch psql with the postgres
in case the previous command doesn't work, as it will depend on the way in which you installed PostgreSQL:
sudo -u postgres psql
In Linux, run the following command to use the postgres
user.
sudo -u psql
Then, run the following SQL statements and finally enter \q
to exit the psql command-line tool. Replace user_name
with your desired user name to use in the new database and password with your chosen password. We will use the username and password in the Django configuration. You don't need to run the steps if you are already working with a specific user in PostgreSQL and you have already granted privileges to the database for the user:
CREATE ROLE user_name WITH LOGIN PASSWORD 'password'; GRANT ALL PRIVILEGES ON DATABASE games TO user_name; ALTER USER user_name CREATEDB; \q
The default SQLite database engine and the database file name are specified in the gamesapi/settings.py
Python file. In case you decide to work with PostgreSQL instead of SQLite for this example, replace the declaration of the DATABASES
dictionary with the following lines. The nested dictionary maps the database named default
with the django.db.backends.postgresql
database engine, the desired database name, and its settings. In this case, we will create a database named games
. Make sure you specify the desired database name in the value for the 'NAME'
key and that you configure the user, password, host, and port based on your PostgreSQL configuration. In case you followed the previous steps, use the settings specified in these steps:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', # Replace games with your desired database name 'NAME': 'games', # Replace username with your desired user name 'USER': 'user_name', # Replace password with your desired password 'PASSWORD': 'password', # Replace 127.0.0.1 with the PostgreSQL host 'HOST': '127.0.0.1', # Replace 5432 with the PostgreSQL configured port # in case you aren't using the default port 'PORT': '5432', } }
In case you decided to use PostgreSQL, after making the preceding changes, it is necessary to install the Psycopg 2 package (psycopg2). This package is a Python-PostgreSQL Database Adapter and Django uses it to interact with a PostgreSQL database.
In macOS installations, we have to make sure that the PostgreSQL bin folder is included in the PATH
environmental variable. For example, in case the path to the bin folder is /Applications/Postgres.app/Contents/Versions/latest/bin
, we must execute the following command to add this folder to the PATH
environmental variable:
export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin
Once we have made sure that the PostgreSQL bin
folder is included in the PATH environmental variable, we just need to run the following command to install this package:
pip install psycopg2
The last lines of the output will indicate that the psycopg2
package has been successfully installed:
Collecting psycopg2 Installing collected packages: psycopg2 Running setup.py install for psycopg2 Successfully installed psycopg2-2.6.2
Now, run the following Python script to generate the migrations that will allow us to synchronize the database for the first time:
python manage.py makemigrations games
The following lines show the output generated after running the previous command:
Migrations for 'games': 0001_initial.py: - Create model Game - Create model GameCategory - Create model Player - Create model PlayerScore - Add field game_category to game
The output indicates that the gamesapi/games/migrations/0001_initial.py
file includes the code to create the Game
, GameCategory
, Player
, and PlayerScore
models. The following lines show the code for this file that was automatically generated by Django. The code file for the sample is included in the restful_python_chapter_02_03
folder:
# -*- coding: utf-8 -*- # Generated by Django 1.9.7 on 2016-06-17 20:39 from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Game', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True)), ('name', models.CharField(max_length=200)), ('release_date', models.DateTimeField()), ('played', models.BooleanField(default=False)), ], options={ 'ordering': ('name',), }, ), migrations.CreateModel( name='GameCategory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=200)), ], options={ 'ordering': ('name',), }, ), migrations.CreateModel( name='Player', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True)), ('name', models.CharField(default='', max_length=50)), ('gender', models.CharField(choices=[('M', 'Male'), ('F', 'Female')], default='M', max_length=2)), ], options={ 'ordering': ('name',), }, ), migrations.CreateModel( name='PlayerScore', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('score', models.IntegerField()), ('score_date', models.DateTimeField()), ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.Game')), ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scores', to='games.Player')), ], options={ 'ordering': ('-score',), }, ), migrations.AddField( model_name='game', name='game_category', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='games', to='games.GameCategory'), ), ]
The preceding code defines a subclass of the django.db.migrations.Migration
class named Migration
that defines an operations
list with many migrations.CreateModel
. Each migrations.CreateModel
will create the table for each of the related models. Note that Django has automatically added an id
field for each of the models. The operations
are executed in the same order in which they appear in the list. The code creates Game
, GameCategory
, Player
, PlayerScore
, and finally adds the game_category
field to Game
with the foreign key to GameCategory
because it created the Game
model before the GameCategory
model. The code creates the foreign keys for PlayerScore
when it creates the model:
Now, run the following Python script to apply all the generated migrations.
python manage.py migrate
The following lines show the output generated after running the previous command:
Operations to perform: Apply all migrations: sessions, contenttypes, games, admin, auth Running migrations: Rendering model states... DONE Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying games.0001_initial... OK Applying sessions.0001_initial... OK
After we run the previous command, we can use the PostgreSQL command line or any other application that allows us to easily check the contents of the PostreSQL database to check the tables that Django generated. In case you are working with SQLite, we have already learned how to check the tables in Chapter 1, Developing RESTful APIs with Django.
Run the following command to list the generated tables:
psql --username=user_name --dbname=games --command="\dt"
The following lines show the output with all the generated table names:
List of relations Schema | Name | Type | Owner --------+----------------------------+-------+----------- public | auth_group | table | user_name public | auth_group_permissions | table | user_name public | auth_permission | table | user_name public | auth_user | table | user_name public | auth_user_groups | table | user_name public | auth_user_user_permissions | table | user_name public | django_admin_log | table | user_name public | django_content_type | table | user_name public | django_migrations | table | user_name public | django_session | table | user_name public | games_game | table | user_name public | games_gamecategory | table | user_name public | games_player | table | user_name public | games_playerscore | table | user_name (14 rows)
As seen in our previous example, Django uses the games_
prefix for the following four table names related to the games
application. Django's integrated ORM generated these tables and the foreign keys, based on the information included in our models:
games_game
: Persists theGame
modelgames_gamecategory
: Persists theGameCategory
modelgames_player
: Persists thePlayer
modelgames_playerscore
: Persists thePlayerScore
model
The following command will allow you to check the contents of the four tables after we compose and send HTTP requests to the RESTful API and make CRUD operations to the four tables. The commands assume that you are running PostgreSQL on the same computer in which you are running the command.
psql --username=user_name --dbname=games --command="SELECT * FROM games_gamecategory;" psql --username=user_name --dbname=games --command="SELECT * FROM games_game;" psql --username=user_name --dbname=games --command="SELECT * FROM games_player;" psql --username=user_name --dbname=games --command="SELECT * FROM games_playerscore;"
Tip
Instead of working with the PostgreSQL command-line utility, you can use a GUI tool to check the contents of the PostgreSQL database. You can also use the database tools included in your favorite IDE to check the contents for the SQLite database.
Django generates additional tables that it requires to support the web framework and the authentication features that we will use later.