1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
|
# 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:
<img class="shadow" alt="Interactive API docs UI getting a single hero" src="/img/tutorial/fastapi/relationships/image01.png">
We get a response of:
```JSON hl_lines="5"
{
"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:
<img class="shadow" alt="Interactive API docs UI getting a single team" src="/img/tutorial/fastapi/relationships/image02.png">
Here we get a response of:
```JSON
{
"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 `HeroPublic` 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 `TeamPublic` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**.
{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[5:7,20:21,29:34,43:44] hl[5:7,20:21,29:34,43:44] *}
Now, remember that <a href="https://fastapi.tiangolo.com/tutorial/response-model/" class="external-link" target="_blank">FastAPI uses the `response_model` to validate and **filter** the response data</a>?
In this case, we used `response_model=TeamPublic` and `response_model=HeroPublic`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**:
{* ./docs_src/tutorial/fastapi/teams/tutorial001_py310.py ln[102:107,155:160] hl[102,107,155,160] *}
## 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:
```JSON hl_lines="2 13 24 34"
{
"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 `HeroPublicWithTeam` and `TeamPublicWithHeroes`.
We'll add them **after** the other models so that we can easily reference the previous models.
{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[59:64] hl[59:60,63:64] *}
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 `HeroPublicWithTeam` **inherits** from `HeroPublic`, which means that it will have the **normal fields for reading**, including the required `id` that was declared in `HeroPublic`.
And then it adds the **new field** `team`, which could be `None`, and is declared with the type `TeamPublic` with the base fields for reading a team.
Then we do the same for the `TeamPublicWithHeroes`, it **inherits** from `TeamPublic`, and declares the **new field** `heroes`, which is a list of `HeroPublic`.
### 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 `TeamPublicWithHeroes`, because that would again create that infinite recursion of data. Instead, we declare it with the normal `TeamPublic` model.
And the same for `TeamPublicWithHeroes`, the model used for the new field `heroes` uses `HeroPublic` 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**, `HeroPublic` and `TeamPublic`, 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.
{* ./docs_src/tutorial/fastapi/relationships/tutorial001_py310.py ln[111:116,164:169] hl[111,116,164,169] *}
## 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`:
<img class="shadow" alt="Interactive API docs UI getting a single hero with team" src="/img/tutorial/fastapi/relationships/image03.png">
Now we get the **team** data included:
```JSON hl_lines="7-11"
{
"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`:
<img class="shadow" alt="Interactive API docs UI getting a single team with the list of heroes" src="/img/tutorial/fastapi/relationships/image04.png">
Now we get the list of **heroes** included:
```JSON hl_lines="5-41"
{
"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. ✅
|