from fastapi import FastAPI
from sqlalchemy import Boolean, Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base, declared_attr
-from sqlalchemy.orm import scoped_session, sessionmaker
+from sqlalchemy.orm import sessionmaker
+from starlette.requests import Request
# SQLAlchemy specific code, as with any other app
SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False}
)
-db_session = scoped_session(
- sessionmaker(autocommit=False, autoflush=False, bind=engine)
-)
+Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class CustomBase:
Base.metadata.create_all(bind=engine)
+db_session = Session()
+
first_user = db_session.query(User).first()
if not first_user:
u = User(email="johndoe@example.com", hashed_password="notreallyhashed")
db_session.add(u)
db_session.commit()
+db_session.close()
+
# Utility
def get_user(db_session, user_id: int):
@app.get("/users/{user_id}")
-def read_user(user_id: int):
- user = get_user(db_session, user_id=user_id)
+def read_user(request: Request, user_id: int):
+ user = get_user(request._scope["db"], user_id=user_id)
return user
+
+
+@app.middleware("http")
+async def close_db(request, call_next):
+ request._scope["db"] = Session()
+ response = await call_next(request)
+ request._scope["db"].close()
+ return response
Define the database that SQLAlchemy should "connect" to:
-```Python hl_lines="7"
+```Python hl_lines="8"
{!./src/sql_databases/tutorial001.py!}
```
## Create the SQLAlchemy `engine`
-```Python hl_lines="10 11 12"
+```Python hl_lines="11 12 13"
{!./src/sql_databases/tutorial001.py!}
```
That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
-## Create a `scoped_session`
+## Create a `Session` class
-```Python hl_lines="13 14 15"
+Each instance of the `Session` class will have a connection to the database.
+
+This is not a connection to the database yet, but once we create an instance of this class, that instance will have the actual connection to the database.
+
+```Python hl_lines="14"
{!./src/sql_databases/tutorial001.py!}
```
-!!! note "Very Technical Details"
- Don't worry too much if you don't understand this. You can still use the code.
+## Create a middleware to handle sessions
- This `scoped_session` is a feature of SQLAlchemy.
+Now let's temporarily jump to the end of the file, to use the `Session` class we created above.
- The resulting object, the `db_session` can then be used anywhere as a normal SQLAlchemy session.
-
- It can be used as a "global" variable because it is implemented to work independently on each "<abbr title="A sequence of code being executed by the program, while at the same time, or at intervals, there can be others being executed too.">thread</abbr>", so the actions you perform with it in one path operation function won't affect the actions performed (possibly concurrently) by other path operation functions.
+We need to have an independent `Session` per request, use the same session through all the request and then close it after the request is finished.
+
+And then a new session will be created for the next request.
+
+For that, we will create a new middleware.
+
+A "middleware" is a function that is always executed for each request, and have code before and after the request.
+
+The middleware we will create (just a function) will create a new SQLAlchemy `Session` for each request, add it to the request and then close it once the request is finished.
+
+```Python hl_lines="62 63 64 65 66 67"
+{!./src/sql_databases/tutorial001.py!}
+```
+
+### About `request._scope`
+
+`request._scope` is a "private property" of each request. We normally shouldn't need to use a "private property" from a Python object.
+
+But we need to attach the session to the request to be able to ensure a single session/database-connection is used through all the request, and then closed afterwards.
+
+In the near future, Starlette <a href="https://github.com/encode/starlette/issues/379" target="_blank">will have a way to attach custom objects to each request</a>.
+
+When that happens, this tutorial will be updated to use the new official way of doing it.
## Create a `CustomBase` model
So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
-```Python hl_lines="18 19 20 21 22"
+```Python hl_lines="17 18 19 20 21"
{!./src/sql_databases/tutorial001.py!}
```
## Create the SQLAlchemy `Base` model
-```Python hl_lines="25"
+```Python hl_lines="24"
{!./src/sql_databases/tutorial001.py!}
```
Here's a user model that will be a table in the database:
-```Python hl_lines="28 29 30 31 32"
+```Python hl_lines="27 28 29 30 31"
{!./src/sql_databases/tutorial001.py!}
```
In a very simplistic way, initialize your database (create the tables, etc) and make sure you have a first user:
-```Python hl_lines="35 37 38 39 40 41"
+```Python hl_lines="34 36 38 39 40 41 42 44"
{!./src/sql_databases/tutorial001.py!}
```
+!!! info
+ Notice that we close the session with `db_session.close()`.
+
+ We close this session because we only used it to create this first user.
+
+ Every new request will get its own new session.
+
### Note
Normally you would probably initialize your database (create tables, etc) with <a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank">Alembic</a>.
By creating a function that is only dedicated to getting your user from a `user_id` (or any other parameter) independent of your path operation function, you can more easily re-use it in multiple parts and also add <abbr title="Automated tests, written in code, that check if another piece of code is working correctly.">unit tests</abbr> for it:
-```Python hl_lines="45 46"
+```Python hl_lines="48 49"
{!./src/sql_databases/tutorial001.py!}
```
Create your app and path operation function:
-```Python hl_lines="50 53 54 55 56"
+```Python hl_lines="53 56 57 58 59"
{!./src/sql_databases/tutorial001.py!}
```
-As we are using SQLAlchemy's `scoped_session`, we don't even have to create a dependency with `Depends`.
+We are creating the database session before each request, attaching it to the request, and then closing it afterwards.
+
+All of this is done in the middleware explained above.
+
+Because of that, we can use the `Request` to access the database session with `request._scope["db"]`.
-We can just call `get_user` directly from inside of the path operation function and use the global `db_session`.
+Then we can just call `get_user` directly from inside of the path operation function and use that session.
## Create the path operation function
Then we should declare the path operation without `async def`, just with a normal `def`:
-```Python hl_lines="54"
+```Python hl_lines="57"
{!./src/sql_databases/tutorial001.py!}
```