ForeignKey, ManyToMany and OneToOne
Real data is connected. A patient has appointments; an appointment is with a doctor; a diagnostic report
belongs to a patient. Modelling those connections correctly is most of what makes a data model good, and
Django gives you three relationship fields — ForeignKey, ManyToManyField, OneToOneField — that map
directly onto the three ways things relate. This lesson is how to choose between them, and how to use the
links once they exist.
The three relationships, and how to tell them apart
Ask "how many of each side relate to how many of the other?" and the answer picks the field:
- One-to-many →
ForeignKey. One doctor has many appointments; each appointment has one doctor. TheForeignKeygoes on the "many" side (the appointment). This is by far the most common relationship. - Many-to-many →
ManyToManyField. A patient can have many diagnostic tests, and each test type is taken by many patients. Neither side "owns" one of the other. - One-to-one →
OneToOneField. Exactly one of each: aUserhas onePatientProfile, and that profile belongs to one user. Rare, but exactly right when you are extending one row with another.
Getting this choice right is the crux. A ForeignKey where you needed many-to-many forces awkward
workarounds later; the reverse over-complicates a simple link. Model the real cardinality.
ForeignKey: the one-to-many workhorse
In Nidaan, an appointment links to one patient and one doctor:
class Appointment(models.Model):
patient = models.ForeignKey(Patient, on_delete=models.CASCADE, related_name="appointments")
doctor = models.ForeignKey(Doctor, on_delete=models.PROTECT, related_name="appointments")
scheduled_for = models.DateTimeField()
The ForeignKey lives on Appointment (the many side). Two arguments carry real meaning, and beginners
skate past both:
on_delete — what happens to this row when the thing it points to is deleted. You must specify it, and
the choice is a genuine domain decision:
CASCADE— delete this row too. Deleting a patient deletes their appointments — verified: a patient with one appointment removed drops the appointment count from 5 to 4. Right when the child has no meaning without the parent.PROTECT— refuse the deletion, raisingProtectedError. Deleting a doctor who has appointments is blocked — verified, it raises rather than silently orphaning or destroying appointment history. Right when you must not lose the related data by accident.SET_NULL(needsnull=True) — keep the row, set the link toNULL. Right when the row outlives its parent (an appointment whose doctor left, kept for records).
Choosing CASCADE versus PROTECT versus SET_NULL is you deciding what should happen to real data — a
clinic almost certainly wants PROTECT on a doctor (never quietly delete medical history) and might want
CASCADE or SET_NULL elsewhere. It is not boilerplate; it is policy.
related_name — the name of the reverse accessor (below). Set it deliberately; it makes your reverse
queries readable.
Following a link: forward and reverse
Once the ForeignKey exists, you traverse it in both directions with attribute access — no SQL:
# forward: from the appointment to what it points to
appointment.patient.name # "Patient0"
appointment.doctor.name # "Rao"
# reverse: from the pointed-to object back to the many side, via related_name
patient.appointments.all() # all appointments for this patient
patient.appointments.count() # verified: 1
doctor.appointments.count() # verified: 3 (Dr Rao has 3 appointments)
Forward access (appointment.patient) gives you the single related object. Reverse access
(patient.appointments) gives you a manager over the many related objects — and it is named by
related_name. Without related_name, Django defaults to appointment_set, which works but reads poorly;
setting related_name="appointments" lets you write patient.appointments, which reads like English. The
reverse accessor is a full queryset, so you can filter it: patient.appointments.filter(status="booked").
Note both ForeignKeys above use related_name="appointments" — that is fine because they are on
different target models (Patient and Doctor), so patient.appointments and doctor.appointments are
unambiguous.
ManyToManyField: links with no owner
When each side relates to many of the other, use ManyToManyField. Suppose a diagnostic TestType (blood
sugar, X-ray) can be ordered for many patients, and a patient can have many test types:
class TestType(models.Model):
name = models.CharField(max_length=100)
class Patient(models.Model):
# ...
tests = models.ManyToManyField(TestType, related_name="patients", blank=True)
Behind the scenes Django creates a hidden join table holding pairs of ids — you never write it, but it is worth knowing it exists (it is where a many-to-many actually lives). You use it as a collection:
patient.tests.add(blood_sugar) # link them
patient.tests.remove(blood_sugar) # unlink
patient.tests.all() # all test types for this patient
blood_sugar.patients.all() # reverse: all patients with this test (related_name)
When the relationship itself needs data — when a test was ordered, by which doctor — you add a
through model (an explicit join model with its own fields). That is a step beyond this lesson, but
know the escape hatch exists: a plain ManyToManyField for a bare link, a through model when the link
carries information.
OneToOneField: extending a row
OneToOneField is a ForeignKey constrained to one-per-side. Its classic use is extending a model you do
not want to modify — most often Django's User:
class PatientProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
phone = models.CharField(max_length=15)
Now user.profile (singular — one object, not a set) gives the profile, and profile.user gives the user.
This is the sanctioned way to attach extra fields to User without a custom user model — though, as the
auth module will discuss, customising the user model early is often better. Use OneToOneField when
"exactly one of each" is the truth; using a ForeignKey there would wrongly permit many.
The rule of thumb
Count the cardinality, and let it choose the field. One-to-many is ForeignKey (on the many side);
many-to-many is ManyToManyField; one-to-one is OneToOneField. Then set on_delete as a real decision
about what should happen to data, and related_name so the reverse side reads well. Get these three right
across your models and the ORM lets you walk your data as naturally as following attributes — which is
exactly what the next lessons on querysets build on.
Check your work
How to choose the relationship. By cardinality: one-to-many → ForeignKey (on the many side);
many-to-many → ManyToManyField; one-to-one → OneToOneField.
What on_delete decides. What happens to this row when its target is deleted — CASCADE (delete too),
PROTECT (block, raising ProtectedError), SET_NULL (keep, null the link). A real data-policy choice.
Forward versus reverse access. Forward (appointment.patient) gives the one related object; reverse
(patient.appointments, named by related_name) gives a filterable manager over the many.
Why set related_name. It names the reverse accessor; without it you get appointment_set — related_name
makes reverse queries read well.
What a ManyToManyField creates, and through. A hidden join table of id pairs, used as a collection
(.add/.remove/.all); add a through model when the link itself needs fields.
When OneToOneField. When exactly one of each is the truth — classically extending User with a
profile, accessed as a single object (user.profile).
Practice
- Add the
doctorandpatientForeignKeys toAppointmentwithrelated_name="appointments". In the shell, create data and traverse both forward (a.doctor) and reverse (doctor.appointments.all()). - Set the doctor
ForeignKeytoon_delete=models.PROTECT, give the doctor an appointment, and try to delete the doctor. Confirm you getProtectedError. - Set the patient
ForeignKeytoCASCADE, delete a patient with appointments, and confirm the appointments vanish. Then reason about why a clinic wants PROTECT on the doctor but might accept CASCADE on the patient. - Add a
TestTypemodel and aManyToManyFieldfromPatient. Link and unlink with.add/.remove, and query both directions. - Filter a reverse accessor:
patient.appointments.filter(status="booked"). Confirm it is a full queryset. - Add a
PatientProfilewith aOneToOneFieldtoUser; accessuser.profileand note it is one object, not a set.
Official documentation
- Django — Relationships (model field reference) —
ForeignKey,ManyToManyField,OneToOneFieldand their options. - Django — Many-to-many relationships — Including
throughmodels. - Django —
on_deleteoptions — Every deletion behaviour.
Next: QuerySets and filtering.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship