Grokking Django
Vote

0% completed

​

Linking Models with ForeignKey

Many-to-One: ForeignKey

Following the Link in Both Directions

Naming the Reverse Side: related_name

What Happens on Delete: on_delete

Key Takeaways

How many times should the database store Ana Lopez's name? In the Article model so far, the answer is once per article, because author is a text field. If she writes forty articles and then changes her name, forty rows need the same edit. If one article spells her name "Ana Lopes", the data no longer agrees with itself.

The usual answer is to store each author once, in a table of authors, and to let every article point to its author's row by that row's id. A column that holds the primary key of a row in another table is called a foreign key. Django builds three kinds of links on this idea: many articles to one author, many articles to many tags, and one profile to one user account. This lesson adds the first kind to the blog, and the next lesson adds the other two.

Many-to-One: ForeignKey

An author writes many articles, and each article has one author. This is a many-to-one relationship, and the field for it is ForeignKey, placed on the "many" side, which is Article:

# blog/models.py from django.db import models from django.utils import timezone class Author(models.Model): name = models.CharField(max_length=100) email = models.EmailField(unique=True) def __str__(self): return self.name class Article(models.Model): title = models.CharField(max_length=200) content = models.TextField() author = models.ForeignKey(Author, on_delete=models.CASCADE) published_date = models.DateTimeField(default=timezone.now) view_count = models.PositiveIntegerField(default=0) def __str__(self): return self.title

The Author class comes first in the file because Article refers to it by name. The unique=True on email makes the database reject a second author with the same address. The on_delete argument is required, and a later section of this lesson explains it.

Clear the practice articles before you migrate. The article left over from the previous lesson stores the name Ben Carter in the author column, and a name is not the id of an author row. If you migrate with that row in the table, the migration fails:

django.db.utils.IntegrityError: The row in table 'blog_article' with primary key '2' has an invalid foreign key: blog_article.author_id contains a value 'Ben Carter' that does not have a corresponding value in blog_author.id.

On a practice database, the simple fix is to run Article.objects.all().delete() in the shell before you edit models.py. On a database with real data, you would instead write a data migration, a migration that runs Python code to create the authors and connect each article to the right one. The lesson "Data Migrations", later in this chapter, makes this same change that way and keeps every article.

With the table empty, the usual two commands create the new table and change the old one:

python manage.py makemigrations python manage.py migrate
Migrations for 'blog': blog/migrations/0004_author_alter_article_author.py + Create model Author ~ Alter field author on article

The number is 0004 because the blog app already has three migrations from the first lesson of this chapter. The SQL from python manage.py sqlmigrate blog 0004 shows what a foreign key is in the database. The article table now has a column called author_id, not author, and the column refers to the id column of blog_author:

"author_id" bigint NOT NULL REFERENCES "blog_author" ("id") DEFERRABLE INITIALLY DEFERRED CREATE INDEX "blog_article_author_id_905add38" ON "blog_article" ("author_id");

Django also adds an index on author_id, so that finding all the articles of one author stays fast as the table grows.

In Python, you assign an Author object to the author field, and reading the field gives you the object back:

from blog.models import Article, Author ana = Author.objects.create(name="Ana Lopez", email="ana@example.com") ben = Author.objects.create(name="Ben Carter", email="ben@example.com") first = Article.objects.create(title="Getting Started with Django", content="Install Django.", author=ana) Article.objects.create(title="Django Models in Practice", content="Fields map to columns.", author=ana, view_count=45) Article.objects.create(title="Understanding Querysets", content="Querysets are lazy.", author=ben, view_count=120) print(first.author, first.author.email, first.author_id)
Ana Lopez ana@example.com 1

The author_id attribute holds the raw number stored in the column. For an article loaded from the database, the first read of article.author runs a query to load the matching Author row, and later reads reuse that object. Here first was created with author=ana, so Django already holds the author and reading it runs no query.

Django also adds an attribute to the other side of the link, so an author can find their articles. Its default name is the related model's name in lowercase followed by _set, so here it is article_set. It is a manager, like Article.objects, but limited to one author's articles:

print(ana.article_set.all()) print(ana.article_set.count())
<QuerySet [<Article: Getting Started with Django>, <Article: Django Models in Practice>]> 2

You can also filter across the link. Two underscores after the field name reach into the related model, and Django adds the JOIN for you:

print(Article.objects.filter(author__name="Ana Lopez").query)
SELECT ... FROM "blog_article" INNER JOIN "blog_author" ON ("blog_article"."author_id" = "blog_author"."id") WHERE "blog_author"."name" = Ana Lopez

Naming the Reverse Side: related_name

Readers of the blog leave comments, and each comment belongs to one article. That is another ForeignKey, this time from Comment to Article. The related_name argument replaces the default comment_set with a name that reads better:

# blog/models.py class Comment(models.Model): article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments") name = models.CharField(max_length=80) body = models.TextField() created = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name} on {self.article}"

The auto_now_add=True option fills created with the current time when the comment is first saved, and it leaves the field out of forms, so nobody can edit it. That suits a creation time that should never change. (It would not suit an article's publication date, which an editor may need to set.)

After makemigrations writes 0005_comment.py and you run migrate, the article reaches its comments through comments:

first.comments.create(name="Sam", body="Very clear, thanks.") first.comments.create(name="Lee", body="What about PostgreSQL?") print(first.comments.all())
<QuerySet [<Comment: Sam on Getting Started with Django>, <Comment: Lee on Getting Started with Django>]>

Calling create() through the reverse manager fills in the article field for you. Once you set related_name, the default name no longer exists, so first.comment_set raises AttributeError: 'Article' object has no attribute 'comment_set'.

What Happens on Delete: on_delete

When a row is deleted, every row that points to it has a problem: its foreign key now refers to nothing. The on_delete argument tells Django what to do with those rows. Leaving it out is an error: TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete'.

With models.CASCADE, deleting an author also deletes their articles, and deleting an article deletes its comments. To see the chain without losing the running example, create a guest author with one article and one comment, and then delete the author:

guest = Author.objects.create(name="Guest Writer", email="guest@example.com") post = Article.objects.create(title="A Guest Post", content="Hello.", author=guest) post.comments.create(name="Sam", body="Welcome!") print(guest.delete())
(3, {'blog.Comment': 1, 'blog.Article': 1, 'blog.Author': 1})

One delete() call removed three rows in two steps: the author's article, because of Article.author, and that article's comment, because of Comment.article. Cascading is right for comments, which mean nothing without their article. It is risky for authors, where deleting one author by mistake erases everything that person wrote.

The table compares CASCADE with the common choices that keep the related rows:

OptionDeleting an author with articles
models.CASCADEdeletes the author and all their articles
models.PROTECTrefuses, and raises ProtectedError
models.SET_NULLkeeps the articles and sets their author to NULL; the field must have null=True
models.SET_DEFAULTkeeps the articles and sets author to the field's default; the field must have a default

All of these options run in Django, which finds the related rows and changes them before it deletes the author. Current Django also offers database-level versions, such as models.DB_CASCADE and models.DB_SET_NULL, which leave that work to the database. A chain of links must use one kind or the other: Comment.article cannot use DB_CASCADE while Article.author uses CASCADE, and python manage.py check reports that mix as error fields.E323.

With on_delete=models.PROTECT, deleting Ana fails while she still has articles:

django.db.models.deletion.ProtectedError: ("Cannot delete some instances of model 'Author' because they are referenced through protected foreign keys: 'Article.author'.", {<Article: Getting Started with Django>, <Article: Django Models in Practice>})

If you choose SET_NULL without null=True, python manage.py check reports the mistake before any data is touched: (fields.E320) Field specifies on_delete=SET_NULL, but cannot be null. In the same way, SET_DEFAULT without a default gives (fields.E321) Field specifies on_delete=SET_DEFAULT, but has no default value.

Key Takeaways

  • A ForeignKey goes on the "many" side. It stores the related row's id in a column named <field>_id, and the related model gets a reverse manager named <model>_set unless you set related_name.
  • Two underscores after a field name, as in author__name, filter across the link, and Django writes the JOIN.
  • on_delete is required. CASCADE deletes dependent rows, PROTECT refuses the delete, and SET_NULL needs null=True.

Reading Progress

0%


Vote for new content

On This Page

Many-to-One: ForeignKey

Following the Link in Both Directions

Naming the Reverse Side: related_name

What Happens on Delete: on_delete

Key Takeaways