1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
|
# 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 update 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 make all those fields **optional**.
And because the `HeroBase` has some of them *required* (without a default value), 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 set a default value of `None`, 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:
{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[5:26] hl[23:26] *}
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.
{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[74:75] *}
We also read the `hero_id` from the *path parameter* and 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.
{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[77:79] *}
### 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.model_dump()` 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 `.model_dump()` 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.model_dump(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.model_dump(exclude_unset=True)` would be:
```Python
{
"name": "Deadpuddle"
}
```
Then we use that to get the data that was actually sent by the client:
{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[80] *}
/// tip
Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2.
///
## Update the Hero in the Database
Now that we have a **dictionary with the data sent by the client**, we can use the method `db_hero.sqlmodel_update()` to update the object `db_hero`.
{* ./docs_src/tutorial/fastapi/update/tutorial001_py310.py ln[74:89] hl[81] *}
/// tip
The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 🤓
Before that, you would need to manually get the values and set them using `setattr()`.
///
The method `db_hero.sqlmodel_update()` takes an argument with another model object or a dictionary.
For each of the fields in the **original** model object (`db_hero` in this example), it checks if the field is available in the **argument** (`hero_data` in this example) and then updates it with the provided value.
## 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.model_dump(exclude_unset=True)`, we would get:
```Python
{
"age": None
}
```
So, we would use that value and update 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 *accidentally* set it to `None` or something. We just didn't touch it because the client didn't send it, so we are **perfectly fine**, even in these corner cases. ✨
These are some of the advantages of Pydantic, that we can use with SQLModel. 🎉
## Recap
Using `.model_dump(exclude_unset=True)` in SQLModel models (and Pydantic models) we can easily update data **correctly**, even in the **edge cases**. 😎
|