Django: Setting Up First App + Intro to Models and Commands
This post is a continuation of my last, which focused on setting up a Django project. The project that I’m currently working on is a Django backend for a fantasy hockey application. I’ve already built out a lot of this in Ruby on Rails, so it’s mostly a project for me to reacclimate to Python and Django. If you’d like to check out the repo, you can find it here. This week I focused on creating my first app, which I’m using to pull data from the NHL’s stats API. I’m saving teams, players, games and scoring events, and we’ll use this data later on to make our ‘matchups’ come to life. In this post we’ll focus on creating a new app, writing models and creating commands that will fetch data from the NHL’s stats API and seed our database.
Creating Your App
So we’ll start out with creating an app. If this is your first time working in Django, I’d suggest reading through the documentation. Particularly this spot explains the difference between a project (which we’ve already set up) and an app. Quick summary: a project is the whole thing including configuration and a collection of apps. An app is a part of a project with specific functionality. In this case, our whole project is going to be the backend for the fantasy hockey game. The app we’re writing today will contain the models for the data from the NHL’s stats API and the code to fetch and seed our database.
To create the app, make sure you’re in the same directory as manage.py
, and run (replace ‘api’ with the name of your app. Admittedly I didn’t name my app very well and I’ll need to change it soon):
python manage.py startapp api
Since all we’re working on today is models, fetching and seeding the database, we can skip the part in the tutorial about views for the time being. Since we’re using PostgreSQL, make sure your server is up:
sudo service postgresql start
Django comes with a bunch of models built in, so you can go ahead and run the initial migrations:
python manage.py migrate
Here I’m going to go a little bit out of order from the documentation, and we’re going to add our api
app to the INSTALLED_APPS
in settings.py
. So in django_fantasy_hockey/settings.py
, edit INSTALLED_APPS
as follows (just tack api.apps.ApiConfig
on the end):
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api.apps.ApiConfig'
]
This’ll tell our project about the app, and we’ll be able to migrate without taking this step later on.
Models
So now we’ve got our app started, and we can begin working on the models. For reference, this is what I drew up for the model diagram when working in Ruby on Rails (we’ll only be working with the right side plus GamePlayer
and Player
today):
The final schema should work out to be nearly identical, but we’ll get to go at it with Django’s approach. To compare and contrast a bit, Django’s approach here is quite cool. In Ruby on Rails, we created a migration that inherits ActiveRecord::Migration
, tell ActiveRecord pretty much exactly what we want the database to look like (including foreign keys) in that migration, then separately tell our models how to interact with each other after migrating. In Django, everything goes in the models.py
file. We declare the models, their fields and how they relate to each other all in one place, then run makemigrations
and Django creates our database schema for us. Say we’ve migrated and want to make changes, we just make changes to the models.py
file and makemigrations
— Django will diff it and create a migration that will update our schema. Cool stuff!
Ok now that I’m off that tangent, lets dive a bit into models. As discussed above, we’ll be dealing with the following models from the picture: Team
, Game
, Player
, GameTeam
, GamePlayer
, Goal
and Assist
. To start out, all models will be created in the models.py
file. When you created the app, the following import was set at the top of the file:
from django.db import models
All of your models will inherit from models.Model
:
class Team(models.Model):class Game(models.Model):
Fields
As mentioned above, data to be saved in each model’s table is referenced by using one of the Fields available through the models
import. Examples are models.CharField
, for storing text, models.IntegerField
, for storing an Integer, and models.DateTimeField
, for storing a DateTime. As an example of this in action, lets take a look at my Team
model:
class Team(models.Model):
api_id = models.IntegerField()
name = models.CharField(max_length=50)
abbreviation = models.CharField(max_length = 3)
city = models.CharField(max_length = 20)
division = models.CharField(max_length = 20)
conference = models.CharField(max_length = 20)
website = models.URLField()
def __str__(self):
return f"{self.city} {self.name}"
Team inherits from models.Model
, then describes many fields that we’ll populate later on with information from the NHL’s API. We’ll be able to use these fields to reference that data, or query the database by (we’ll get into both of these further later, but here’s some examples):
>>> penguins = Team.objects.get(name = 'Penguins')
>>> penguins
<Team: Pittsburgh Penguins>
>>> penguins.api_id
5
>>> penguins.conference
'Eastern'
Once the database has been populated, we can search for a team in a few ways, but above we’re using Team.objects.get()
. Above we’re searching for the Penguins by name. Since there is a team named ‘Penguins’, an instance of a Team
is stored in the penguins
variable so we can reference it later. Since we’ve defined a __str__
method for this class, when the team is referenced by itself, it’ll return a formatted string in the format “{team city} {team name}”:
def __str__(self):
return f"{self.city} {self.name}"
From here we can reference any of the fields we’ve defined in the model and saved in the database ( api_id => 5, conference => 'Eastern'
).
Relationships
Next lets dive a bit into defining relationships between models. Similar to storing data, relationships are defined using model Fields. There are three types that can be used — ForeignKeyField
, ManyToManyField
and OneToOneField
.
I’m going to relate these to how model relationships are described in Rails. ForeignKey
will create a create a :belongs_to
, :has_many
relationship. An example in my project would be between the Game
and Goal
models. A Goal
belongs to a Game
, and a Game
can have many Goal
s. In Django, we’ll use ForeignKeyField
in the Goal
model:
class Goal(models.Model):
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name='goals')
This will create a game_id
column in the Goals
table that references the primary key of the related game. Later on, when we have instances of goals and games, we can reference the game related to a goal by instance_of_goal.game
, and the goals related to a game by instance_of_game.goals
.
There are two required arguments for ForeignKey
, and those are the related model and on_delete
. Since python reads top down, if the model has already been defined it can be referenced by the model name directly ( Game
), or in quotations if the related model will be defined later ( 'Game'
). on_delete
defines the action to be taken if the related object is deleted. Since our goal belongs to a game, what do we do with the goal if the game is deleted? In this case, we set on_delete=models.CASCADE
. If the game is deleted, all of the goals are also deleted. This will help to maintain the integrity of our database — no foreign keys pointing to objects that no longer exist. CASCADE
is only one way to handle this — please look at the documentation for further options.
I had a bit of trouble regarding the related names here, so discussing that briefly. In Rails, related names are specifically singular or plural of the related model ( instance_of_goal.game
, instance_of_game.goals
). Django is slightly different here, that when referencing a has_many
type relationship, the reverse relationship is the model name with _set
appended to it. So by default, looking up the goals related to a game would be instance_of_game.goal_set
. Coming from Rails, having the plural of the related name was more intuitive, so I’d note that the default can be overwritten by using the related_name
option — above I’ve set the related_name='goals'
, so we can use my preferred version instance_of_game.goals
. Fun stuff.
On to ManyToManyField
. ManyToManyField
describes a many-to-many relationship, the equivalent in rails being :has_many, through: :join_model
. A good example of this in my project is Game
and Player
. A Game
has many Player
s, and a Player
plays in many Games
. To represent this in a database, we have the games
and players
tables with a join table game_players
. Django’s ManyToManyField
will do all of this for you in one line:
players = models.ManyToManyField(
'Player',
related_name='games'
)
Now a Game
can reference its players ( instance_of_game.players
), and a Player
can reference their games ( instance_of_player.games
).
By default, this will automatically create a join table; however, sometimes you’d like to store some extra information in the join table. In my case, I’d like to store the position the player played in during the game and what jersey number they were wearing. Since these values can change game-to-game, we could store a most recent version of these in the Player
model, but would want to have a separate record for each individual game. In order to do this, we need to create the join model ourselves, and use the through
option on ManyToManyField
:
class Game(models.Model):
players = models.ManyToManyField(
'Player',
through='GamePlayer',
related_name='games'
)class Player(models.Model):
api_id = models.IntegerField()
name = models.CharField(max_length = 50)
def __str__(self):
return f"{self.name}"class GamePlayer(models.Model):
game = models.ForeignKey(
Game,
on_delete=models.CASCADE,
related_name='game_players'
)
player = models.ForeignKey(
Player,
on_delete=models.CASCADE,
related_name='game_players'
)
position = models.CharField(max_length = 3)
jersey_num = models.IntegerField()
With the models defined in this way, we can look up players in a game ( instance_of_game.players.all()
) and games a player has played in ( instance_of_player.games.all()
) while holding the additional information about the player’s position and jersey number in the game. Cool stuff!
The last relationship is OneToOneField
. This operates very similarly to ForeignKeyField
, but only returns one object for the related model. This corresponds with the :belongs_to
, :has_one
type relationship in Rails. I don’t have an example of this yet in my project, but wanted to make sure to tack it on here.
After you’ve set up your models, you can run the following commands to migrate the database (here are my full models if you’d like to check them out):
python manage.py makemigrations
python manage.py migrate
Commands, Fetching and Seeding the Database
Last I want to touch on how I’m pulling the data from the NHL. In my Rails project I created a fetcher module in my services directory that I’d run in the rails console
. My plans for the long term were to create a job that would run the fetcher at a set time of day to update the games for that day. I was having trouble figuring out how to port this over to Django until I ran across this article by Alexis Chilinski outlining how to seed your database with data from an external API.
The solution here is to create ‘Command’s, that can be run via the command line. The commands we’ll write here will fetch data from the API based on the parameters you put in and save it to the database. Later on, these commands can be linked to a cron job to be run at a certain time or interval.
For now I’m only going to go over the command that fetches teams, as this post is getting a little long. Maybe next week we can go a bit deeper into the one that fetches schedule and game data. If you’re following along, I’ll be referencing this file.
To start out, we’ll need to create a management
directory in our api
directory, and a commands
directory inside management
. Inside commands
, we’ll create make_teams.py
(the file referenced above):
In make_teams.py
, we’ll need requests
to fetch data from the API, BaseCommand
to create our command and Team
from our api.models
so we can build teams here. the start of your file should look like this:
import requests
from django.core.management.base import BaseCommand
from api.models import Team
At the bottom of the file:
class Command(BaseCommand):
def handle(self, *args, **options):
fetch_teams()
This defines the command, which will call our fetch_teams
method. fetch_teams
will send a get request to the NHL’s API and attempt to build a team for every team in the response:
def fetch_teams():
url = 'https://statsapi.web.nhl.com/api/v1/teams'
response = requests.get(url, headers={'Content-Type': 'application/json'})
teams_dict = response.json() for team in teams_dict['teams']:
build_team(team)
build_team
uses the Team.objects
manager’s get_or_create()
method to either find a team with the related ID, or create a new team.
def build_team(team):
Team.objects.get_or_create(
api_id = team['id'],
defaults={
'name': team['teamName'],
'abbreviation': team['abbreviation'],
'city': team['locationName'],
'division': team['division']['name'],
'conference': team['conference']['name'],
'website': team['officialSiteUrl']
}
)
get_or_create
operates very similarly to ActiveRecord’s find_or_create_by
. All of the values not specified in defaults
will be used to find a match of the current teams created. In our case, we’re using only api_id
, as it’s unique per team on the NHL API. The keys and values provided in defaults
will only be used to create a new team, and not for lookup / comparison. In this way, the first time we run this command, it’ll pull all of the teams. Then we can run it as many times as we want and it’ll never create another (unless a new team joins the league — looking at you Seattle Kraken with your null division and conference -.-).
There’s some more to commands — you can add keyword arguments, which can be pretty cool. I think I’ll touch on that a bit more next week unless I dive down another rabbit hole in the meantime. We’ll see.
But yeah, that’s a pretty good overview of what I’ve gone over this week in Django. Having fun with it so far and looking forward to diving in a bit more.