Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/advanced/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The **Advanced User Guide** is gradually growing, you can already read about som

At some point it will include:

* How to use `async` and `await` with the async session.
* [How to use `async` and `await` with the async session](../tutorial/async/index.md).
* How to run migrations.
* How to combine **SQLModel** models with SQLAlchemy.
* ...and more. 🤓
108 changes: 108 additions & 0 deletions docs/tutorial/async/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Async with SQLModel

**SQLModel** is based on SQLAlchemy, which has great support for `async` and `await` with `asyncio`.

You can use **SQLModel** in an asynchronous way as well.

## Install `aiosqlite`

For this example, we will use **SQLite** with the `aiosqlite` driver to make it asynchronous.

Make sure you install `aiosqlite`:

<div class="termy">

```console
$ pip install aiosqlite
---> 100%
```

</div>

## Create the Async Engine

Instead of `create_engine`, we use `create_async_engine` from `sqlalchemy.ext.asyncio`.

And we change the connection URL to use `sqlite+aiosqlite`.

{* ./docs_src/tutorial/async/tutorial001_py310.py ln[1:16] hl[2,15:16] *}

## Async Session

To use an asynchronous session, we use `AsyncSession` from `sqlmodel.ext.asyncio`.

It is a subclass of SQLAlchemy's `AsyncSession` with added support for SQLModel's `exec()` method.

## Create the Database and Tables

When using an async engine, we cannot call `SQLModel.metadata.create_all(engine)` directly because it is a synchronous operation.

Instead, we use `engine.begin()` and `conn.run_sync()`.

{* ./docs_src/tutorial/async/tutorial001_py310.py ln[19:21] hl[19:21] *}

## Async FastAPI Dependency

We can create an async dependency to get the session.

{* ./docs_src/tutorial/async/tutorial001_py310.py ln[27:30] hl[27:30] *}

## Async Path Operations

Now we can use `async def` for our path operations and `await` the session methods.

We use `await session.exec()` to execute queries, and `await session.commit()`, `await session.refresh()` for mutations.

{* ./docs_src/tutorial/async/tutorial001_py310.py ln[33:45] hl[35:37,42:43] *}

## Full Example

Here is the complete file:

{* ./docs_src/tutorial/async/tutorial001_py310.py *}

## Common Pitfalls and Best Practices

### Use `await` for Database Operations

When using `AsyncSession`, remember to `await` all methods that interact with the database.

This includes:
* `session.exec()`
* `session.commit()`
* `session.refresh()`
* `session.get()`
* `session.delete()`

### Relationships and Lazy Loading

By default, SQLAlchemy (and SQLModel) uses "lazy loading" for relationships. In synchronous code, this means that when you access a relationship attribute, it automatically fetches the data from the database.

In asynchronous code, **lazy loading is not supported** because it would need to perform I/O without an `await`.

If you try to access a relationship that hasn't been loaded yet, you will get an error.

To solve this, you should use **eager loading** with `selectinload`.

```Python
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlmodel import select

# ...

async def read_heroes(session: AsyncSession):
statement = select(Hero).options(selectinload(Hero.team))
result = await session.exec(statement)
heroes = result.all()
return heroes
```

### Async Database Drivers

Make sure you use an asynchronous database driver.

* For **SQLite**, use `aiosqlite` with `sqlite+aiosqlite://`.
* For **PostgreSQL**, use `asyncpg` with `postgresql+asyncpg://`.

If you use a synchronous driver (like `sqlite://` or `postgresql://`), it will not work with `create_async_engine`.
52 changes: 52 additions & 0 deletions docs_src/tutorial/async/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import List, Union

from fastapi import Depends, FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import Field, SQLModel, select
from sqlmodel.ext.asyncio import AsyncSession


class Hero(SQLModel, table=True):
id: Union[int, None] = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Union[int, None] = Field(default=None, index=True)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}"

engine = create_async_engine(sqlite_url, echo=True)


async def init_db():
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)


app = FastAPI()


@app.on_event("startup")
async def on_startup():
await init_db()


async def get_session():
async with AsyncSession(engine) as session:
yield session


@app.post("/heroes/", response_model=Hero)
async def create_hero(hero: Hero, session: AsyncSession = Depends(get_session)):
session.add(hero)
await session.commit()
await session.refresh(hero)
return hero


@app.get("/heroes/", response_model=List[Hero])
async def read_heroes(session: AsyncSession = Depends(get_session)):
result = await session.exec(select(Hero))
heroes = result.all()
return heroes
50 changes: 50 additions & 0 deletions docs_src/tutorial/async/tutorial001_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from fastapi import Depends, FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import Field, SQLModel, select
from sqlmodel.ext.asyncio import AsyncSession


class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}"

engine = create_async_engine(sqlite_url, echo=True)


async def init_db():
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)


app = FastAPI()


@app.on_event("startup")
async def on_startup():
await init_db()


async def get_session():
async with AsyncSession(engine) as session:
yield session


@app.post("/heroes/", response_model=Hero)
async def create_hero(hero: Hero, session: AsyncSession = Depends(get_session)):
session.add(hero)
await session.commit()
await session.refresh(hero)
return hero


@app.get("/heroes/", response_model=list[Hero])
async def read_heroes(session: AsyncSession = Depends(get_session)):
result = await session.exec(select(Hero))
heroes = result.all()
return heroes
52 changes: 52 additions & 0 deletions docs_src/tutorial/async/tutorial001_py39.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import List, Optional

from fastapi import Depends, FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import Field, SQLModel, select
from sqlmodel.ext.asyncio import AsyncSession


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Optional[int] = Field(default=None, index=True)


sqlite_file_name = "database.db"
sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}"

engine = create_async_engine(sqlite_url, echo=True)


async def init_db():
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)


app = FastAPI()


@app.on_event("startup")
async def on_startup():
await init_db()


async def get_session():
async with AsyncSession(engine) as session:
yield session


@app.post("/heroes/", response_model=Hero)
async def create_hero(hero: Hero, session: AsyncSession = Depends(get_session)):
session.add(hero)
await session.commit()
await session.refresh(hero)
return hero


@app.get("/heroes/", response_model=List[Hero])
async def read_heroes(session: AsyncSession = Depends(get_session)):
result = await session.exec(select(Hero))
heroes = result.all()
return heroes
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ nav:
- tutorial/fastapi/tests.md
- Advanced User Guide:
- advanced/index.md
- tutorial/async/index.md
- advanced/decimal.md
- advanced/uuid.md
- Resources:
Expand Down
1 change: 1 addition & 0 deletions sqlmodel/ext/asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .session import AsyncSession as AsyncSession
Loading