Model relationships: ForeignKey and related lookups
Post.author has been a ForeignKey since part 5 without much explanation of what that buys beyond storing a reference. This part adds a second model, Comment, explicitly to make relationships — and querying across them in both directions — concrete.
The Comment model
# blog/models.py
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
name = models.CharField(max_length=80)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["created_at"]
def __str__(self):
return f"Comment by {self.name} on {self.post}"python manage.py makemigrations blog
python manage.py migraterelated_name="comments" is what makes the reverse lookup below read as post.comments instead of Django's default guess, post.comment_set — worth setting explicitly on every ForeignKey, since the default is a genuinely awkward name to write in a template.
Querying forward: from comment to post
comment = Comment.objects.first()
comment.post # <Post: My First Post>
comment.post.title # "My First Post"Going from the "many" side to the "one" side — many comments, one post each — is just attribute access. comment.post runs a query the first time it's accessed and caches the result, so comment.post.title right after doesn't trigger a second one.
Querying backward: from post to its comments
post = Post.objects.get(slug="my-first-post")
post.comments.all()
post.comments.count()This is exactly what related_name set up: post.comments is a manager, the same kind Post.objects is, scoped to only this post's comments. Without related_name, this would be post.comment_set.all() instead — functionally identical, just harder to read.
Showing comments on the detail page
{# blog/templates/blog/post_detail.html — inside {% block content %} #}
<h3>Comments ({{ post.comments.count }})</h3>
{% for comment in post.comments.all %}
<p><strong>{{ comment.name }}</strong>: {{ comment.body }}</p>
{% empty %}
<p>No comments yet.</p>
{% endfor %}post.comments.all in a template — no parentheses. Django's template engine calls a method automatically when it encounters one with no arguments; post.comments.all() in Python becomes post.comments.all in the template.
Registering Comment in the admin
# blog/admin.py
from .models import Post, Comment
admin.site.register(Comment)on_delete=models.CASCADE on Comment.post means deleting a post deletes every comment on it too — almost always the right call here. The alternative, models.SET_NULL (which requires the field to allow null=True), keeps the comment but clears the link — better suited to something like a post's optional "featured image" reference, where losing the image shouldn't delete the post.
FAQ
When would I use ManyToManyField instead of ForeignKey?
ForeignKey is for a "many-to-one" relationship — many comments, one post each, as here. ManyToManyField fits a relationship where each side can have several of the other — tags on a post, where one post has multiple tags and one tag applies to multiple posts. Django creates a hidden join table automatically for a ManyToManyField, which a ForeignKey never needs.
Does looping over post.comments.all() in a template run one query per comment?
No, in this specific case — post.comments.all() runs one query returning every comment for that post, then the template loop iterates over the already-fetched results in memory. The N+1 query problem shows up in a different pattern: looping over many posts and accessing post.comments.all inside that loop, which does run one query per post unless prefetch_related() is used.
What does related_name actually change if I don't set it?
Nothing breaks without it — Django generates comment_set automatically. related_name="comments" only changes what that reverse accessor is called, purely for readability in templates and code; it has no effect on what data is returned.
Next: a real form for submitting a comment, instead of only the admin being able to add one.