mirror of
https://github.com/fastapi/sqlmodel.git
synced 2026-01-08 10:29:38 -06:00
📝 Add docs
This commit is contained in:
44
docs/tutorial/fastapi/delete.md
Normal file
44
docs/tutorial/fastapi/delete.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Delete Data with FastAPI
|
||||
|
||||
Let's now add a *path operation* to delete a hero.
|
||||
|
||||
This is quite straightforward. 😁
|
||||
|
||||
## Delete Path Operation
|
||||
|
||||
Because we want to **delete** data, we use an HTTP `DELETE` operation.
|
||||
|
||||
We get a `hero_id` from the path parameter and verify if it exists, just as we did when reading a single hero or when updating it, **possibly raising an error** with a `404` response.
|
||||
|
||||
And if we actually find a hero, we just delete it with the **session**.
|
||||
|
||||
```Python hl_lines="3-11"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:91-99]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/delete/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
After deleting it successfully, we just return a response of:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"ok": true
|
||||
}
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
That's it, feel free to try it out in the interactve docs UI to delete some heroes. 💥
|
||||
|
||||
Using **FastAPI** to read data and combining it with **SQLModel** makes it quite straightforward to delete data from the database.
|
||||
17
docs/tutorial/fastapi/index.md
Normal file
17
docs/tutorial/fastapi/index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# FastAPI and Pydantic - Intro
|
||||
|
||||
One of the use cases where **SQLModel** shines the most, and the main one why it was built, was to be combined with **FastAPI**. ✨
|
||||
|
||||
<a href="https://fastapi.tiangolo.com/" class="external-link" target="_blank">FastAPI</a> is a Python web framework for building web APIs created by the same <a href="https://twitter.com/tiangolo" class="external-link" target="_blank">author</a> of SQLModel. FastAPI is also built on top of **Pydantic**.
|
||||
|
||||
In this group of chapters we will see how to combine SQLModel **table models** representing tables in the SQL database as all the ones we have seen up to now, with **data models** that only represent data (which are actually just Pydantic models behind the scenes).
|
||||
|
||||
Being able to combine SQLModel **table** models with pure **data** models would be useful on its own, but to make all the examples more concrete, we will use them with **FastAPI**.
|
||||
|
||||
By the end we will have a **simple** but **complete** web **API** to interact with the data in the database. 🎉
|
||||
|
||||
## Learning FastAPI
|
||||
|
||||
If you have never used FastAPI, maybe a good idea would be to go and study it a bit before continuing.
|
||||
|
||||
Just reading and trying the examples on the <a href="https://fastapi.tiangolo.com/" class="external-link" target="_blank">FastAPI main page</a> should be enough, and it shouldn't take you more than **10 minutes**.
|
||||
64
docs/tutorial/fastapi/limit-and-offset.md
Normal file
64
docs/tutorial/fastapi/limit-and-offset.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Read Heroes with Limit and Offset wtih FastAPI
|
||||
|
||||
When a client sends a request to get all the heroes, we have been returning them all.
|
||||
|
||||
But if we had **thousands** of heroes that could consume a lot of **computational resources**, network bandwith, etc.
|
||||
|
||||
So we probably want to limit it.
|
||||
|
||||
Let's use the same **offset** and **limit** we learned about in the previous tutorial chapters for the API.
|
||||
|
||||
!!! info
|
||||
In many cases this is also called **pagination**.
|
||||
|
||||
## Add a Limit and Offset to the Query Parameters
|
||||
|
||||
Let's add `limit` and `offset` to the query parameters.
|
||||
|
||||
By default, we will return the first results from the database, so `offset` will have a default value of `0`.
|
||||
|
||||
And by default, we will return a maximum of `100` heroes, so `limit` will have a default value of `100`.
|
||||
|
||||
```Python hl_lines="3 9 11"
|
||||
{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py[ln:54-58]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
We want to allow clients to set a different `offset` and `limit` values.
|
||||
|
||||
But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱
|
||||
|
||||
So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess **t**han or **e**qual to `100` with `lte=100`.
|
||||
|
||||
This way, a client can decide to take less heroes if they want, but not more.
|
||||
|
||||
!!! info
|
||||
If you need to refresh how query parameters and their validation work, check out the docs in FastAPI:
|
||||
|
||||
* <a href="https://fastapi.tiangolo.com/tutorial/query-params/" class="external-link" target="_blank">Query Parameters</a>
|
||||
* <a href="https://fastapi.tiangolo.com/tutorial/query-params-str-validations/" class="external-link" target="_blank">Query Parameters and String Validations</a>
|
||||
* <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/" class="external-link" target="_blank">Path Parameters and Numeric Validations</a>
|
||||
|
||||
## Check the Docs UI
|
||||
|
||||
Now we can see that the docs UI shows the new parameters to control **limit** and **offset** of our data.
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/limit-and-offset/image01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
You can use **FastAPI**'s automatic data validation to get the parameters for `limit` and `offset`, and then use them with the **session** to control ranges of data to be sent in responses.
|
||||
443
docs/tutorial/fastapi/multiple-models.md
Normal file
443
docs/tutorial/fastapi/multiple-models.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# Multiple Models with FastAPI
|
||||
|
||||
We have been using the same `Hero` model to declare the schema of the data we receive in the API, the table model in the database, and the schema of the data we send back in responses.
|
||||
|
||||
But in most of the cases there are slight differences, let's use multiple models to solve it.
|
||||
|
||||
Here you will see the main and biggest feature of **SQLModel**. 😎
|
||||
|
||||
## Review Creation Schema
|
||||
|
||||
Let's start by reviewing the automatically generated schemas from the docs UI.
|
||||
|
||||
For input we have:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/simple-hero-api/image01.png">
|
||||
|
||||
If we pay attention, it shows that the client *could* send an `id` in the JSON body of the request.
|
||||
|
||||
This means that the client could try to use the same ID that already exists in the database for another hero.
|
||||
|
||||
That's not what we want.
|
||||
|
||||
We want the client to only send the data that is needed to create a new hero:
|
||||
|
||||
* `name`
|
||||
* `secret_name`
|
||||
* Optional `age`
|
||||
|
||||
And we want the `id` to be generated automatically by the database, so we don't want the client to send it.
|
||||
|
||||
We'll see how to fix it in a bit.
|
||||
|
||||
## Review Response Schema
|
||||
|
||||
Now let's review the schema of the response we send back to the client in the docs UI.
|
||||
|
||||
If you click the small tab <kbd>Schema</kbd> instead of the <kbd>Example Value</kbd>, you will see something like this:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/multiple-models/image01.png">
|
||||
|
||||
Let's see the details.
|
||||
|
||||
The fields with a red asterisk (<span style="color: #ff0000;">*</span>) are "required".
|
||||
|
||||
This means that our API application is required to return those fields in the response:
|
||||
|
||||
* `name`
|
||||
* `secret_name`
|
||||
|
||||
The `age` is optional, we don't have to return it, or it could be `None` (or `null` in JSON), but the `name` and the `secret_name` are required.
|
||||
|
||||
Here's the weird thing, the `id` currently seems also "optional". 🤔
|
||||
|
||||
This is because in our **SQLModel** class we declare the `id` with `Optional[int]`, because it could be `None` in memory until we save it in the database and we finally get the actual ID.
|
||||
|
||||
But in the responses, we would always send a model from the database, and it would **always have an ID**. So the `id` in the responses could be declared as required too.
|
||||
|
||||
This would mean that our application is making the compromise with the clients that if it sends a hero, it would for sure have an `id` with a value, it would not be `None`.
|
||||
|
||||
### Why Is it Important to Compromise with the Responses
|
||||
|
||||
The ultimate goal of an API is for some **clients to use it**.
|
||||
|
||||
The clients could be a frontend application, a command line program, a graphical user interface, a mobile application, another backend application, etc.
|
||||
|
||||
And the code those clients write depend on what our API tells them they **need to send**, and what they can **expect to receive**.
|
||||
|
||||
Making both sides very clear will make it much easier to interact with the API.
|
||||
|
||||
And in most of the cases, the developer of the client for that API **will also be yourself**, so you are **doing your future self a favor** by declaring those schemas for requests and responses. 😉
|
||||
|
||||
### So Why is it Important to Have Required IDs
|
||||
|
||||
Now, what's the matter with having one **`id` field marked as "optional"** in a response when in reality it is always required?
|
||||
|
||||
For example, **automatically generated clients** in other languages (or also in Python) would have some declaration that this field `id` is optional.
|
||||
|
||||
And then the developers using those clients in their languages would have to be checking all the time in all their code if the `id` is not `None` before using it anywhere.
|
||||
|
||||
That's a lot of unnecessary checks and **unnecessary code** that could have been saved by declaring the schema properly. 😔
|
||||
|
||||
It would be a lot simpler for that code to know that the `id` from a response is required and **will always have a value**.
|
||||
|
||||
Let's fix that too. 🤓
|
||||
|
||||
## Multiple Hero Schemas
|
||||
|
||||
So, we want to have our `Hero` model that declares the **data in the database**:
|
||||
|
||||
* `id`, optional on creation, required on database
|
||||
* `name`, required
|
||||
* `secret_name`, required
|
||||
* `age`, optional
|
||||
|
||||
But we also want to have a `HeroCreate` for the data we want to receive when **creating** a new hero, which is almost all the same data as `Hero`, except for the `id`, because that is created automatically by the database:
|
||||
|
||||
* `name`, required
|
||||
* `secret_name`, required
|
||||
* `age`, optional
|
||||
|
||||
And we want to have a `HeroRead` with the `id` field, but this time annotated with `id: int`, instead of `id: Optional[int]`, to make it clear that it is required in responses **read** from the clients:
|
||||
|
||||
* `id`, required
|
||||
* `name`, required
|
||||
* `secret_name`, required
|
||||
* `age`, optional
|
||||
|
||||
## Multiple Models with Duplicated Fields
|
||||
|
||||
The simplest way to solve it could be to create **multiple models**, each one with all the corresponding fields:
|
||||
|
||||
```Python hl_lines="5-9 12-15 18-22"
|
||||
# This would work, but there's a better option below 🚨
|
||||
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:7-24]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Here's the important detail, and probably the most important feature of **SQLModel**: only `Hero` is declared with `table = True`.
|
||||
|
||||
This means that the class `Hero` represents a **table** in the database. It is both a **Pydantic** model and a **SQLAlchemy** model.
|
||||
|
||||
But `HeroCreate` and `HeroRead` don't have `table = True`. They are only **data models**, they are only **Pydantic** models. They won't be used with the database, but only to declare data schemas for the API (or for other uses).
|
||||
|
||||
This also means that `SQLModel.metadata.create_all()` won't create tables in the database for `HeroCreate` and `HeroRead`, because they don't have `table = True`, which is exactly what we want. 🚀
|
||||
|
||||
!!! tip
|
||||
We will improve this code to avoid duplicating the fields, but for now we can continue learning with these models.
|
||||
|
||||
## Use Multiple Models to Create a Hero
|
||||
|
||||
Let's now see how to use these new models in the FastAPI application.
|
||||
|
||||
Let's first check how is the process to create a hero now:
|
||||
|
||||
```Python hl_lines="3-4 6"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:46-53]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Let's check that in detail.
|
||||
|
||||
Now we use the type annotation `HeroCreate` for the request JSON data, in the `hero` parameter of the **path operation function**.
|
||||
|
||||
```Python hl_lines="3"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:47]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
Then we create a new `Hero` (this is the actual **table** model that saves things to the database) using `Hero.from_orm()`.
|
||||
|
||||
The method `.from_orm()` reads data from another object with attributes and creates a new instance of this class, in this case `Hero`.
|
||||
|
||||
The alternative is `Hero.parse_obj()` that reads data from a dictionary.
|
||||
|
||||
But as in this case we have a `HeroCreate` instance in the `hero` variable, this is an object with attributes, so we use `.from_orm()` to read those attributes.
|
||||
|
||||
With this we create a new `Hero` instance (the one for the database) and put it in the variable `db_hero` from the data in the `hero` variable that is the `HeroCreate` instance we received from the request.
|
||||
|
||||
```Python hl_lines="3"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:49]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
Then we just `add` it to the **session**, `commit`, and `refresh` it, and finally we return the same `db_hero` variable that has the just refreshed `Hero` instance.
|
||||
|
||||
Because it is just refreshed, it has the `id` field set with a new ID taken from the database.
|
||||
|
||||
And now that we return it, FastAPI will validate the data with the `response_model`, which is a `HeroRead`:
|
||||
|
||||
```Python hl_lines="3"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial001.py[ln:46]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
This will validate that all the data that we promised is there, and will remove any data we didn't declare.
|
||||
|
||||
!!! tip
|
||||
This filtering could be very important, and could be a very good security feature, for example to make sure you filter private data, hashed passwords, etc.
|
||||
|
||||
You can read more about it in the <a href="https://fastapi.tiangolo.com/tutorial/response-model/" class="external-link" target="_blank">FastAPI docs about Response Model</a>.
|
||||
|
||||
In particular, it will make sure that the `id` is there, and that it is indeed an integer (and not `None`).
|
||||
|
||||
## Shared Fields
|
||||
|
||||
But looking closely, we could see that these models have a lot of **duplicated information**.
|
||||
|
||||
All **the 3 models** declare that thay share some **common fields** that look exactly the same:
|
||||
|
||||
* `name`, required
|
||||
* `secret_name`, required
|
||||
* `age`, optional
|
||||
|
||||
And then they declare other fields with some differences (in this case only about the `id`).
|
||||
|
||||
We want to **avoid duplicated information** if possible.
|
||||
|
||||
This is important if, for example, in the future we decide to **refactor the code** and rename one field (column). For example, from `secret_name` to `secret_identity`.
|
||||
|
||||
If we have that duplicated in multiple models, we could easily forget to update one of them. But if we **avoid duplication**, there's only one place that would need updating. ✨
|
||||
|
||||
Let's now improve that. 🤓
|
||||
|
||||
## Multiple Models with Inheritance
|
||||
|
||||
And here it is, you found the biggest feature of **SQLModel**. 💎
|
||||
|
||||
Each of these models is only a **data model** or both a data model and a **table model**.
|
||||
|
||||
So, it's possible to create models with **SQLModel** that don't represent tables in the database.
|
||||
|
||||
On top of that, we can use inheritance to avoid duplicated information in these models.
|
||||
|
||||
We can see from above that they all share some **base** fields:
|
||||
|
||||
* `name`, required
|
||||
* `secret_name`, required
|
||||
* `age`, optional
|
||||
|
||||
So let's create a **base** model `HeroBase` that the others can inherit from:
|
||||
|
||||
```Python hl_lines="3-6"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-10]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
As you can see, this is *not* a **table model**, it doesn't have the `table = True` config.
|
||||
|
||||
But now we can create the **other models inheriting from it**, they will all share these fields, just as if they had them declared.
|
||||
|
||||
### The `Hero` **Table Model**
|
||||
|
||||
Let's start with the only **table model**, the `Hero`:
|
||||
|
||||
```Python hl_lines="9-10"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-14]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Notice that `Hero` now doesn't inherit from `SQLModel`, but from `HeroBase`.
|
||||
|
||||
And now we only declare one single field directly, the `id`, that here is `Optional[int]`, and is a `primary_key`.
|
||||
|
||||
And even though we don't declare the other fields **explicitly**, because they are inherited, they are also part of this `Hero` model.
|
||||
|
||||
And of course, all these fields will be in the columns for the resulting `hero` table in the database.
|
||||
|
||||
And those inherited fields will also be in the **autocompletion** and **inline errors** in editors, etc.
|
||||
|
||||
### The `HeroCreate` **Data Model**
|
||||
|
||||
Now let's see the `HeroCreate` model that will be used to define the data that we want to receive in the API when creating a new hero.
|
||||
|
||||
This is a fun one:
|
||||
|
||||
```Python hl_lines="13-14"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-18]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
What's happening here?
|
||||
|
||||
The fields we need to create are **exactly the same** as the ones in the `HeroBase` model. So we don't have to add anything.
|
||||
|
||||
And because we can't leave the empty space when creating a new class, but we don't want to add any field, we just use `pass`.
|
||||
|
||||
This means that there's nothing else special in this class apart from the fact that it is named `HeroCreate` and that it inherits from `HeroBase`.
|
||||
|
||||
As an alternative, we could use `HeroBase` directly in the API code instead of `HeroCreate`, but it would show up in the auomatic docs UI with that name "`HeroBase`" which could be **confusing** for clients. Instead, "`HeroCreate`" is a bit more explicit about what it is for.
|
||||
|
||||
On top of that, we could easily decide in the future that we want to receive **more data** when creating a new hero apart from the data in `HeroBase` (for example a password), and now we already have the class to put those extra fields.
|
||||
|
||||
### The `HeroRead` **Data Model**
|
||||
|
||||
Now let's check the `HeroRead` model.
|
||||
|
||||
This one just declares that the `id` field is required when reading a hero from the API, because a hero read from the API will come from the database, and in the database it will always have an ID.
|
||||
|
||||
```Python hl_lines="17-18"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py[ln:7-22]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/multiple_models/tutorial002.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Review the Updated Docs UI
|
||||
|
||||
The FastAPI code is still the same as above, we still use `Hero`, `HeroCreate`, and `HeroRead`. But now we define them in a smarter way with inheritance.
|
||||
|
||||
So, we can jump to the docs UI right away and see how they look with the updated data.
|
||||
|
||||
### Docs UI to Create a Hero
|
||||
|
||||
Let's see the new UI for creating a hero:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/multiple-models/image02.png">
|
||||
|
||||
Nice! It now shows that to create a hero, we just pass the `name`, `secret_name`, and optinally `age`.
|
||||
|
||||
We no longer pass an `id`.
|
||||
|
||||
### Docs UI with Hero Responses
|
||||
|
||||
Now we can scroll down a bit to see the response schema:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/multiple-models/image03.png">
|
||||
|
||||
We can now see that `id` is a required field, it has a red asterisk (<span style="color: #f00;">*</span>).
|
||||
|
||||
And if we check the schema for the **Read Heroes** *path operation* it will also show the updated schema.
|
||||
|
||||
## Inheritance and Table Models
|
||||
|
||||
We just saw how powerful inheritance of these models can be.
|
||||
|
||||
This is a very simple example, and it might look a bit... meh. 😅
|
||||
|
||||
But now imagine that your table has **10 or 20 columns**. And that you have to duplicate all that information for all your **data models**... then it becomes more obvious why it's quite useful to be able to avoid all that information duplication with inheritance.
|
||||
|
||||
Now, this probably looks so flexible that it's not obvious **when to use inheritance** and for what.
|
||||
|
||||
Here are a couple of rules of thumb that can help you.
|
||||
|
||||
### Only Inherit from Data Models
|
||||
|
||||
Only inherit from **data models**, don't inherit from **table models**.
|
||||
|
||||
It will help you avoid confusion, and there won't be any reason for you to need to inherit from a **table model**.
|
||||
|
||||
If you feel like you need to inherit from a **table model**, then instead create a **base** class that is only a **data model** and has all those fields, like `HeroBase`.
|
||||
|
||||
And then inherit from that **base** class that is only a **data model** for any other **data model** and for the **table model**.
|
||||
|
||||
### Avoid Duplication - Keep it Simple
|
||||
|
||||
It could feel like you need to have a profound reason why to inherit from one model or another, because "in some mystical way" they separate different concepts... or something like that.
|
||||
|
||||
In some cases, there are **simple separations** that you can use, like the models to create data, read, update, etc. If that's quick and obvious, nice, use it. 💯
|
||||
|
||||
Otherwise, don't worry too much about profound conceptual reasons to separate models, just try to **avoid duplication** and **keep the code simple** enough to reason about it.
|
||||
|
||||
If you see you have a lot of **overlap** between two models, then you can probably **avoid some of that duplication** with a base model.
|
||||
|
||||
But if to avoid some duplication you end up with a crazy tree of models with inheritance, then it might be **simpler** to just duplicate some of those fields, and that might be easier to reason about and to maintain.
|
||||
|
||||
Do whatever is easier to **reason** about, to **program** with, to **maintain**, and to **refactor** in the future. 🤓
|
||||
|
||||
Remember that inheritance, the same as **SQLModel**, and anything else, are just tools to **help you be more productive**, that's one of their main objectives. If something is not helping with that (e.g. too much duplication, too much complexity), then change it. 🚀
|
||||
|
||||
## Recap
|
||||
|
||||
You can use **SQLModel** to declare multiple models:
|
||||
|
||||
* Some models can be only **data models**. They will also be **Pydantic** models.
|
||||
* And some can *also* be **table models** (apart from already being **data models**) by having the config `table = True`. They will also be **Pydantic** models and **SQLAlchemy** models.
|
||||
|
||||
Only the **table models** will create tables in the database.
|
||||
|
||||
So, you can use all the other **data models** to validate, convert, filter, and document the schema of the data for your application. ✨
|
||||
|
||||
You can use inheritance to **avoid information and code duplication**. 😎
|
||||
|
||||
And you can use all these models directly with **FastAPI**. 🚀
|
||||
97
docs/tutorial/fastapi/read-one.md
Normal file
97
docs/tutorial/fastapi/read-one.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Read One Model with FastAPI
|
||||
|
||||
Let's now add a *path operation* to read a single model to our **FastAPI** application.
|
||||
|
||||
## Path Operation for One Hero
|
||||
|
||||
Let's add a new *path operation* to read one single hero.
|
||||
|
||||
We want to get the hero based on the `id`, so we will use a **path parameter** `hero_id`.
|
||||
|
||||
!!! info
|
||||
If you need to refresh how *path parameters* work, including their data validation, check the <a href="https://fastapi.tiangolo.com/tutorial/path-params/" class="external-link" target="_blank">FastAPI docs about Path Parameters</a>.
|
||||
|
||||
```Python hl_lines="8"
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
For example, to get the hero with ID `2` we would send a `GET` request to:
|
||||
|
||||
```
|
||||
/heroes/2
|
||||
```
|
||||
|
||||
## Handling Errors
|
||||
|
||||
Then, because FastAPI already takes care of making sure that the `hero_id` is an actual integer, we can use it directly with `Hero.get()` to try and get one hero by that ID.
|
||||
|
||||
But if the integer is not the ID of any hero in the database, it will not find anything, and the variable `hero` will be `None`.
|
||||
|
||||
So, we check it in an `if` block, if it's `None`, we raise an `HTTPException` with a `404` status code.
|
||||
|
||||
And to use it we first import `HTTPException` from `fastapi`.
|
||||
|
||||
This will let the client know that they probably made a mistake on their side and requested a hero that doesn't exist in the database.
|
||||
|
||||
```Python hl_lines="3 11-13"
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Return the Hero
|
||||
|
||||
Then, if the hero exists, we return it.
|
||||
|
||||
And because we are using the `response_model` with `HeroRead`, it will be validated, documented, etc.
|
||||
|
||||
```Python hl_lines="8 14"
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py[ln:61-67]!}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/read_one/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Check the Docs UI
|
||||
|
||||
We can then go to the docs UI and see the new *path operation*.
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/read-one/image01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
You can combine **FastAPI** features like automatic path parameter validation to get models by ID.
|
||||
337
docs/tutorial/fastapi/relationships.md
Normal file
337
docs/tutorial/fastapi/relationships.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# 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 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 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. ✅
|
||||
110
docs/tutorial/fastapi/response-model.md
Normal file
110
docs/tutorial/fastapi/response-model.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# FastAPI Response Model with SQLModel
|
||||
|
||||
Now I'll show you how to use FastAPI's `response_model` with **SQLModel**.
|
||||
|
||||
## Interactive API Docs
|
||||
|
||||
Up to now, with the code we have used, the API docs know the data the clients have to send:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/simple-hero-api/image01.png">
|
||||
|
||||
This interactive docs UI is powered by <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>, and what Swagger UI does is to read a big JSON content that defines the API with all the data schemas (data shapes) using the standard <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md" class="external-link" target="_blank">OpenAPI</a>, and showing it in that nice <abbr title="User Interface">UI</abbr>.
|
||||
|
||||
FastAPI automatically **generates that OpenAPI** for Swagger UI to read it.
|
||||
|
||||
And it generates it **based on the code you write**, using the Pydantic models (in this case **SQLModel** models) and type annotations to know the schemas of the data that the API handles.
|
||||
|
||||
## Response Data
|
||||
|
||||
But up to now, the API docs UI doesn't know the schema of the *responses* our app sends back.
|
||||
|
||||
You can see that there's a possible "Successful Response" with a code `200`, but we have no idea how the response data would look like.
|
||||
|
||||
<img class="shadow" alt="API docs UI without response data schemas" src="/img/tutorial/fastapi/response-model/image01.png">
|
||||
|
||||
Right now we only tell FastAPI the data we want to receive, but we don't tell it yet the data we want to send back.
|
||||
|
||||
Let's do that now. 🤓
|
||||
|
||||
## Use `response_model`
|
||||
|
||||
We can use `response_model` to tell FastAPI the schema of the data we want to send back.
|
||||
|
||||
For example, we can pass the same `Hero` **SQLModel** class (because it is also a Pydantic model):
|
||||
|
||||
```Python hl_lines="3"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:33-39]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## List of Heroes in `response_model`
|
||||
|
||||
We can also use other type annotations, the same way we can use with Pydantic fields. For example, we can pass a list of `Hero`s.
|
||||
|
||||
First, we import `List` from `typing` and then we declare the `response_model` with `List[Hero]`:
|
||||
|
||||
```Python hl_lines="1 5"
|
||||
{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:1]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/response_model/tutorial001.py[ln:42-46]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/response_model/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## FastAPI and Response Model
|
||||
|
||||
FastAPI will do data validation and filtering of the response with this `response_model`.
|
||||
|
||||
So this works like a contract between our application and the client.
|
||||
|
||||
You can read more about it in the <a href="https://fastapi.tiangolo.com/tutorial/response-model/" class="external-link" target="_blank">FastAPI docs about `response_model`</a>.
|
||||
|
||||
## New API Docs UI
|
||||
|
||||
Now we can go back to the docs UI and see that they now show the schema of the response we will receive.
|
||||
|
||||
<img class="shadow" alt="API docs UI without response data schemas" src="/img/tutorial/fastapi/response-model/image02.png">
|
||||
|
||||
The clients will know what data they should expect.
|
||||
|
||||
## Automatic Clients
|
||||
|
||||
The most visible advantage of using the `response_model` is that it shows up in the API docs UI.
|
||||
|
||||
But there are other advantages, like that FastAPI will do automatic <a href="https://fastapi.tiangolo.com/tutorial/response-model/" class="external-link" target="_blank">data validation and filtering</a> of the response data using this model.
|
||||
|
||||
Additionally, because the schemas are defined in using a standard, there are many tools that can take advantage of this.
|
||||
|
||||
For example, client generators, that can automatically create the code necessary to talk to your API in many languages.
|
||||
|
||||
!!! info
|
||||
If you are curious about the standards, FastAPI generates OpenAPI, that internally uses JSON Schema.
|
||||
|
||||
You can read about all that in the <a href="https://fastapi.tiangolo.com/tutorial/first-steps/#openapi" class="external-link" target="_blank">FastAPI docs - First Steps</a>.
|
||||
|
||||
## Recap
|
||||
|
||||
Use the `response_model` to tell FastAPI the schema of the data you want to send back and have awesome data APIs. 😎
|
||||
198
docs/tutorial/fastapi/session-with-dependency.md
Normal file
198
docs/tutorial/fastapi/session-with-dependency.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Session with FastAPI Dependency
|
||||
|
||||
Before we keep adding things, let's change a bit how we get the session for each request to simplify our life later.
|
||||
|
||||
## Current Sessions
|
||||
|
||||
Up to now, we have been creating a session in each *path operation*, in a `with` block.
|
||||
|
||||
```Python hl_lines="5"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/delete/tutorial001.py[ln:50-57]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/delete/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
That's perfectly fine, but in many use cases we would want to use <a href="https://fastapi.tiangolo.com/tutorial/dependencies/" class="external-link" target="_blank">FastAPI Dependencies</a>, for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*.
|
||||
|
||||
These dependencies are also very useful during **testing**, because we can **easily replace them**, and then, for example, use a new database for our tests, or put some data before the tests, etc.
|
||||
|
||||
So, let's refactor these sessions to use **FastAPI Dependencies**.
|
||||
|
||||
## Create a **FastAPI** Dependency
|
||||
|
||||
A **FastAPI** dependency is very simple, it's just a function that returns a value.
|
||||
|
||||
It could use `yield` instead of `return`, and in that case **FastAPI** will make sure it executes all the code **after** the `yield`, once it is done with the request.
|
||||
|
||||
```Python hl_lines="3-5"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Use the Dependency
|
||||
|
||||
Now let's make FastAPI execute a dependency and get its value in the *path operation*.
|
||||
|
||||
We import `Depends()` from `fastapi`. Then we use it in the *path operation function* in a **parameter**, the same way we declared parameters to get JSON bodies, path parameters, etc.
|
||||
|
||||
```Python hl_lines="3 15"
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
!!! tip
|
||||
Here's a tip about that `*,` thing in the parameters.
|
||||
|
||||
Here we are passing the parameter `session` that has a "default value" of `Depends(get_session)` before the parameter `hero`, that doesn't have any default value.
|
||||
|
||||
Python would normally complain about that, but we can use the initial "parameter" `*,` to mark all the rest of the parameters as "keyword only", which solves the problem.
|
||||
|
||||
You can read more about it in the FastAPI documentation <a href="https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#order-the-parameters-as-you-need-tricks" class="external-link" target="_blank">Path Parameters and Numeric Validations - Order the parameters as you need, tricks</a>
|
||||
|
||||
The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code, and will give you the value from that dependency.
|
||||
|
||||
If it had `yield`, then it will continue the rest of the execution once you are done sending the response. In the case of the **session**, it will finish the cleanup code from the `with` block, closing the session, etc.
|
||||
|
||||
Then FastAPI will call it again for the **next request**.
|
||||
|
||||
Because it is called **once per request**, we will still get a **single session per request** as we should, so we are still fine with that. ✅
|
||||
|
||||
And because dependencies can use `yield`, FastAPI will make sure to run the code **after** the `yield` once it is done, including all the **cleanup code** at the end of the `with` block. So we are also fine with that. ✅
|
||||
|
||||
## The `with` Block
|
||||
|
||||
This means that in the main code of the *path operation function*, it will work equivalently to the previous version with the explicit `with` block.
|
||||
|
||||
```Python hl_lines="16-20"
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes.
|
||||
|
||||
But now, the `with` block is not explicitly in the function, but in the dependency above:
|
||||
|
||||
```Python hl_lines="9-10"
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-61]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
We will see how this is very useful when testing the code later. ✅
|
||||
|
||||
## Update the Path Operations to Use the Dependency
|
||||
|
||||
Now we can update the rest of the *path operations* to use the new dependency.
|
||||
|
||||
We just declare the dependency in the parameters of the function, with:
|
||||
|
||||
```Python
|
||||
session: Session = Depends(get_session)
|
||||
```
|
||||
|
||||
And then we remove the previous `with` block with the old **session**.
|
||||
|
||||
```Python hl_lines="15 26 35 44 59"
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:42-44]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py[ln:55-107]!}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Recap
|
||||
|
||||
You just learned how to use **FastAPI dependencies** to handle the database session. This will come in handy later when testing the code.
|
||||
|
||||
And you will see how much these dependencies can help the more you work with FastAPI, to handle **permissions**, **authentication**, resources like database **sessions**, etc. 🚀
|
||||
|
||||
If you want to learn more about dependencies, checkout the <a href="https://fastapi.tiangolo.com/tutorial/dependencies/" class="external-link" target="_blank">FastAPI docs about Dependencies</a>.
|
||||
290
docs/tutorial/fastapi/simple-hero-api.md
Normal file
290
docs/tutorial/fastapi/simple-hero-api.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Simple Hero API with FastAPI
|
||||
|
||||
Let's start by building a simple hero web API with **FastAPI**. ✨
|
||||
|
||||
## Install **FastAPI**
|
||||
|
||||
The first step is to install FastAPI.
|
||||
|
||||
FastAPI is the framework to create the **web API**.
|
||||
|
||||
But we also need another type of program to run it, it is called a "**server**". We will use **Uvicorn** for that. And we will install Uvicorn with its *standard* dependencies.
|
||||
|
||||
Make sure you [have a virtual environment activated](../index.md#create-a-python-virtual-environment){.internal-link target=_blank}.
|
||||
|
||||
Then install FastAPI and Uvicorn:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python -m pip install fastapi "uvicorn[standard]"
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
s
|
||||
## **SQLModel** Code - Models, Engine
|
||||
|
||||
Now let's start with the SQLModel code.
|
||||
|
||||
We will start with the **simplest version**, with just heroes (no teams yet).
|
||||
|
||||
This is almost the same code we have seen up to now in previous examples:
|
||||
|
||||
```Python hl_lines="20-21"
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1]!}
|
||||
|
||||
# One line of FastAPI imports here later 👈
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:4]!}
|
||||
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:7-22]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
There's only one change here from the code we have used before, the `check_same_thread` in the `connect_args`.
|
||||
|
||||
That is a configuration that SQLAlchemy passes to the low-level library in charge of communicating with the database.
|
||||
|
||||
`check_same_thread` is by default set to `True`, to prevent misuses in some simple cases.
|
||||
|
||||
But here we will make sure we don't share the same **session** in more than one request, and that's the actual **safest way** to prevent any of the problems that configuration is there for.
|
||||
|
||||
And we also need to disable it because in **FastAPI** each request could be handled by multiple interacting threads.
|
||||
|
||||
!!! info
|
||||
That's enough information for now, you can read more about it in the <a href="https://fastapi.tiangolo.com/async/" class="external-link" target="_blank">FastAPI docs for `async` and `await`</a>.
|
||||
|
||||
The main point is, by ensuring you **don't share** the same **session** with more than one request, the code is already safe.
|
||||
|
||||
## **FastAPI** App
|
||||
|
||||
The next step is to create the **FastAPI** app.
|
||||
|
||||
We will import the `FastAPI` class from `fastapi`.
|
||||
|
||||
And then create an `app` object that is an instance of that `FastAPI` class:
|
||||
|
||||
```Python hl_lines="3 8"
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:1-4]!}
|
||||
|
||||
# SQLModel code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Create Database and Tables on `startup`
|
||||
|
||||
We want to make sure that once the app starts running, the function `create_tables` is called. To create the database and tables.
|
||||
|
||||
This should be called only once at startup, not before every request, so we put it in the function to handle the `"startup"` event:
|
||||
|
||||
```Python hl_lines="6-8"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-30]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Create Heroes *Path Operation*
|
||||
|
||||
!!! info
|
||||
If you need a refresher on what a **Path Operation** is (an endpoint with a specific HTTP Operation) and how to work with it in FastAPI, check out the <a href="https://fastapi.tiangolo.com/tutorial/first-steps/" class="external-link" target="_blank">FastAPI First Steps docs</a>.
|
||||
|
||||
Let's create the **path operation** code to create a new hero.
|
||||
|
||||
It will be called when a user sends a request with a `POST` **operation** to the `/heroes/` **path**:
|
||||
|
||||
```Python hl_lines="11-12"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-39]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
!!! info
|
||||
If you need a refresher on some of those concepts, checkout the FastAPI documentation:
|
||||
|
||||
* <a href="https://fastapi.tiangolo.com/tutorial/first-steps/" class="external-link" target="_blank">First Steps</a>
|
||||
* <a href="https://fastapi.tiangolo.com/tutorial/path-params/" class="external-link" target="_blank">Path Parameters - Data Validation and Data Conversion</a>
|
||||
* <a href="https://fastapi.tiangolo.com/tutorial/body/" class="external-link" target="_blank">Request Body</a>
|
||||
|
||||
## The **SQLModel** Advantage
|
||||
|
||||
Here's where having our **SQLModel** class models be both **SQLAlchemy** models and **Pydantic** models at the same tieme shine. ✨
|
||||
|
||||
Here we use the **same** class model to define the **request body** that will be received by our API.
|
||||
|
||||
Because **FastAPI** is based on Pydantic, it will use the same model (the Pydantic part) to do automatic data validation and <abbr title="also called serialization, marshalling">conversion</abbr> from the JSON request to an object that is an actual instance of the `Hero` class.
|
||||
|
||||
And then because this same **SQLModel** object is not only a **Pydantic** model instance but also a **SQLAlchemy** model instance, we can use it directly in a **session** to create the row in the database.
|
||||
|
||||
So we can use intuitive standard Python **type annotations**, and we don't have to duplicate a lot of the code for the database models and the API data models. 🎉
|
||||
|
||||
!!! tip
|
||||
We will improve this further later, but for now, it already shows the power of having **SQLModel** classes be both **SQLAlchemy** models and **Pydantic** models at the same time.
|
||||
|
||||
## Read Heroes *Path Operation*
|
||||
|
||||
Now let's add another **path operation** to read all the heroes:
|
||||
|
||||
```Python hl_lines="20-24"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py[ln:25-46]!}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/simple_hero_api/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
This is pretty straightforward.
|
||||
|
||||
When a client sends a request to the **path** `/heroes/` with a `GET` HTTP **operation**, we run this function that gets the heroes from the database and returns them.
|
||||
|
||||
## One Session per Request
|
||||
|
||||
Remember that we shoud use a SQLModel **session** per each group of operations and if we need other unrelated operations we should use a different session?
|
||||
|
||||
Here it is much more obvious.
|
||||
|
||||
We should normally have **one session per request** in most of the cases.
|
||||
|
||||
In some isolated cases we would want to have new sessions inside, so, **more than one session** per request.
|
||||
|
||||
But we would **never want to *share* the same session** among different requests.
|
||||
|
||||
In this simple example, we just create the new sessions manually in the **path operation functions**.
|
||||
|
||||
In future examples later we will use a <a href="https://fastapi.tiangolo.com/tutorial/dependencies/" class="external-link" target="_blank">FastAPI Dependency</a> to get the **session**, being able to share it with other dependencies and being able to replace it during testing. 🤓
|
||||
|
||||
## Run the **FastAPI** Application
|
||||
|
||||
Now we are ready to run the FastAPI application.
|
||||
|
||||
Put all that code in a file called `main.py`.
|
||||
|
||||
Then run it with **Uvicorn**:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app
|
||||
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
<span style="color: green;">INFO</span>: Started reloader process [28720]
|
||||
<span style="color: green;">INFO</span>: Started server process [28722]
|
||||
<span style="color: green;">INFO</span>: Waiting for application startup.
|
||||
<span style="color: green;">INFO</span>: Application startup complete.
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
!!! info
|
||||
The command `uvicorn main:app` refers to:
|
||||
|
||||
* `main`: the file `main.py` (the Python "module").
|
||||
* `app`: the object created inside of `main.py` with the line `app = FastAPI()`.
|
||||
|
||||
### Uvicorn `--reload`
|
||||
|
||||
During development (and only during development), you can also add the option `--reload` to Uvicorn.
|
||||
|
||||
It will restart the server every time you make a change to the code, this way you will be able to develop faster. 🤓
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ uvicorn main:app --reload
|
||||
|
||||
<span style="color: green;">INFO</span>: Will watch for changes in these directories: ['/home/user/code/sqlmodel-tutorial']
|
||||
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
<span style="color: green;">INFO</span>: Started reloader process [28720]
|
||||
<span style="color: green;">INFO</span>: Started server process [28722]
|
||||
<span style="color: green;">INFO</span>: Waiting for application startup.
|
||||
<span style="color: green;">INFO</span>: Application startup complete.
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
Just remember to never use `--reload` in production, as it consumes much more resources than necessary, would be more error prone, etc.
|
||||
|
||||
## Check the API docs UI
|
||||
|
||||
Now you can go to that URL in your browser `http://127.0.0.1:8000`. We didn't create a *path operation* for the root path `/`, so that URL alone will only show a "Not Found" error... that "Not Found" error is produced by your FastAPI application.
|
||||
|
||||
But you can go to the **automatically generated interactive API documentation** at the path `/docs`: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. ✨
|
||||
|
||||
You will see that this **automatic API docs <abbr title="user interface">UI</abbr>** has the *paths* that we defined above with their *operations*, and that it already knows the shape of the data that the **path operations** will receive:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/simple-hero-api/image01.png">
|
||||
|
||||
## Play with the API
|
||||
|
||||
You can actually click the button <kbd>Try it out</kbd> and send some requests to create some heroes with the **Create Hero** *path operation*.
|
||||
|
||||
And then you can get them back with the **Read Heroes** *path operation*:
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI reading heroes" src="/img/tutorial/fastapi/simple-hero-api/image02.png">
|
||||
|
||||
## Check the Database
|
||||
|
||||
Now you can terminate that Uvicorn server by going back to the terminal and pressing <kbd>Ctrl+C</kbd>.
|
||||
|
||||
And then you can open **DB Browser for SQLite** and check the database, to explore the data and confirm that it indeed saved the heroes. 🎉
|
||||
|
||||
<img class="shadow" alt="DB Browser for SQLite showing the heroes" src="/img/tutorial/fastapi/simple-hero-api/db-browser-01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
Good job! This is already a FastAPI **web API** application to interact with the heroes database. 🎉
|
||||
|
||||
There are several things we can improve and extend. For example, we want the database to decide the ID of each new hero, we don't want to allow a user to send it.
|
||||
|
||||
We will do all those improvements in the next chapters. 🚀
|
||||
123
docs/tutorial/fastapi/teams.md
Normal file
123
docs/tutorial/fastapi/teams.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# FastAPI Path Opeartions for Teams - Other Models
|
||||
|
||||
Let's now update the **FastAPI** application to handle data for teams.
|
||||
|
||||
This is very similar to the things we have done for heroes, so we will go over it quickly here.
|
||||
|
||||
We will use the same models we used in previous examples, with the **relationship attributes**, etc.
|
||||
|
||||
## Add Teams Models
|
||||
|
||||
Let's add the models for the teams.
|
||||
|
||||
It's the same process we did for heroes, with a base model, a **table model**, and some other **data models**.
|
||||
|
||||
We have a `TeamBase` **data model**, and from it we inherit with a `Team` **table model**.
|
||||
|
||||
Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamRead` **data models**.
|
||||
|
||||
And we also create a `TeamUpdate` **data model**.
|
||||
|
||||
```Python hl_lines="7-9 12-15 18-19 22-23 26-29"
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:1-29]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
We now also have **relationship attributes**. 🎉
|
||||
|
||||
Let's now update the `Hero` models too.
|
||||
|
||||
## Update Hero Models
|
||||
|
||||
```Python hl_lines="3-8 11-15 17-18 21-22 25-29"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:32-58]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
We now have a `team_id` in the hero models.
|
||||
|
||||
Notice that we can declare the `team_id` in the `HeroBase` because it can be reused by all the models, in all the cases it's an optional integer.
|
||||
|
||||
And even though the `HeroBase` is *not* a **table model**, we can declare `team_id` in it with the `foreign key` parameter. It won't do anything in most of the models that inherit from `HeroBase`, but in the **table model** `Hero` it will be used to tell **SQLModel** that this is a **foreign key** to that table.
|
||||
|
||||
## Relationship Attributes
|
||||
|
||||
Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them.
|
||||
|
||||
```Python hl_lines="11 39"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:7-58]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Path Operations for Teams
|
||||
|
||||
Let's now add the **path operations** for teams.
|
||||
|
||||
These are equivalent and very similar to the **path operations** for the **heroes** we had before, so we don't have to go over the details for each one, let's check the code.
|
||||
|
||||
```Python hl_lines="3-9 12-20 23-28 31-47 50-57"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py[ln:140-194]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/teams/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Using Relationships Attributes
|
||||
|
||||
Up to this point we are actually not using the **relationship attributes**, but we could access them in our code.
|
||||
|
||||
In the next chapter we will play more with them.
|
||||
|
||||
## Check the Docs UI
|
||||
|
||||
Now we can check the automatic docs UI to see all the **path operations** for heroes and teams.
|
||||
|
||||
<img class="shadow" alt="Interactive API docs UI" src="/img/tutorial/fastapi/teams/image01.png">
|
||||
|
||||
## Recap
|
||||
|
||||
We can use the same patterns to add more models and API **path operations** to our **FastAPI** application. 🎉
|
||||
417
docs/tutorial/fastapi/tests.md
Normal file
417
docs/tutorial/fastapi/tests.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# Test Applications with FastAPI and SQLModel
|
||||
|
||||
To finish this group of chapters about **FastAPI** with **SQLModel**, let's now learn how to implement automated tests for an application using FastAPI with SQLModel. ✅
|
||||
|
||||
Including the tips and tricks. 🎁
|
||||
|
||||
## FastAPI Application
|
||||
|
||||
Let's work with one of the **simpler** FastAPI applications we built in the previous chapters.
|
||||
|
||||
All the same **concepts**, **tips** and **tricks** will apply to more complex applications as well.
|
||||
|
||||
We will use the application with the hero models, but without team models, and we will use the dependency to get a **session**.
|
||||
|
||||
Now we will see how useful it is to have this session dependency. ✨
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/main.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## File Structure
|
||||
|
||||
Now we will have a Python project with multiple files, one file `main.py` with all the application, and one file `test_main.py` with the tests, with the same ideas from [Code Structure and Multiple Files](../code-structure.md){.internal-link target=_blank}.
|
||||
|
||||
The file structure is:
|
||||
|
||||
```
|
||||
.
|
||||
├── project
|
||||
├── __init__.py
|
||||
├── main.py
|
||||
└── test_main.py
|
||||
```
|
||||
|
||||
## Testing FastAPI Applications
|
||||
|
||||
If you haven't done testing in FastAPI applications, first check the <a href="https://fastapi.tiangolo.com/tutorial/testing/" class="external-link" target="_blank">FastAPI docs about Testing</a>.
|
||||
|
||||
Then, we can continue here, the first step is to install the dependencies, `requests` and `pytest`.
|
||||
|
||||
Make sure you do it in the same [Python environment](../index.md#create-a-python-virtual-environment){.internal-link target=_blank}.
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python -m pip install requests pytest
|
||||
|
||||
---> 100%
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Basic Tests Code
|
||||
|
||||
Let's start with a simple test, with just the basic test code we need the check that the **FastAPI** application is creating a new hero correctly.
|
||||
|
||||
```{ .python .annotate }
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:1-7]!}
|
||||
# Some code here omitted, we will see it later 👈
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:20-24]!}
|
||||
# Some code here omitted, we will see it later 👈
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_001.py[ln:26-32]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md!}
|
||||
|
||||
!!! tip
|
||||
Check out the number bubbles to see what is done by each line of code.
|
||||
|
||||
That's the **core** of the code we need for all the tests later.
|
||||
|
||||
But now we need to deal with a bit of logistics and details we are not paying attention to just yet. 🤓
|
||||
|
||||
## Testing Database
|
||||
|
||||
This test looks fine, but there's a problem.
|
||||
|
||||
If we run it, it will use the same **production database** that we are using to store our very important **heroes**, and we will end up adding adding unnecesary data to it, or even worse, in future tests we could end up removing production data.
|
||||
|
||||
So, we should use an independent **testing database**, just for the tests.
|
||||
|
||||
To do this, we need to change the URL used for the database.
|
||||
|
||||
But when the code for the API is executed, it gets a **session** that is already connected to an **engine**, and the **engine** is already using a specific database URL.
|
||||
|
||||
Even if we import the variable from the `main` module and change its value just for the tests, by that point the **engine** is already created with the original value.
|
||||
|
||||
But all our API *path operations* get the *session* using a FastAPI **dependency**, and we can override dependencies in tests.
|
||||
|
||||
Here's where dependencies start to help a lot.
|
||||
|
||||
## Override a Dependency
|
||||
|
||||
Let's override the `get_session()` dependency for the tests.
|
||||
|
||||
This dependency is used by all the *path operations* to get the **SQLModel** session object.
|
||||
|
||||
We will override it to use a different **session** object just for the tests.
|
||||
|
||||
That way we protect the production database and we have better control of the data we are testing.
|
||||
|
||||
```{ .python .annotate hl_lines="4 9-10 12 19" }
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py[ln:1-7]!}
|
||||
# Some code here omitted, we will see it later 👈
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_002.py[ln:15-32]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md!}
|
||||
|
||||
!!! tip
|
||||
Check out the number bubbles to see what is done by each line of code.
|
||||
|
||||
## Create the Engine and Session for Testing
|
||||
|
||||
Now let's create that **session** object that will be used during testing.
|
||||
|
||||
It will use its own **engine**, and this new engine will use a new URL for the testing database:
|
||||
|
||||
```
|
||||
sqlite:///testing.db
|
||||
```
|
||||
|
||||
So, the testing database will be in the file `testing.db`.
|
||||
|
||||
``` { .python .annotate hl_lines="4 8-11 13 16 33"}
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_003.py!}
|
||||
```
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md!}
|
||||
|
||||
### Import Table Models
|
||||
|
||||
Here we create all the tables in the testing database with:
|
||||
|
||||
```Python
|
||||
SQLModel.metadata.create_all(engine)
|
||||
```
|
||||
|
||||
But remember that [Order Matters](../create-db-and-table.md#sqlmodel-metadata-order-matters){.internal-link target=_blank} and we need to make sure all the **SQLModel** models are already defined and **imported** before calling `.create_all()`.
|
||||
|
||||
In this case, it all works for a little subtlety that deserves some attention.
|
||||
|
||||
Because we import something, *anything*, from `.main`, the code in `.main` will be executed, including the definition of the **table models**, and that will automatically register them in `SQLModel.metadata`.
|
||||
|
||||
That way, when we call `.create_all()` all the **table models** are correctly registered in `SQLModel.metadata` and it will all work. 👌
|
||||
|
||||
## Memory Database
|
||||
|
||||
Now we are not using the production database, instead we use a **new testing database** with the `testing.db` file, which is great.
|
||||
|
||||
But SQLite also supports having an **in memory** database. This means that all the database is only in memory, and it is never saved in a file on disk.
|
||||
|
||||
After the program terminates, **the in-memory database is deleted**, so it wouldn't help much for a production database.
|
||||
|
||||
But **it works great for testing**, because it can be quickly created before each test, and quickly removed after each test. ✅
|
||||
|
||||
And also, because it never has to write anything to a file and it's all just in memory, it will be even faster than normally. 🏎
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Other alternatives and ideas 👀
|
||||
</summary>
|
||||
Before arriving at the idea of using an **in-memory database** we could have explored other alternatives and ideas.
|
||||
|
||||
The first, is that we are not deleting the file after we finish the test, so, the next test could have **leftover data**. So, the right thing would be to delete the file right after finishing the test. 🔥
|
||||
|
||||
But if each test has to create a new file and then delete it afterwards, running all the tests could be **a bit slow**.
|
||||
|
||||
Right now, we have a file `testing.db` that is used by all the tests (we only have one test now, but we will have more).
|
||||
|
||||
So, if we tried to run the tests at the same time **in parallel** to try to speed things up a bit, they would clash trying to use the *same* `testing.db` file.
|
||||
|
||||
Of couse, we could also fix that, using some **random name** for each testing database file... but in the case of SQLite, we have an even better alternative with just using an **in-memory database**. ✨
|
||||
|
||||
</details>
|
||||
|
||||
## Configure the In-Memory Database
|
||||
|
||||
Let's update our code to use the in-memory database.
|
||||
|
||||
We just have to change a couple of parameters in the **engine**.
|
||||
|
||||
```{ .python .annotate hl_lines="3 9-13"}
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_004.py[ln:1-13]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md!}
|
||||
|
||||
!!! tip
|
||||
Check out the number bubbles to see what is done by each line of code.
|
||||
|
||||
That's it, now the test will run using the **in-memory database**, which will be faster and probably safer.
|
||||
|
||||
And all the other tests can do the same.
|
||||
|
||||
## Boilerplate Code
|
||||
|
||||
Great, that works, and you could replicate all that process in each of the test functions.
|
||||
|
||||
But we had to add a lot of **boilerplate code** to handle the custom database, creating it in memory, the custom session, the dependency override.
|
||||
|
||||
Do we really have to duplicate all that for **each test**? No, we can do better! 😎
|
||||
|
||||
We are using **pytest** to run the tests. And pytest also has a very similar concept to the **dependencies in FastAPI**.
|
||||
|
||||
!!! info
|
||||
In fact, pytest was one of the things that inspired the design of the dependencies in FastAPI.
|
||||
|
||||
It's a way for us to declare some **code that should be run before** each test and **provide a value** for the test function (that's pretty much the same as FastAPI dependencies).
|
||||
|
||||
In fact, it also has the same trick of allowing to use `yield` instead of `return` to provide the value, and then **pytest** makes sure that the code after `yield` is executed *after* the function with the test is done.
|
||||
|
||||
In pytest, these things are called **fixtures** instead of *dependencies*.
|
||||
|
||||
Let's use these **fixtures** to improve our code and reduce de duplicated boilerplate for the next tests.
|
||||
|
||||
## Pytest Fixtures
|
||||
|
||||
You can read more about them in the <a href="https://docs.pytest.org/en/6.2.x/fixture.html" class="external-link" target="_blank">pytest docs for fixtures</a>, but I'll give you a short example for what we need here.
|
||||
|
||||
Let's see the first code example with a fixture:
|
||||
|
||||
``` { .python .annotate }
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_005.py!}
|
||||
```
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md!}
|
||||
|
||||
!!! tip
|
||||
Check out the number bubbles to see what is done by each line of code.
|
||||
|
||||
**pytest** fixtures work in a very similar way to FastAPI dependencies, but have some minor differences:
|
||||
|
||||
* In pytest fixtures we need to add a decorator of `@pytest.fixture()` on top.
|
||||
* To use a pytest fixture in a function, we have to declare the parameter with the **exact same name**. In FastAPI we have to **explicitly use `Depends()`** with the actual function inside it.
|
||||
|
||||
But apart from the way we declare them and how we tell the framework that we want to have them in the function, they **work in a very similar way**.
|
||||
|
||||
Now we create lot's of tests, and re-use that same fixture in all of them, saving us that **boilerplate code**.
|
||||
|
||||
**pytest** will make sure to run them right before (and finish them right after) each test function. So, each test function will actually have its own database, engine, and session.
|
||||
|
||||
## Client Fixture
|
||||
|
||||
Awesome, that fixture helps us prevent a lot of duplicated code.
|
||||
|
||||
But currently we still have to write some code in the test function that will be repetitive for other tests, right now we:
|
||||
|
||||
* create the **dependency override**
|
||||
* put it in the `app.dependency_overrides`
|
||||
* create the `TestClient`
|
||||
* Clear the dependency override(s) after making the request
|
||||
|
||||
That's still gonna be repetitive in the other future tests. Can we improve it? Yes! 🎉
|
||||
|
||||
Each **pytest** fixture (the same way as **FastAPI** dependencies), can require other fixtures.
|
||||
|
||||
So, we can create a **client fixture** that will be used in all the tests, and it will itself require the **session fixture**.
|
||||
|
||||
``` { .python .annotate hl_lines="19-28 31" }
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main_006.py!}
|
||||
```
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md!}
|
||||
|
||||
!!! tip
|
||||
Check out the number bubbles to see what is done by each line of code.
|
||||
|
||||
Now we have a **client fixture** that in turns uses the **session fixture**.
|
||||
|
||||
And in the actual test function, we just have to declare that we require this **client fixture**.
|
||||
|
||||
## Add More Tests
|
||||
|
||||
At this point, it all might seem like we just did a lot of changes for nothing, to get **the same result**. 🤔
|
||||
|
||||
But normally we will create **lots of other test functions**. And now all the boilerplate and complexity is **writen only once**, in those two fixtures.
|
||||
|
||||
Let's add some more tests:
|
||||
|
||||
```Python hl_lines="3 22"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:30-58]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
!!! tip
|
||||
It's always **good idea** to not only test the normal case, but also that **invalid data**, **errors**, and **corner cases** are handled correctly.
|
||||
|
||||
That's why we add these two extra tests here.
|
||||
|
||||
Now, any additional test functions can be as **simple** as the first one, they just have to **declate the `client` parameter** to get the `TestClient` **fixture** with all the database stuff setup. Nice! 😎
|
||||
|
||||
## Why Two Fixtures
|
||||
|
||||
Now, seeing the code we could think, why do we put **two fixtures** instead of **just one** with all the code? And that makes total sense!
|
||||
|
||||
For these examples, **that would have been simpler**, there's no need to separate that code in two fixtures for them...
|
||||
|
||||
But for the next test function, we will require **both fixtures**, the **client** and the **session**.
|
||||
|
||||
```Python hl_lines="6 10"
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:1-6]!}
|
||||
|
||||
# Code here omitted 👈
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:61-81]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
In this test function we want to check that the *path operation* to **read a list of heroes** actually sends us heroes.
|
||||
|
||||
But if the **database is empty**, we would get an **empty list**, and we wouldn't know if the hero data is being sent correctly or not.
|
||||
|
||||
But we can **create some heroes** in the testing database right before sending the API request. ✨
|
||||
|
||||
And because we are using the **testing database**, we don't affect anything by creating heroes for the test.
|
||||
|
||||
To do it, we have to:
|
||||
|
||||
* import the `Hero` model
|
||||
* require both fixtures, the **client** and the **session**
|
||||
* create some heroes and save them in the database using the **session**
|
||||
|
||||
After that, we can send the request and check that we actually got the data back correctly from the database. 💯
|
||||
|
||||
Here's the important detail to notice: we can require fixtures in other fixtures **and also** in the test functions.
|
||||
|
||||
The function for the **client fixture** and the actual testing function will **both** receive the same **session**.
|
||||
|
||||
## Add the Rest of the Tests
|
||||
|
||||
Using the same ideas, requiring the fixtures, creating data that we need for the tests, etc. we can now add the rest of the tests, they look quite similar to what we have done up to now.
|
||||
|
||||
```Python hl_lines="3 18 33"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py[ln:84-125]!}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/app_testing/tutorial001/test_main.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Run the Tests
|
||||
|
||||
Now we can run the tests with `pytest` and see the results:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ pytest
|
||||
|
||||
============= test session starts ==============
|
||||
platform linux -- Python 3.7.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
|
||||
rootdir: /home/user/code/sqlmodel-tutorial
|
||||
<b>collected 7 items </b>
|
||||
|
||||
---> 100%
|
||||
|
||||
project/test_main.py <font color="#A6E22E">....... [100%]</font>
|
||||
|
||||
<font color="#A6E22E">============== </font><font color="#A6E22E"><b>7 passed</b></font><font color="#A6E22E"> in 0.83s ===============</font>
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Recap
|
||||
|
||||
Did you read all that? Wow, I'm impressed! 😎
|
||||
|
||||
Adding tests to your application will give you a lot of **certainty** that everything is **working correctly**, as you indended.
|
||||
|
||||
And tests will be notoriously useful when **refactoring** your code, **changing things**, **adding features**. Because tests they can help catch a lot of errors that can be easily introduced by refactoring.
|
||||
|
||||
And they will give you the confidence to work faster and **more efficiently**, because you know that you are checking if you are **not breaking anything**. 😅
|
||||
|
||||
I think tests are one of those things that bring your code and you as a developer to the next professional level. 😎
|
||||
|
||||
And if you read and studied all this, you already know a lot of the advanced ideas and tricks that took me years to learn. 🚀
|
||||
231
docs/tutorial/fastapi/update.md
Normal file
231
docs/tutorial/fastapi/update.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Update Data with FastAPI
|
||||
|
||||
Now let's see how to update data in the database with a **FastAPI** *path operation*.
|
||||
|
||||
## `HeroUpdate` Model
|
||||
|
||||
We want clients to be able to udpate the `name`, the `secret_name`, and the `age` of a hero.
|
||||
|
||||
But we don't want them to have to include all the data again just to **update a single field**.
|
||||
|
||||
So, we need to have all those fields **marked as optional**.
|
||||
|
||||
And because the `HeroBase` has some of them as *required* and not optional, we will need to **create a new model**.
|
||||
|
||||
!!! tip
|
||||
Here is one of those cases where it probably makes sense to use an **independent model** instead of trying to come up with a complex tree of models inheriting from each other.
|
||||
|
||||
Because each field is **actually different** (we just change it to `Optional`, but that's already making it different), it makes sense to have them in their own model.
|
||||
|
||||
So, let's create this new `HeroUpdate` model:
|
||||
|
||||
```Python hl_lines="21-24"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:7-28]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
This is almost the same as `HeroBase`, but all the fields are optional, so we can't simply inherit from `HeroBase`.
|
||||
|
||||
## Create the Update Path Operation
|
||||
|
||||
Now let's use this model in the *path operation* to update a hero.
|
||||
|
||||
We will use a `PATCH` HTTP operation. This is used to **partially update data**, which is what we are doing.
|
||||
|
||||
```Python hl_lines="3-4"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
We also read the `hero_id` from the *path parameter* an the request body, a `HeroUpdate`.
|
||||
|
||||
### Read the Existing Hero
|
||||
|
||||
We take a `hero_id` with the **ID** of the hero **we want to update**.
|
||||
|
||||
So, we need to read the hero from the database, with the **same logic** we used to **read a single hero**, checking if it exists, possibly raising an error for the client if it doesn't exist, etc.
|
||||
|
||||
```Python hl_lines="6-8"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Get the New Data
|
||||
|
||||
The `HeroUpdate` model has all the fields with **default values**, because they all have defaults, they are all optional, which is what we want.
|
||||
|
||||
But that also means that if we just call `hero.dict()` we will get a dictionary that could potentially have several or all of those values with their defaults, for example:
|
||||
|
||||
```Python
|
||||
{
|
||||
"name": None,
|
||||
"secret_name": None,
|
||||
"age": None,
|
||||
}
|
||||
```
|
||||
|
||||
And then if we update the hero in the database with this data, we would be removing any existing values, and that's probably **not what the client intended**.
|
||||
|
||||
But fortunately Pydantic models (and so SQLModel models) have a parameter we can pass to the `.dict()` method for that: `exclude_unset=True`.
|
||||
|
||||
This tells Pydantic to **not include** the values that were **not sent** by the client. Saying it another way, it would **only** include the values that were **sent by the client**.
|
||||
|
||||
So, if the client sent a JSON with no values:
|
||||
|
||||
```JSON
|
||||
{}
|
||||
```
|
||||
|
||||
Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be:
|
||||
|
||||
```Python
|
||||
{}
|
||||
```
|
||||
|
||||
But if the client sent a JSON with:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"name": "Deadpuddle"
|
||||
}
|
||||
```
|
||||
|
||||
Then the dictionary we would get in Python using `hero.dict(exclude_unset=True)` would be:
|
||||
|
||||
```Python
|
||||
{
|
||||
"name": "Deadpuddle"
|
||||
}
|
||||
```
|
||||
|
||||
Then we use that to get the data that was actually sent by the client:
|
||||
|
||||
```Python hl_lines="9"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Update the Hero in the Database
|
||||
|
||||
Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`.
|
||||
|
||||
```Python hl_lines="10-11"
|
||||
# Code above omitted 👆
|
||||
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!}
|
||||
|
||||
# Code below omitted 👇
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>👀 Full file preview</summary>
|
||||
|
||||
```Python
|
||||
{!./docs_src/tutorial/fastapi/update/tutorial001.py!}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
If you are not familiar with that `setattr()`, it takes an object, like the `db_hero`, then an attribute name (`key`), that in our case could be `"name"`, and a value (`value`). And then it **sets the attribute with that name to the value**.
|
||||
|
||||
So, if `key` was `"name"` and `value` was `"Deadpuddle"`, then this code:
|
||||
|
||||
```Python
|
||||
setattr(db_hero, key, value)
|
||||
```
|
||||
|
||||
...would be more or less equivalent to:
|
||||
|
||||
```Python
|
||||
db_hero.name = "Deadpuddle"
|
||||
```
|
||||
|
||||
## Remove Fields
|
||||
|
||||
Here's a bonus. 🎁
|
||||
|
||||
When getting the dictionary of data sent by the client, we only include **what the client actually sent**.
|
||||
|
||||
This sounds simple, but it has some additional nuances that become **nice features**. ✨
|
||||
|
||||
We are **not simply omitting** the data that has the **default values**.
|
||||
|
||||
And we are **not simply omitting** anything that is `None`.
|
||||
|
||||
This means that, if a model in the database **has a value different than the default**, the client could **reset it to the same value as the default**, or even `None`, and we would **still notice it** and **update it accordingly**. 🤯🚀
|
||||
|
||||
So, if the client wanted to intentionally remove the `age` of a hero, they could just send a JSON with:
|
||||
|
||||
```JSON
|
||||
{
|
||||
"age": null
|
||||
}
|
||||
```
|
||||
|
||||
And when getting the data with `hero.dict(exclude_unset=True)`, we would get:
|
||||
|
||||
```Python
|
||||
{
|
||||
"age": None
|
||||
}
|
||||
```
|
||||
|
||||
So, we would use that value and upate the `age` to `None` in the database, **just as the client intended**.
|
||||
|
||||
Notice that `age` here is `None`, and **we still detected it**.
|
||||
|
||||
Also that `name` was not even sent, and we don't *accidentaly* set it to `None` or something, we just didn't touch it, because the client didn't sent it, so we are **pefectly fine**, even in these corner cases. ✨
|
||||
|
||||
These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉
|
||||
|
||||
## Recap
|
||||
|
||||
Using `.dict(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎
|
||||
Reference in New Issue
Block a user