Encapsulate an Entity With a Model
In Part 2, we created our first model, the Band
model, with a name
field. This field was of a specific type: CharField
, which is short for character field - a field that holds character data, or string data.
Our Band
model could additionally hold data about many other characteristics:
Genre
Biography
year_formed
Active (is the band currently active - yes or no?)
official_homepage
What data type could we use for each of these fields? Think about it before adding them in the next section.
We may also want to apply some rules to these fields. For example, what’s the maximum length of characters in a biography? Should genre be a “free text” field, or should we make the user pick from a list of choices? If the band has no official homepage, can we leave that field empty?
In this chapter, you will learn about the different types of fields that a model can have. You’ll also see how to apply rules to these fields to constrain their values within limits you are happy with.
Define Fields in a Model
Let’s go back to our Band
model, which we left looking like this:
# listings/models.py
class Band(models.Model):
name = models.fields.CharField(max_length=100)
Add the additional fields we listed above:
# listings/models.py
class Band(models.Model):
name = models.fields.CharField(max_length=100)
genre = models.fields.CharField()
biography = models.fields.CharField()
year_formed = models.fields.IntegerField()
active = models.fields.BooleanField()
official_homepage = models.fields.URLField()
Let’s consider the field type we’ve used for each field:
genre
andbiography
: likename
, these fields will contain character or string data, so we’ll useCharField
.year_formed
: a year is essentially an integer, so I’ve opted for anIntegerField
here. Django also has aDateField
, but that would include a month and day too, which would be unused in this case.active
: this is a “yes” or “no” answer, so aBooleanField
withTrue
orFalse
values is perfect here.official_homepage
: we could have used anotherCharField
here, but Django has a better choice:URLField
, which will only allow valid URLs in this field - we’ll see how that works later.
Pass Arguments to Each Field
Now that we know which field types we’re using, let’s pass arguments (also known as field options) to each field:
# listings/models.py
from django.core.validators import MaxValueValidator, MinValueValidator
...
class Band(models.Model):
name = models.fields.CharField(max_length=100)
genre = models.fields.CharField(max_length=50)
biography = models.fields.CharField(max_length=1000)
year_formed = models.fields.IntegerField(
validators=[MinValueValidator(1900), MaxValueValidator(2021)]
)
active = models.fields.BooleanField(default=True)
official_homepage = models.fields.URLField(null=True, blank=True)
For
CharField
s, themax_length
option is required - we would get an error if we tried to run the application without it. We’ve chosen appropriate values for each field.year_formed
should have appropriate minimum and maximum values. I’ve opted for a minimum of1900
(We can change this later if we really need to add a band that’s older than that!). The maximum is set to2021
- that will do for the year in which I’m writing this, but what happens next year? We might need to revisit this later.
To enforce these constraints, we pass a list of validators to thevalidators
option. In this example, we use two of Django’s built-in validator classes:MinValueValidator
andMaxValueValidator
, which we import fromdjango.core.validators
.
For
active
, we’ve set adefault
ofTrue
. So if we failed to specify the active status of a band, it would fall back toTrue
.Remember we said we might want to leave the
official_hompage
empty if a band didn’t have one? We can use the optionnull=True
to allow NULL values in the database. And when we build a form to create or editBand
objects, settingblank=True
here will allow us to submit that form with an empty textbox for this field.
Let’s make one more customization to this model. We said we might like to have the genre
field be limited to a list of specified choices. One problem that free text fields have is that different users can type in multiple versions of the same value - e.g., “Hip Hop,” “hip hop,” and “Hip-Hop.” We can eliminate these variants and keep genres consistent by making the user choose from a list. The downside is that we would need to maintain this list and add new genres from time to time - but for the purposes of our first Django web app, let’s accept that.
Add this code to our models file:
# listings/models.py
class Band(models.Model):
class Genre(models.TextChoices):
HIP_HOP = 'HH'
SYNTH_POP = 'SP'
ALTERNATIVE_ROCK = 'AR'
...
genre = models.fields.CharField(choices=Genre.choices, max_length=5)
...
We’ve created the Genre
class, defining the choices that may be used for the genre
field. Genre
is a nested class - defined within another class. Sometimes you do this in Python if the classes are very closely related, as they are here.
The
Genre
class inherits frommodels.TextChoices
- that’s a class in Django that’s designed for defining a list of choices.In
Genre
, add a constant for each genre choice we want to allow, (e.g.,HIP_HOP
).For each constant we define a key (
’HH’
,’SP’
,’AR’
) - this is the value that will be stored in the database for that genre.
Finally, we update the options for our genre
field: set choices=Genre.choices
to limit the choices to those defined in the nested class. So far, our keys are only two characters long (e.g., ’HH’
), but let’s give ourselves some headroom in case the list grows, and set max_length=5
.
Try the New Model
It’s time to try our new model! Open up the Django shell, and let’s try to save a new Band
object:
(env) ~/projects/django-web-app/merchex
→ python manage.py shell
>>> from listings.models import Band
>>> band = Band()
>>> band.save()
...
django.db.utils.OperationalError: table listings_band has no column named genre
Oh! We can’t save a new Band
.
There’s a problem with the table listings_band
.
There’s no column named genre
yet.
Can you remember what we need to do first?
Add New Model Fields to the Database Schema With a Migration
The problem is that while we have added new fields to our model, we haven't updated our database’s schema - its structure - accordingly.
So let’s do this with a migration. But before we start, here is a quick explanation in video oh what we will do!
Ready to dig in?
The makemigrations
command will scan our models.py file and figure out what has changed since we last ran the command in Part 2. It will notice that we have added new fields to our Band
model, and generate a migration that will update our schema with new columns.
Hit Ctrl-D
to exit the shell, and then:
(env) ~/projects/django-web-app/merchex
→ python manage.py makemigrations
You are trying to add a non-nullable field 'biography' to band without a default; we can't do that (the database needs something to populate existing rows).
We’ll now be prompted for some default values. Django will sometimes ask for these when it makes a migration, and some of the fields are “non-nullable” (in other words, not optional).
Think about it like this: we’ve already got three rows in this table:
id | name |
1 | De La Soul |
2 | Cut Copy |
3 | Foo Fighters |
If we’re going to add new columns, and those columns’ values cannot be NULL, then we must put something in those columns for the existing rows (marked with ‘?’):
id | name | biography | genre | year_formed | active | official_homepage |
1 | De La Soul | ? | ? | ? | True | NULL |
2 | Cut Copy | ? | ? | ? | True | NULL |
3 | Foo Fighters | ? | ? | ? | True | NULL |
As Django prompts for each of these fields, in turn, select option 1 (“Provide a one-off default now”), and then use these defaults:
biography
: we can pass an empty string here:''
.genre
: this should be one of the keys we defined earlier. Let’s use’HH’
- sure, not all of our bands are Hip Hop, but we can fix this in the next chapter.year_formed
: let’s use a default of the year2000
- again, this isn’t true for our bands, but we’ll fix this later.
Here’s the terminal output for the first field, biography
.
(env) ~/projects/django-web-app/merchex
→ python manage.py makemigrations
You are trying to add a non-nullable field 'biography' to band without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> ''
You’ll then be prompted for the other two fields.
Finally, our migration has been generated:
# Migrations for 'listings':
listings/migrations/0003_auto_20210329_2350.py
- Add field active to band
- Add field biography to band
- Add field genre to band
- Add field official_homepage to band
- Add field year_formed to band
Let’s run it:
(env) ~/projects/django-web-app/merchex
→ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, listings, sessions
Running migrations:
Applying listings.0003_auto_20210329_2350... OK
Now let’s go back into the shell to try out our model once more:
(env) ~/projects/django-web-app/merchex
→ python manage.py shell
>>> from listings.models import Band
>>> band = Band()
>>> band.save()
...
django.db.utils.IntegrityError: NOT NULL constraint failed: listings_band.year_formed
We got an IntegrityError
on the field year_formed
. This is a good thing! We didn’t specify null=True
on that field, so it will error on NULL values.
You can see that year_formed
is currently set to None
:
>>> band.year_formed is None
True
None
values will besaved as NULL to the database. So let’s set year_formed
to something:
>>> band.year_formed = 2000
And now let’s try saving it again:
>>> band.save()
>>> band
<Band: Band object (4)>
This time it worked, and the object was saved to the database.
But what about the other fields like name
, genre
, and biography
? We didn’t specify null=True
on those either, so how come they could be saved to the database?
Let’s look at the values of those fields:
>>> band.name
''
>>> band.genre
''
>>> band.biography
''
Django has set them to empty strings. These are not NULLs, so it was possible for this band to be inserted into the database.
But is that really what we wanted? What if we wanted to force users to enter a value?
You’ll see how form validation forces the user to enter a value for these fields in the next chapter.
In the meantime, let’s delete this nameless band from our database:
>>> band.delete()
(1, {'listings.Band': 1})
The shell confirms that one Band
object was deleted.
And finally, close the shell with Ctrl-D.
Watch the screencast to go back over anything you're unsure of.
Now You Try! Define More Fields in Your Model, and Make and Run a Migration
Add the following fields to your Listing
model, thinking about which data type you should use, and which type of field you should use for each one ( CharField
, IntegerField
, etc.):
description
- describe the item being sold.sold
- has it sold yet or not? What should be the default value for a new listing?year
- what year does the item originate from? This will be useful to buyers searching for vintage items. But we should be able to leave this field empty if the year is unknown.type
- there are a few “types” of listing: Records, Clothing, Posters, and Miscellaneous.
Then make a migration. Remember you’ll be prompted for default values for any non-nullable fields. Don’t worry if the default you choose isn’t strictly correct for your listings objects - we’ll fix that in the next chapter.
Finally, apply your migration. We’ll see the new fields in action in the next chapter.
Let’s Recap!
Django comes with different field types that map to different data types, like
CharField
andIntegerField
. There are also more specific fields that will constrain the input, likeURLField
.You can define constraints and rules for fields by setting options on them like
max_length
,null
, andchoices
.You can fine-tune constraints on fields even further by specifying validators on fields using the
validators
option.When you add new fields to a model, you have to make a migration to add new columns to the database before you can start using them.
If you add non-nullable fields to a model, you’ll be prompted to provide an initial default for them when you make a migration.
Now that we’ve built out our models, let’s learn about an interface we can use to manage them that’s built right into Django - the admin site.