sqlmodel/docs/tutorial/fastapi/relationships.md
Sebastián Ramírez 6d1d86ab85 📝 Add docs
2021-08-24 15:02:48 +02:00

338 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `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**.
```Python hl_lines="3-5 9-10 14-19 23-24"
# 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:32-37]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:46-47]!}
# Code below omitted 👇
```
<details>
<summary>👀 Full file preview</summary>
```Python
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
```
</details>
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=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**:
```Python hl_lines="3 8 12 17"
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:105-110]!}
# Code here omitted 👈
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:160-165]!}
# Code below omitted 👇
```
<details>
<summary>👀 Full file preview</summary>
```Python
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
```
</details>
## 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 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.
```Python hl_lines="3-4 7-8"
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py[ln:61-66]!}
# Code below omitted 👇
```
<details>
<summary>👀 Full file preview</summary>
```Python
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!}
```
</details>
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 declare 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.
```Python hl_lines="3 8 12 17"
# 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 👇
```
<details>
<summary>👀 Full file preview</summary>
```Python
{!./docs_src/tutorial/fastapi/relationships/tutorial001.py!}
```
</details>
## 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 Margarets 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 additonal **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. ✅