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:
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 needsnull=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.
Reverse Lookups with related_name
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:
uv run manage.py makemigrations
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:
uv run manage.py migrate
Saving Related Rows
The shell imports our models when it starts, so we start a new one to get AdoptionApplication:
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:
>>> Animal.objects.all().delete()
Then we create three animals.
In the shell:
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:
>>> maria = AdoptionApplication.objects.create(animal=peanut, applicant_name="Maria")
>>> aisha = AdoptionApplication.objects.create(animal=peanut, applicant_name="Aisha")