|
| 1 | +# Update with Extra Data (Hashed Passwords) with FastAPI |
| 2 | + |
| 3 | +In the previous chapter I explained to you how to update data in the database from input data coming from a **FastAPI** *path operation*. |
| 4 | + |
| 5 | +Now I'll explain to you how to add **extra data**, additional to the input data, when updating or creating a model object. |
| 6 | + |
| 7 | +This is particularly useful when you need to **generate some data** in your code that is **not coming from the client**, but you need to store it in the database. For example, to store a **hashed password**. |
| 8 | + |
| 9 | +## Password Hashing |
| 10 | + |
| 11 | +Let's imagine that each hero in our system also has a **password**. |
| 12 | + |
| 13 | +We should never store the password in plain text in the database, we should only stored a **hashed version** of it. |
| 14 | + |
| 15 | +"**Hashing**" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish. |
| 16 | + |
| 17 | +Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish. |
| 18 | + |
| 19 | +But you **cannot convert** from the gibberish **back to the password**. |
| 20 | + |
| 21 | +### Why use Password Hashing |
| 22 | + |
| 23 | +If your database is stolen, the thief won't have your users' **plaintext passwords**, only the hashes. |
| 24 | + |
| 25 | +So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous). |
| 26 | + |
| 27 | +/// tip |
| 28 | + |
| 29 | +You could use <a href="https://passlib.readthedocs.io/en/stable/" class="external-link" target="_blank">passlib</a> to hash passwords. |
| 30 | + |
| 31 | +In this example we will use a fake hashing function to focus on the data changes. 🤡 |
| 32 | + |
| 33 | +/// |
| 34 | + |
| 35 | +## Update Models with Extra Data |
| 36 | + |
| 37 | +The `Hero` table model will now store a new field `hashed_password`. |
| 38 | + |
| 39 | +And the data models for `HeroCreate` and `HeroUpdate` will also have a new field `password` that will contain the plain text password sent by clients. |
| 40 | + |
| 41 | +```Python hl_lines="11 15 26" |
| 42 | +# Code above omitted 👆 |
| 43 | + |
| 44 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:7-30]!} |
| 45 | + |
| 46 | +# Code below omitted 👇 |
| 47 | +``` |
| 48 | + |
| 49 | +/// details | 👀 Full file preview |
| 50 | + |
| 51 | +```Python |
| 52 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} |
| 53 | +``` |
| 54 | + |
| 55 | +/// |
| 56 | + |
| 57 | +When a client is creating a new hero, they will send the `password` in the request body. |
| 58 | + |
| 59 | +And when they are updating a hero, they could also send the `password` in the request body to update it. |
| 60 | + |
| 61 | +## Hash the Password |
| 62 | + |
| 63 | +The app will receive the data from the client using the `HeroCreate` model. |
| 64 | + |
| 65 | +This contains the `password` field with the plain text password, and we cannot use that one. So we need to generate a hash from it. |
| 66 | + |
| 67 | +```Python hl_lines="11" |
| 68 | +# Code above omitted 👆 |
| 69 | + |
| 70 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:44-46]!} |
| 71 | + |
| 72 | +# Code here omitted 👈 |
| 73 | + |
| 74 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-59]!} |
| 75 | + |
| 76 | +# Code below omitted 👇 |
| 77 | +``` |
| 78 | + |
| 79 | +/// details | 👀 Full file preview |
| 80 | + |
| 81 | +```Python |
| 82 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} |
| 83 | +``` |
| 84 | + |
| 85 | +/// |
| 86 | + |
| 87 | +## Create an Object with Extra Data |
| 88 | + |
| 89 | +Now we need to create the database hero. |
| 90 | + |
| 91 | +In previous examples, we have used something like: |
| 92 | + |
| 93 | +```Python |
| 94 | +db_hero = Hero.model_validate(hero) |
| 95 | +``` |
| 96 | + |
| 97 | +This creates a `Hero` (which is a *table model*) object from the `HeroCreate` (which is a *data model*) object that we received in the request. |
| 98 | + |
| 99 | +And this is all good... but as `Hero` doesn't have a field `password`, it won't be extracted from the object `HeroCreate` that has it. |
| 100 | + |
| 101 | +`Hero` actually has a `hashed_password`, but we are not providing it. We need a way to provide it... |
| 102 | + |
| 103 | +### Dictionary Update |
| 104 | + |
| 105 | +Let's pause for a second to check this, when working with dictionaries, there's a way to `update` a dictionary with extra data from another dictionary, something like this: |
| 106 | + |
| 107 | +```Python hl_lines="14" |
| 108 | +db_user_dict = { |
| 109 | + "name": "Deadpond", |
| 110 | + "secret_name": "Dive Wilson", |
| 111 | + "age": None, |
| 112 | +} |
| 113 | + |
| 114 | +hashed_password = "fakehashedpassword" |
| 115 | + |
| 116 | +extra_data = { |
| 117 | + "hashed_password": hashed_password, |
| 118 | + "age": 32, |
| 119 | +} |
| 120 | + |
| 121 | +db_user_dict.update(extra_data) |
| 122 | + |
| 123 | +print(db_user_dict) |
| 124 | + |
| 125 | +# { |
| 126 | +# "name": "Deadpond", |
| 127 | +# "secret_name": "Dive Wilson", |
| 128 | +# "age": 32, |
| 129 | +# "hashed_password": "fakehashedpassword", |
| 130 | +# } |
| 131 | +``` |
| 132 | + |
| 133 | +This `update` method allows us to add and override things in the original dictionary with the data from another dictionary. |
| 134 | + |
| 135 | +So now, `db_user_dict` has the updated `age` field with `32` instead of `None` and more importantly, **it has the new `hashed_password` field**. |
| 136 | + |
| 137 | +### Create a Model Object with Extra Data |
| 138 | + |
| 139 | +Similar to how dictionaries have an `update` method, **SQLModel** models have a parameter `update` in `Hero.model_validate()` that takes a dictionary with extra data, or data that should take precedence: |
| 140 | + |
| 141 | +```Python hl_lines="8" |
| 142 | +# Code above omitted 👆 |
| 143 | + |
| 144 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-66]!} |
| 145 | + |
| 146 | +# Code below omitted 👇 |
| 147 | +``` |
| 148 | + |
| 149 | +/// details | 👀 Full file preview |
| 150 | + |
| 151 | +```Python |
| 152 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} |
| 153 | +``` |
| 154 | + |
| 155 | +/// |
| 156 | + |
| 157 | +Now, `db_hero` (which is a *table model* `Hero`) will extract its values from `hero` (which is a *data model* `HeroCreate`), and then it will **`update`** its values with the extra data from the dictionary `extra_data`. |
| 158 | + |
| 159 | +It will only take the fields defined in `Hero`, so **it will not take the `password`** from `HeroCreate`. And it will also **take its values** from the **dictionary passed to the `update`** parameter, in this case, the `hashed_password`. |
| 160 | + |
| 161 | +If there's a field in both `hero` and the `extra_data`, **the value from the `extra_data` passed to `update` will take precedence**. |
| 162 | + |
| 163 | +## Update with Extra Data |
| 164 | + |
| 165 | +Now let's say we want to **update a hero** that already exists in the database. |
| 166 | + |
| 167 | +The same way as before, to avoid removing existing data, we will use `exclude_unset=True` when calling `hero.model_dump()`, to get a dictionary with only the data sent by the client. |
| 168 | + |
| 169 | +```Python hl_lines="9" |
| 170 | +# Code above omitted 👆 |
| 171 | + |
| 172 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-91]!} |
| 173 | + |
| 174 | +# Code below omitted 👇 |
| 175 | +``` |
| 176 | + |
| 177 | +/// details | 👀 Full file preview |
| 178 | + |
| 179 | +```Python |
| 180 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} |
| 181 | +``` |
| 182 | + |
| 183 | +/// |
| 184 | + |
| 185 | +Now, this `hero_data` dictionary could contain a `password`. We need to check it, and if it's there, we need to generate the `hashed_password`. |
| 186 | + |
| 187 | +Then we can put that `hashed_password` in a dictionary. |
| 188 | + |
| 189 | +And then we can update the `db_hero` object using the method `db_hero.sqlmodel_update()`. |
| 190 | + |
| 191 | +It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data. |
| 192 | + |
| 193 | +```Python hl_lines="15" |
| 194 | +# Code above omitted 👆 |
| 195 | + |
| 196 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-101]!} |
| 197 | + |
| 198 | +# Code below omitted 👇 |
| 199 | +``` |
| 200 | + |
| 201 | +/// details | 👀 Full file preview |
| 202 | + |
| 203 | +```Python |
| 204 | +{!./docs_src/tutorial/fastapi/update/tutorial002.py!} |
| 205 | +``` |
| 206 | + |
| 207 | +/// |
| 208 | + |
| 209 | +/// tip |
| 210 | + |
| 211 | +The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 😎 |
| 212 | + |
| 213 | +/// |
| 214 | + |
| 215 | +## Recap |
| 216 | + |
| 217 | +You can use the `update` parameter in `Hero.model_validate()` to provide extra data when creating a new object and `Hero.sqlmodel_update()` to provide extra data when updating an existing object. 🤓 |
0 commit comments