📝 Add docs annotations for source examples

This commit is contained in:
Sebastián Ramírez 2021-08-24 14:51:14 +02:00
parent e6308146f7
commit dc331f649c
13 changed files with 1074 additions and 0 deletions

View File

@ -0,0 +1,387 @@
1. Create the `hero_1`.
**Doesn't generate any output**.
2. Create the `hero_2`.
**Doesn't generate any output**.
3. Create the `hero_3`.
**Doesn't generate any output**.
4. Print the line `"Before interacting with the database"`.
Generates the output:
```
Before interacting with the database
```
5. Print the `hero_1` before interacting with the database.
Generates the output:
```
Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None
```
6. Print the `hero_2` before interacting with the database.
Generates the output:
```
Hero 2: id=None name='Spider-Boy' secret_name='Pedro Parqueador' age=None
```
7. Print the `hero_3` before interacting with the database.
Generates the output:
```
Hero 3: id=None name='Rusty-Man' secret_name='Tommy Sharp' age=48
```
8. Create the `Session` in a `with` block.
**Doesn't generate any output**.
9. Add the `hero_1` to the session.
This still doesn't save it to the database.
**Doesn't generate any output**.
10. Add the `hero_2` to the session.
This still doesn't save it to the database.
**Doesn't generate any output**.
11. Add the `hero_3` to the session.
This still doesn't save it to the database.
**Doesn't generate any output**.
12. Print the line `"After adding to the session"`.
Generates the output:
```
After adding to the session
```
13. Print the `hero_1` after adding it to the session.
It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`.
Generates the output:
```
Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None
```
14. Print the `hero_2` after adding it to the session.
It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`.
Generates the output:
```
Hero 2: id=None name='Spider-Boy' secret_name='Pedro Parqueador' age=None
```
15. Print the `hero_3` after adding it to the session.
It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`.
Generates the output:
```
Hero 3: id=None name='Rusty-Man' secret_name='Tommy Sharp' age=48
```
16. `commit` the **session**.
This will **save** all the data to the database. The **session** will use the **engine** to run a lot of SQL.
Generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [generated in 0.00018s] ('Deadpond', 'Dive Wilson', None)
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [cached since 0.0008968s ago] ('Spider-Boy', 'Pedro Parqueador', None)
INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)
INFO Engine [cached since 0.001143s ago] ('Rusty-Man', 'Tommy Sharp', 48)
INFO Engine COMMIT
```
17. Print the line `"After committing the session"`.
Generates the output:
```
After committing the session
```
18. Print the `hero_1` after committing the session.
The `hero_1` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data.
Generates the output:
```
Hero 1:
```
19. Print the `hero_2` after committing the session.
The `hero_2` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data.
Generates the output:
```
Hero 2:
```
20. Print the `hero_3` after committing the session.
The `hero_3` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data.
Generates the output:
```
Hero 3:
```
21. Print the line `"After commiting the session, show IDs"`.
Generates the output:
```
After committing the session, show IDs
```
22. Print the `hero_1.id`. A lot happens here.
Because we are accessing the attribute `id` of `hero_1`, **SQLModel** (actually SQLAlchemy) can detect that we are trying to access data from the `hero_1`.
It then detects that `hero_1` is currently associated with a **session** (because we added it to the session and committed it), and it is marked as expired.
Then with the **session**, it uses the **engine** to execute all the SQL to fetch the data for this object from the database.
Next it updates the object with the new data and marks it internally as "fresh" or "not expired".
Finally, it makes the ID value available for the rest of the Python expression. In this case, the Python expression just prints the ID.
Generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00017s] (1,)
Hero 1 ID: 1
```
23. Print the `hero_2.id`.
A lot happens here, all the same stuff that happened at point 22, but for this `hero_2` object.
Generates the output:
```
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.001245s ago] (2,)
Hero 2 ID: 2
```
24. Print the `hero_3.id`.
A lot happens here, all the same stuff that happened at point 22, but for this `hero_3` object.
Generates the output:
```
INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.002215s ago] (3,)
Hero 3 ID: 3
```
25. Print the line `"After committing the session, show names"`.
Generates the output:
```
After committing the session, show names
```
26. Print the `hero_1.name`.
Because `hero_1` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available.
Generates the output:
```
Hero 1 name: Deadpond
```
27. Print the `hero_2.name`.
Because `hero_2` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available.
Generates the output:
```
Hero 2 name: Spider-Boy
```
28. Print the `hero_3.name`.
Because `hero_3` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available.
Generates the output:
```
Hero 3 name: Rusty-Man
```
29. Explicitly refresh the `hero_1` object.
The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_1` object.
Generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00024s] (1,)
```
30. Explicitly refresh the `hero_2` object.
The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_2` object.
Generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.001487s ago] (2,)
```
31. Explicitly refresh the `hero_3` object.
The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_3` object.
Generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.002377s ago] (3,)
```
32. Print the line `"After refreshing the heroes"`.
Generates the output:
```
After refreshing the heroes
```
33. Print the `hero_1`.
!!! info
Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute.
Because the `hero_1` is fresh it has all it's data available.
Generates the output:
```
Hero 1: age=None id=1 name='Deadpond' secret_name='Dive Wilson'
```
34. Print the `hero_2`.
!!! info
Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute.
Because the `hero_2` is fresh it has all it's data available.
Generates the output:
```
Hero 2: age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador'
```
35. Print the `hero_3`.
!!! info
Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute.
Because the `hero_3` is fresh it has all it's data available.
Generates the output:
```
Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp'
```
36. The `with` block ends here (there's no more indented code), so the **session** is closed, running all it's closing code.
This includes doing a `ROLLBACK` of any possible transaction that could have been started.
Generates the output:
```
INFO Engine ROLLBACK
```
37. Print the line `"After the session closes"`.
Generates the output:
```
After the session closes
```
38. Print the `hero_1` after closing the session.
Generates the output:
```
Hero 1: age=None id=1 name='Deadpond' secret_name='Dive Wilson'
```
39. Print the `hero_2` after closing the session.
Generates the output:
```
Hero 2: age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador'
```
40. Print the `hero_3` after closing the session.
Generates the output:
```
Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp'
```

View File

@ -0,0 +1,75 @@
1. Import `Optional` from `typing` to declare fields that could be `None`.
2. Import the things we will need from `sqlmodel`: `Field`, `SQLModel`, `create_engine`.
3. Create the `Hero` model class, representing the `hero` table in the database.
And also mark this class as a **table model** with `table=True`.
4. Create the `id` field:
It could be `None` until the database assigns a value to it, so we annotate it with `Optional`.
It is a **primary key**, so we use `Field()` and the argument `primary_key=True`.
5. Create the `name` field.
It is required, so there's no default value, and it's not `Optional`.
6. Create the `secret_name` field.
Also required.
7. Create the `age` field.
It is not required, the default value is `None`.
In the database, the default value will be `NULL`, the SQL equivalent of `None`.
As this field could be `None` (and `NULL` in the database), we annotate it with `Optional`.
8. Write the name of the database file.
9. Use the name of the database file to create the database URL.
10. Create the engine using the URL.
This doesn't create the database yet, no file or table is created at this point, only the **engine** object that will handle the connections with this specific database, and with specific support for SQLite (based on the URL).
11. Put the code that creates side effects in a function.
In this case, only one line that creates the database file with the table.
12. Create all the tables that were automatically registered in `SQLModel.metadata`.
13. Add a main block, or "Top-level script environment".
And put some logic to be executed when this is called directly with Python, as in:
<div class="termy">
```console
$ python app.py
// Execute all the stuff and show the output
```
</div>
...but that is not executed when importing something from this module, like:
```Python
from app import Hero
```
14. In this main block, call the function that creates the database file and the table.
This way when we call it with:
<div class="termy">
```console
$ python app.py
// Doing stuff ✨
```
</div>
...it will create the database file and the table.

View File

@ -0,0 +1,96 @@
1. Select the hero we will delete.
2. Execute the query with the select statement object.
This generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00011s] ('Spider-Youngster',)
```
3. Get one hero object, expecting exactly one.
!!! tip
This ensures there's no more than one, and that there's exactly one, not `None`.
This would never return `None`, instead it would raise an exception.
4. Print the hero object.
This generates the output:
```
Hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
```
5. Delete the hero from the **session**.
This marks the hero as deleted from the session, but it will not be removed from the database until we **commit** the changes.
6. Commit the session.
This saves the changes in the session, including deleting this row.
It generates the output:
```
INFO Engine DELETE FROM hero WHERE hero.id = ?
INFO Engine [generated in 0.00020s] (2,)
INFO Engine COMMIT
```
7. Print the deleted hero object.
The hero is deleted in the database. And is marked as deleted in the session.
But we still have the object in memory with its data, so we can use it to print it.
This generates the output:
```
Deleted hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
```
8. Select the same hero again.
We'll do this to confirm if the hero is really deleted.
9. Execute the select statement.
This generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00013s] ('Spider-Youngster',)
```
10. Get the "first" item from the `results`.
If no items were found, this will return `None`, which is what we expect.
11. Check if the first item from the results is `None`.
12. If this first item is indeed `None`, it means that it was correctly deleted from the database.
Now we can print a message to confirm.
This generates the output:
```
There's no hero named Spider-Youngster
```
13. This is the end of the `with` block, here the **session** executes its closing code.
This generates the output:
```
INFO Engine ROLLBACK
```

View File

@ -0,0 +1,17 @@
1. Import the `app` from the the `main` module.
2. We create a `TestClient` for the FastAPI `app` and put it in the variable `client`.
3. Then we use use this `client` to **talk to the API** and send a `POST` HTTP operation, creating a new hero.
4. Then we get the **JSON data** from the response and put it in the variable `data`.
5. Next we start testing the results with `assert` statements, we check that the status code of the response is `200`.
6. We check that the `name` of the hero created is `"Deadpond"`.
7. We check that the `secret_name` of the hero created is `"Dive Wilson"`.
8. We check that the `age` of the hero created is `None`, because we didn't send an age.
9. We check that the hero created has an `id` created by the database, so it's not `None`.

View File

@ -0,0 +1,25 @@
1. Import the `get_session` dependency from the the `main` module.
2. Define the new function that will be the new **dependency override**.
3. This function will return a different **session** than the one that would be returned by the original `get_session` function.
We haven't seen how this new **session** object is created yet, but the point is that this is a different session than the original one from the app.
This session is attached to a different **engine**, and that different **engine** uses a different URL, for a database just for testing.
We haven't defined that new **URL** nor the new **engine** yet, but here we already see the that this object `session` will override the one returned by the original dependency `get_session()`.
4. Then, the FastAPI `app` object has an attribute `app.dependency_overrides`.
This attribute is a dictionary, and we can put dependency overrides in it by passing, as the **key**, the **original dependency function**, and as the **value**, the **new overriding dependency function**.
So, here we are telling the FastAPI app to use `get_session_override` instead of `get_session` in all the places in the code that depend on `get_session`, that is, all the parameters with something like:
```Python
session: Session = Depends(get_session)
```
5. After we are done with the dependency override, we can restore the application back to normal, by removing all the values in this dictionary `app.dependency_overrides`.
This way whenever a *path operation function* needs the dependency FastAPI will use the original one instead of the override.

View File

@ -0,0 +1,37 @@
1. Here's a subtle thing to notice.
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 line, by importing 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`.
2. Here we create a new **engine**, completely different from the one in `main.py`.
This is the engine we will use for the tests.
We use the new URL of the database for tests:
```
sqlite:///testing.db
```
And again, we use the connection argument `check_same_thread=False`.
3. Then we call:
```Python
SQLModel.metadata.create_all(engine)
```
...to make sure we create all the tables in the new testing database.
The **table models** are registered in `SQLModel.metadata` just because we imported *something* from `.main`, and the code in `.main` was executed, creating the classes for the **table models** and automatically registering them in `SQLModel.metadata`.
So, by the point we call this method, the **table models** are already registered there. 💯
4. Here's where we create the custom **session** object for this test in a `with` block.
It uses the new custom **engine** we created, so anything that uses this session will be using the testing database.
5. Now, back to the dependency override, it is just returning the same **session** object from outside, that's it, that's the whole trick.
6. By this point, the testing **session** `with` block finishes, and the session is closed, the file is closed, etc.

View File

@ -0,0 +1,26 @@
1. Import `StaticPool` from `sqlmodel`, we will use it in a bit.
2. For the **SQLite URL**, don't write any file name, leave it empty.
So, instead of:
```
sqlite:///testing.db
```
...just write:
```
sqlite://
```
This is enough to tell **SQLModel** (actually SQLAlchemy) that we want to use an **in-memory SQLite database**.
3. Remember that we told the **low-level** library in charge of communicating with SQLite that we want to be able to **access the database from different threads** with `check_same_thread=False`?
Now that we use an **in-memory database**, we need to also tell SQLAlchemy that we want to be able to use the **same in-memory database** object from different threads.
We tell it that with the `poolclass=StaticPool` parameter.
!!! info
You can read more details in the <a href="https://docs.sqlalchemy.org/en/14/dialects/sqlite.html#using-a-memory-database-in-multiple-threads" class="external-link" target="_blank">SQLAlchemy documentation about Using a Memory Database in Multiple Threads</a>

View File

@ -0,0 +1,41 @@
1. Import `pytest`.
2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency).
We also give it a name of `"session"`, this will be important in the testing function.
3. Create the fixture function. This is equivalent to a FastAPI dependency function.
In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**.
Then we `yield` the `session` object.
4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object.
Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done.
We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**.
By using `yield`, pytest will:
* run the first part
* create the **session** object
* give it to the test function
* run the test function
* once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block.
5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with:
```Python
session: Session = Depends(session_fixture)
```
...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture.
In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work.
We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor.
6. Now in the dependency override function, we just return the same `session` object that came from outside it.
The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override.

View File

@ -0,0 +1,23 @@
1. Create the new fixture named `"client"`.
2. This **client fixture**, in turn, also requires the **session fixture**.
3. Now we create the **dependency override** inside the client fixture.
4. Set the **dependency override** in the `app.dependency_overrides` dictionary.
5. Create the `TestClient` with the **FastAPI** `app`.
6. `yield` the `TestClient` instance.
By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`.
7. This is the cleanup code, after `yield`, and after the test function is done.
Here we clear the dependency overrides (here it's only one) in the FastAPI `app`.
8. Now the test function requires the **client fixture**.
And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it.
The fixtures take care of all the **setup** and **cleanup** code.

View File

@ -0,0 +1,57 @@
1. We use a function `create_heroes()` to put this logic together.
2. Create each of the objects/instances of the `Hero` model.
Each of them represents the data for one row.
3. Use a `with` block to create a `Session` using the `engine`.
The new **sesion** will be assigned to the variable `session`.
And it will be automatically closed when the `with` block is finished.
4. Add each of the objects/instances to the **session**.
Each of these objects represents a row in the database.
They are all waiting there in the session to be saved.
5. **Commit** the changes to the database.
This will actually send the data to the database.
It will start a transaction automatically and save all the data in a single batch.
6. By this point, after the `with` block is finished, the **session** is automatically closed.
7. We have a `main()` function with all the code that should be executed when the program is called as a **script from the console**.
That way we can add more code later to this function.
We then put this function `main()` in the main block below.
And as it is a single function, other Python files could **import it** and call it directly.
8. In this `main()` function, we are also creating the database and the tables.
In the previous version, this function was called directly in the main block.
But now it is just called in the `main()` function.
9. And now we are also creating the heroes in this `main()` function.
10. We still have a main block to execute some code when the program is run as a script from the command line, like:
<div class="termy">
```console
$ python app.py
// Do whatever is in the main block 🚀
```
</div>
11. There's a single `main()` function now that contains all the code that should be executed when running the program from the console.
So this is all we need to have in the main block. Just call the `main()` function.

View File

@ -0,0 +1,63 @@
1. Import from `sqlmodel` everything we will use, including the new `select()` function.
2. Create the `Hero` class model, representing the `hero` table.
3. Create the **engine**, we should use a single one shared by all the application code, and that's what we are doing here.
4. Create all the tables for the models registered in `SQLModel.metadata`.
This also creates the database if it doesn't exist already.
5. Create each one of the `Hero` objects.
You might not have this in your version if you had already created the data in the database.
6. Create a new **session** and use it to `add` the heroes to the database, and then `commit` the changes.
7. Create a new **session** to query data.
!!! tip
Notice that this is a new **session** independent from the one in the other function above.
But it still uses the same **engine**. We still have one engine for the whole application.
8. Use the `select()` function to create a statement selecting all the `Hero` objects.
This selects all the rows in the `hero` table.
9. Use `session.exec(statement)` to make the **session** use the **engine** to execute the internal SQL statement.
This will go to the database, execute that SQL, and get the results back.
It returns a special iterable object that we put in the variable `results`.
This generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
INFO Engine [no key 0.00032s] ()
```
10. Iterate for each `Hero` object in the `results`.
11. Print each `hero`.
The 3 iterations in the `for` loop will generate this output:
```
id=1 name='Deadpond' age=None secret_name='Dive Wilson'
id=2 name='Spider-Boy' age=None secret_name='Pedro Parqueador'
id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp'
```
12. At this point, after the `with` block, the **session** is closed.
This generates the output:
```
INFO Engine ROLLBACK
```
13. Add this function `select_heroes()` to the `main()` function so that it is called when we run this program from the command line.

View File

@ -0,0 +1,68 @@
1. Select the hero we will work with.
2. Execute the query with the select statement object.
This generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00017s] ('Spider-Boy',)
```
3. Get one hero object, expecting exactly one.
!!! tip
This ensures there's no more than one, and that there's exactly one, not `None`.
This would never return `None`, instead it would raise an exception.
4. Print the hero object.
This generates the output:
```
Hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2
```
5. Set the hero's age field to the new value `16`.
Now the `hero` object in memory has a different value for the age, but it is still not saved to the database.
6. Add the hero to the session.
This puts it in that temporary place in the session before committing.
But it's still not saved in the database yet.
7. Commit the session.
This saves the updated hero to the database.
And this generates the output:
```
INFO Engine UPDATE hero SET age=? WHERE hero.id = ?
INFO Engine [generated in 0.00017s] (16, 2)
INFO Engine COMMIT
```
8. Refresh the hero object to have the recent data, including the age we just committed.
This generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00018s] (2,)
```
9. Print the updated hero object.
This generates the output:
```
Updated hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=16 id=2
```

View File

@ -0,0 +1,159 @@
1. Select the hero `Spider-Boy`.
2. Execute the select statement.
This generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00018s] ('Spider-Boy',)
```
3. Get one hero object, the only one that should be there for **Spider-Boy**.
4. Print this hero.
This generates the output:
```
Hero 1: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2
```
5. Select another hero.
6. Execute the select statement.
This generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.name = ?
INFO Engine [no key 0.00020s] ('Captain North America',)
```
!!! tip
See the `BEGIN` at the top?
This is SQLAlchemy automatically starting a transaction for us.
This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database.
7. Get one hero object for this new query.
The only one that should be there for **Captain North America**.
8. Print this second hero.
This generates the output:
```
Hero 2: name='Captain North America' secret_name='Esteban Rogelios' age=93 id=7
```
9. Update the age for the first hero.
Set the value of the attribute `age` to `16`.
This updates the hero object in memory, but not yet in the database.
10. Update the name of the first hero.
Now the name of the hero will not be `"Spider-Boy"` but `"Spider-Youngster"`.
Also, this updates the object in memory, but not yet in the database.
11. Add this first hero to the session.
This puts it in the temporary space in the **session** before committing it to the database.
It is not saved yet.
12. Update the name of the second hero.
Now the hero has a bit more precision in the name. 😜
This updates the object in memory, but not yet in the database.
13. Update the age of the second hero.
This updates the object in memory, but not yet in the database.
14. Add the second hero to the session.
This puts it in the temporary space in the **session** before committing it to the database.
15. Commit all the changes tracked in the session.
This commits everything in one single batch.
This generates the output:
```
INFO Engine UPDATE hero SET name=?, age=? WHERE hero.id = ?
INFO Engine [generated in 0.00028s] (('Spider-Youngster', 16, 2), ('Captain North America Except Canada', 110, 7))
INFO Engine COMMIT
```
!!! tip
See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch.
Here it updates both heroes in a single SQL query.
16. Refresh the first hero.
This generates the output:
```
INFO Engine BEGIN (implicit)
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [generated in 0.00023s] (2,)
```
!!! tip
Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`.
17. Refresh the second hero.
This generates the output:
```
INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age
FROM hero
WHERE hero.id = ?
INFO Engine [cached since 0.001709s ago] (7,)
```
!!! tip
SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one.
18. Print the first hero, now udpated.
This generates the output:
```
Updated hero 1: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2
```
19. Print the second hero, now updated.
This generates the output:
```
Updated hero 2: name='Captain North America Except Canada' secret_name='Esteban Rogelios' age=110 id=7
```
20. Here is the end of the `with` block statement, so the session can execute its terminating code.
The session will `ROLLBACK` (undo) any possible changes in the last transaction that were not committed.
This generates the output:
```
INFO Engine ROLLBACK
```