When one model points to another with a ForeignKey, Django needs to know two things: what to call the reverse lookup from a parent record to its children, and what should happen to the children when the parent is deleted. A clear related_name and the right on_delete make your code easier to read and protect you from accidental data loss. This post uses an Author–Book example to make both choices concrete.
The problem
Suppose you have Author and Book models, where each book belongs to one author. A common way to write this is to declare the ForeignKey with on_delete=models.CASCADE and no related_name. It runs, but two problems creep in over time. First, to get an author’s books you must use Django’s auto-generated default name, book_set — for example author.book_set.all(). That name is hard to read, and it gets worse when one model has two ForeignKeys pointing to the same model: Django then reports a reverse accessor clash. Second, CASCADE means deleting an author deletes all of that author’s books. That is right for something like Order–OrderItem, but usually wrong for Author–Book: you want to keep the books and just drop the author link.
class Author(models.Model): name = models.CharField(max_length=100)class Book(models.Model): author = models.ForeignKey(Author, on_delete=models.CASCADE) title = models.CharField(max_length=200)
The solution
The fix is short: give related_name a clear plural noun, and choose on_delete by asking, “does the child still make sense when the parent is gone?” For Author–Book, the answer is usually SET_NULL with null=True, so the book survives without an author.
class Author(models.Model): name = models.CharField(max_length=100)class Book(models.Model): author = models.ForeignKey( Author, on_delete=models.SET_NULL, null=True, related_name="books", ) title = models.CharField(max_length=200)
From now on, author.books.all() reads exactly like plain English: “the author’s books”. With related_name you can also filter in reverse: Author.objects.filter(books__title__icontains=’django’) finds authors with a book containing “django”. Besides SET_NULL, remember three other common options. CASCADE deletes children with the parent — right for order items or comments. PROTECT blocks deleting a parent that still has children — right for a category that still has products. SET_DEFAULT falls back to the declared default value. DO_NOTHING, on the other hand, is almost never what you want: the database still enforces the foreign key and will raise an error when you delete the parent.
Applying it to your project
In a running project, audit every ForeignKey and OneToOneField: list each relationship, ask whether the child still makes sense without its parent, and settle on_delete and related_name from that answer. Changing these two attributes only creates an AlterField migration — it does not delete data by itself — but back up first and test deleting a parent on staging, because one wrong CASCADE delete can wipe out a whole cluster of children. Name related_name with plural nouns (books, comments, items) and keep the convention consistent across the project.
Related posts
- Django: fix N+1 queries with select_related in 5 minutes
- Django: fix N+1 queries with prefetch_related for ManyToMany and reverse relations
- Django: build forms twice as fast with ModelForm — no manual field mapping
FAQ
What happens if I don’t set related_name?
Django auto-generates a reverse name in the form <model>_set, for example book_set, so the code still runs. That name is hard to read, and if two ForeignKeys point to the same model without distinct related_name values, Django reports a clash and refuses to run.
What if I use SET_NULL but forget null=True?
The migration fails because the database column does not allow NULL, while SET_NULL needs to write NULL into it. Add null=True, plus blank=True if you use forms or the admin.
Does changing on_delete or related_name on a live project lose data?
No — changing these two attributes only creates a migration that alters the relationship definition; it does not delete any records by itself. The risk comes later, when you delete a parent record while the relationship is set to CASCADE.
💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold good ideas into the post.
