Skip to content

Relationships & Foreign Keys

We need to store adoption applications. An application is for one animal, and an animal can have many applications. This is called a one-to-many relationship.

flowchart LR
    maria["Maria's application"] --> peanut(["Peanut"])
    aisha["Aisha's application"] --> peanut

In a relational database, that link is a foreign key: a column in one table that holds the id of a row in another.

The foreign key goes on the "many" side. Each application row holds the id of its animal, and an animal row holds nothing about its applications. To find an animal's applications, the database looks for the application rows that hold that animal's id.

Django has a field for each of the other kinds of relationship:

  • One-to-one: A foreign key that's also unique, so each row on one side links to at most one row on the other. Django's field is OneToOneField.
  • Many-to-many: A join table holds one row for each linked pair. Django's field is ManyToManyField, and the join table is created for us.

Adding Foreign Keys

We'll add a second model to animals/models.py, below Animal, with the foreign key as one of its fields:

animals/models.py
class AdoptionApplication(models.Model):
    animal = models.ForeignKey(
        Animal, on_delete=models.CASCADE, related_name="applications"
    )
    applicant_name = models.CharField(max_length=100)

We named the field animal, and its column is animal_id.

application.animal_id is the numeric id, and application.animal is the Animal object. The object is loaded the first time we use it.

The exercise's branch brings the rest of the model's fields.

Deleting with on_delete

on_delete is required. It decides what happens to an application when its animal is deleted:

  • models.CASCADE: Delete the applications too.
  • models.PROTECT: Refuse to delete the animal while it has applications.
  • models.SET_NULL: Keep the applications and clear the link. The field needs null=True.

We'll use CASCADE for our app, because in this version we don't need to keep the applications once the animal leaves the system.

Warning

Deleting an animal deletes all of its adoption applications, without asking.

We can follow a foreign key from both sides. From an application, the field gives us the animal: application.animal. From an animal, related_name gives us its applications: animal.applications.all().

If we leave related_name out, Django generates a name from the model: animal.adoptionapplication_set.

Migrating the New Model

Our models changed, so we need a new migration:

shell
uv run manage.py makemigrations
Text
Migrations for 'animals':
  animals/migrations/0003_adoptionapplication.py
    + Create model AdoptionApplication

Django compared our models with the migrations we already have, and wrote one for the new model alone. After looking over the file, we apply it:

shell
uv run manage.py migrate

The shell imports our models when it starts, so we start a new one to get AdoptionApplication:

shell
uv run manage.py shell

We'll start by deleting the animals we've saved so far, so we know exactly which rows each query finds. delete() deletes all the animals here, and the shell shows how many rows were deleted:

Python REPL
>>> Animal.objects.all().delete()

Then we create three animals.

In the shell:

Python
from datetime import date, timedelta
today = date.today()
year = timedelta(days=365)
Animal.objects.create(name="Rocket", species="dog", breed="Beagle mix", birth_date=today - 3 * year)
peanut = Animal.objects.create(name="Peanut", species="cat", birth_date=today - timedelta(days=60))
Animal.objects.create(name="Smokey", species="cat", birth_date=today - 4 * year, status=Animal.Status.PENDING)

We keep Peanut in a variable, because both applications are for Peanut. To set a foreign key, we pass the Animal object itself, and its id is saved in the animal_id column:

Python REPL
>>> maria = AdoptionApplication.objects.create(animal=peanut, applicant_name="Maria")
>>> aisha = AdoptionApplication.objects.create(animal=peanut, applicant_name="Aisha")