Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
11 KiB
Models with Relationships in FastAPI
If we go right now and read a single hero by ID, we get the hero data with the team ID.
But we don't get any data about the particular team:
We get a response of:
{
"name": "Deadpond",
"secret_name": "Dive Wilson",
"age": null,
"team_id": 1,
"id": 1,
}
And the same way, if we get a team by ID, we get the team data, but we don't get any information about this team's heroes:
Here we get a response of:
{
"name": "Preventers",
"headquarters": "Sharp Tower",
"id": 2
}
...but no information about the heroes.
Let's update that. 🤓
Why Aren't We Getting More Data
First, why is it that we are not getting the related data for each hero and for each team?
It's because we declared the HeroRead
with only the same base fields of the HeroBase
plus the id
. But it doesn't include a field team
for the relationship attribute.
And the same way, we declared the TeamRead
with only the same base fields of the TeamBase
plus the id
. But it doesn't include a field heroes
for the relationship attribute.
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-9]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:22-23]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:31-36]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:45-46]!}
# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
Now, remember that FastAPI uses the response_model
to validate and filter the response data?
In this case, we used response_model=TeamRead
and response_model=HeroRead
, so FastAPI will use them to filter the response data, even if we return a table model that includes relationship attributes:
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:104-109]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:159-164]!}
# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
Don't Include All the Data
Now let's stop for a second and think about it.
We cannot simply include all the data, including all the internal relationships, because each hero has an attribute team
with their team, and then that team also has an attribute heroes
with all the heroes in the team, including this one.
If we tried to include everything, we could make the server application crash trying to extract infinite data, going through the same hero and team over and over again internally, something like this:
{
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
"team_id": 1,
"id": 1,
"team": {
"name": "Preventers",
"headquarters": "Sharp Tower",
"id": 2,
"heroes": [
{
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
"team_id": 1,
"id": 1,
"team": {
"name": "Preventers",
"headquarters": "Sharp Tower",
"id": 2,
"heroes": [
{
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
"team_id": 1,
"id": 1,
"team": {
"name": "Preventers",
"headquarters": "Sharp Tower",
"id": 2,
"heroes": [
...with infinite data here... 😱
]
}
}
]
}
}
]
}
}
As you can see, in this example, we would get the hero Rusty-Man, and from this hero we would get the team Preventers, and then from this team we would get its heroes, of course, including Rusty-Man... 😱
So we start again, and in the end, the server would just crash trying to get all the data with a "Maximum recursion error"
, we would not even get a response like the one above.
So, we need to carefully choose in which cases we want to include data and in which not.
What Data to Include
This is a decision that will depend on each application.
In our case, let's say that if we get a list of heroes, we don't want to also include each of their teams in each one.
And if we get a list of teams, we don't want to get a list of the heroes for each one.
But if we get a single hero, we want to include the team data (without the team's heroes).
And if we get a single team, we want to include the list of heroes (without each hero's team).
Let's add a couple more data models that declare that data so we can use them in those two specific path operations.
Models with Relationships
Let's add the models HeroReadWithTeam
and TeamReadWithHeroes
.
We'll add them after the other models so that we can easily reference the previous models.
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:61-66]!}
# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!}
These two models are very simple in code, but there's a lot happening here. Let's check it out.
Inheritance and Type Annotations
The HeroReadWithTeam
inherits from HeroRead
, which means that it will have the normal fields for reading, including the required id
that was declared in HeroRead
.
And then it adds the new field team
, which could be None
, and is declared with the type TeamRead
with the base fields for reading a team.
Then we do the same for the TeamReadWithHeroes
, it inherits from TeamRead
, and declares the new field heroes
, which is a list of HeroRead
.
Data Models Without Relationship Attributes
Now, notice that these new fields team
and heroes
are not declared with Relationship()
, because these are not table models, they cannot have relationship attributes with the magic access to get that data from the database.
Instead, here these are only data models that will tell FastAPI which attributes to get data from and which data to get from them.
Reference to Other Models
Also, notice that the field team
is not declared with this new TeamReadWithHeroes
, because that would again create that infinite recursion of data. Instead, we declare it with the normal TeamRead
model.
And the same for TeamReadWithHeroes
, the model used for the new field heroes
uses HeroRead
to get only each hero's data.
This also means that, even though we have these two new models, we still need the previous ones, HeroRead
and TeamRead
, because we need to reference them here (and we are also using them in the rest of the path operations).
Update the Path Operations
Now we can update the path operations to use the new models.
This will tell FastAPI to take the object that we return from the path operation function (a table model) and access the additional attributes from them to extract their data.
In the case of the hero, this tells FastAPI to extract the team
too. And in the case of the team, to extract the list of heroes
too.
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:113-118]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:168-173]!}
# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!}
Check It Out in the Docs UI
Now let's try it out again in the docs UI.
Let's try again with the same hero with ID 1
:
Now we get the team data included:
{
"name": "Deadpond",
"secret_name": "Dive Wilson",
"age": null,
"team_id": 1,
"id": 1,
"team": {
"name": "Z-Force",
"headquarters": "Sister Margaret’s Bar",
"id": 1
}
}
And if we get now the team with ID 2
:
Now we get the list of heroes included:
{
"name": "Preventers",
"headquarters": "Sharp Tower",
"id": 2,
"heroes": [
{
"name": "Rusty-Man",
"secret_name": "Tommy Sharp",
"age": 48,
"team_id": 2,
"id": 2
},
{
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador",
"age": null,
"team_id": 2,
"id": 3
},
{
"name": "Tarantula",
"secret_name": "Natalia Roman-on",
"age": 32,
"team_id": 2,
"id": 6
},
{
"name": "Dr. Weird",
"secret_name": "Steve Weird",
"age": 36,
"team_id": 2,
"id": 7
},
{
"name": "Captain North America",
"secret_name": "Esteban Rogelios",
"age": 93,
"team_id": 2,
"id": 8
}
]
}
Recap
Using the same techniques to declare additional data models, we can tell FastAPI what data to return in the responses, even when we return table models.
Here we almost didn't have to change the FastAPI app code, but of course, there will be cases where you need to get the data and process it in different ways in the path operation function before returning it.
But even in those cases, you will be able to define the data models to use in response_model
to tell FastAPI how to validate and filter the data.
By this point, you already have a very robust API to handle data in a SQL database combining SQLModel with FastAPI, and implementing best practices, like data validation, conversion, filtering, and documentation. ✨
In the next chapter, I'll tell you how to implement automated testing for your application using FastAPI and SQLModel. ✅