PYTHON / DJANGO
Relationships: ForeignKey and ManyToManyField
Model one-to-many and many-to-many links in Django with ForeignKey and ManyToManyField, then traverse them in both directions with the ORM.
What you will learn
- Declare a ForeignKey with on_delete and read it back via obj.field and obj.field_id
- Use related_name to control the reverse accessor instead of the default book_set
- Manage a ManyToManyField with add(), remove(), set() and clear()
- Filter across relations with double-underscore lookups such as authors__name
Understanding Relationships: ForeignKey and ManyToManyField
A ForeignKey is a single column on the table that declares it. Putting publisher = ForeignKey(Publisher) on Book means every book row stores one publisher id, which is exactly why a publisher can have many books but a book has one publisher. Django creates the column as publisher_id, so sp.publisher_id gives you the raw id with no query, while sp.publisher loads the Publisher row from the database the first time you touch it.
A ManyToManyField cannot be a column, because a single column can hold only one value. Django instead creates a separate join table holding pairs of ids, and both sides of the relation are exposed as managers rather than as attributes. That is the practical difference you feel while coding: sp.publisher is a model instance you can assign, but sp.authors is a manager you must call methods on, and add() writes a row into the join table rather than into the Book row.
Every relation is readable from the other end too. Django installs a reverse accessor on the target model, named <model>_set by default, or whatever you pass as related_name, and that accessor is always a manager because the reverse side of a ForeignKey is many. The same names work inside queries: Book.objects.filter(publisher__name='No Starch') makes the ORM emit a JOIN, so you can filter books by publisher data without fetching publishers yourself.
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=['__main__'],
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}},
DEFAULT_AUTO_FIELD='django.db.models.BigAutoField',
)
django.setup()
from django.core.management import call_command
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=100)
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, related_name='books')
authors = models.ManyToManyField(Author, related_name='books')
call_command('migrate', run_syncdb=True, verbosity=0)
nsp = Publisher.objects.create(name='No Starch')
ore = Publisher.objects.create(name='OReilly')
danjou = Author.objects.create(name='Danjou')
seitz = Author.objects.create(name='Seitz')
sp = Book.objects.create(title='Serious Python', publisher=nsp)
bhp = Book.objects.create(title='Black Hat Python', publisher=nsp)
Book.objects.create(title='Fluent Python', publisher=ore)
sp.authors.add(danjou)
bhp.authors.add(danjou, seitz)
print(sp.publisher.name)
print(sp.publisher_id)
print(nsp.books.count())
print([b.title for b in nsp.books.order_by('title')])
print([a.name for a in bhp.authors.order_by('name')])
print([b.title for b in danjou.books.order_by('title')])
print(Book.objects.filter(publisher__name='No Starch', authors__name='Danjou').count())A ForeignKey is one id column on the declaring row, while a ManyToManyField is a hidden join table, and that structural difference dictates whether you get an object or a manager.
Worked examples
Default reverse accessor and cascade delete
Shows the automatic <model>_set accessor when related_name is omitted, and what on_delete=CASCADE does to the child rows.
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=['__main__'],
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}},
DEFAULT_AUTO_FIELD='django.db.models.BigAutoField',
)
django.setup()
from django.core.management import call_command
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
class Chapter(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
heading = models.CharField(max_length=200)
call_command('migrate', run_syncdb=True, verbosity=0)
book = Book.objects.create(title='Serious Python')
Chapter.objects.create(book=book, heading='Starting Your Project')
Chapter.objects.create(book=book, heading='Modules and Libraries')
print(book.chapter_set.count())
print(Chapter.objects.get(heading='Modules and Libraries').book_id)
print(Chapter.objects.filter(book__title__startswith='Serious').count())
book.delete()
print(Chapter.objects.count())Example explained
Line 1book.chapter_set works because no related_name was given, so Django derives the accessor from the lowercased model name.
Line 2chapter.book_id reads the stored integer column directly and issues no extra query, unlike chapter.book.
Line 3book__title__startswith filters chapters through a JOIN on the ForeignKey, walking the relation forward from Chapter.
Line 4book.delete() removes the two chapters as well because on_delete=CASCADE tells Django to delete rows that point at the deleted one.
Many-to-many with a through model
Stores extra data on the relationship itself by supplying an explicit intermediate model.
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=['__main__'],
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:'}},
DEFAULT_AUTO_FIELD='django.db.models.BigAutoField',
)
django.setup()
from django.core.management import call_command
from django.db import models
class Musician(models.Model):
name = models.CharField(max_length=100)
class Band(models.Model):
name = models.CharField(max_length=100)
members = models.ManyToManyField(Musician, through='Membership', related_name='bands')
class Membership(models.Model):
band = models.ForeignKey(Band, on_delete=models.CASCADE)
musician = models.ForeignKey(Musician, on_delete=models.CASCADE)
instrument = models.CharField(max_length=50)
call_command('migrate', run_syncdb=True, verbosity=0)
rush = Band.objects.create(name='Rush')
geddy = Musician.objects.create(name='Geddy')
neil = Musician.objects.create(name='Neil')
Membership.objects.create(band=rush, musician=geddy, instrument='bass')
rush.members.add(neil, through_defaults={'instrument': 'drums'})
print([m.name for m in rush.members.order_by('name')])
print(Membership.objects.get(musician=geddy).instrument)
print(Membership.objects.get(musician=neil).instrument)
Membership.objects.filter(musician=geddy).delete()
print(rush.members.count())Example explained
Line 1through='Membership' tells Django to use your model as the join table instead of generating a hidden one.
Line 2Creating a Membership row is enough to make the musician appear in rush.members, because that manager reads the through table.
Line 3add() still works on a through relation only if you pass through_defaults for the extra columns.
Line 4Deleting the Membership row breaks the link, which is why rush.members.count() drops to 1 while both Musician rows still exist.
Important notes
Chaining two filter() calls on a many-to-many relation creates two JOINs, so filter(authors__name='A').filter(authors__name='B') means both authors, while putting both conditions in one filter() asks a single join row to match both names and returns nothing.
Reverse accessors are querysets, not cached lists, so calling nsp.books.all() in a loop re-queries the database each time; select_related for ForeignKey and prefetch_related for ManyToManyField collapse those queries.
Common mistakes
Writing ForeignKey(Publisher) without on_delete: the module fails to import with TypeError about a missing required argument, so no command runs at all.
Assigning a list to a many-to-many attribute, as in book.authors = [a, b]: Django raises TypeError about direct assignment to the forward side being prohibited; use book.authors.set([a, b]).
Calling book.authors.add(author) before either object has been saved: there is no primary key to store in the join table, so Django raises ValueError instead of quietly saving both.
Try it yourself
Change, predict, then run
Extend the Book model with a categories = ManyToManyField(Category, related_name='books') field, attach two categories to one book, and print the book titles found by Book.objects.filter(categories__name='security').
Open the Python workspaceCheck your understanding
You want the books written by both Danjou and Seitz together. Which expression returns exactly those books?
- Book.objects.filter(authors__name__in=['Danjou', 'Seitz'])
- Book.objects.filter(authors__name='Danjou').filter(authors__name='Seitz')
- Book.objects.filter(authors__name='Danjou', authors__name='Seitz')
- Book.objects.filter(authors=['Danjou', 'Seitz'])
Show answer
Each separate filter() call on a multi-valued relation adds its own JOIN to the join table, so the two conditions can be satisfied by two different rows, meaning both authors must be linked to the same book. The __in version filters a single join and matches any book with either author, so it also returns books written by only one of them.