From 8769f40f3842dcbfff7e77502c5adbf50dfcf2ab Mon Sep 17 00:00:00 2001 From: shuteng8787-sudo Date: Thu, 12 Feb 2026 10:07:50 +0800 Subject: [PATCH 1/2] docs: add async/await tutorial and examples (fixes #626) --- docs/advanced/index.md | 2 +- docs/tutorial/async/index.md | 108 +++++++++++++++++++ docs_src/tutorial/async/tutorial001.py | 52 +++++++++ docs_src/tutorial/async/tutorial001_py310.py | 50 +++++++++ docs_src/tutorial/async/tutorial001_py39.py | 52 +++++++++ mkdocs.yml | 1 + sqlmodel/ext/asyncio/__init__.py | 1 + 7 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 docs/tutorial/async/index.md create mode 100644 docs_src/tutorial/async/tutorial001.py create mode 100644 docs_src/tutorial/async/tutorial001_py310.py create mode 100644 docs_src/tutorial/async/tutorial001_py39.py diff --git a/docs/advanced/index.md b/docs/advanced/index.md index f6178249ce..bbc4156909 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -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. 🤓 diff --git a/docs/tutorial/async/index.md b/docs/tutorial/async/index.md new file mode 100644 index 0000000000..63cb26eacc --- /dev/null +++ b/docs/tutorial/async/index.md @@ -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`: + +
+ +```console +$ pip install aiosqlite +---> 100% +``` + +
+ +## 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`. diff --git a/docs_src/tutorial/async/tutorial001.py b/docs_src/tutorial/async/tutorial001.py new file mode 100644 index 0000000000..ccd1851372 --- /dev/null +++ b/docs_src/tutorial/async/tutorial001.py @@ -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 diff --git a/docs_src/tutorial/async/tutorial001_py310.py b/docs_src/tutorial/async/tutorial001_py310.py new file mode 100644 index 0000000000..800d688803 --- /dev/null +++ b/docs_src/tutorial/async/tutorial001_py310.py @@ -0,0 +1,50 @@ +from fastapi import FastAPI, Depends +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 diff --git a/docs_src/tutorial/async/tutorial001_py39.py b/docs_src/tutorial/async/tutorial001_py39.py new file mode 100644 index 0000000000..ddf9eaccaf --- /dev/null +++ b/docs_src/tutorial/async/tutorial001_py39.py @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index b89516e024..05979d5227 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: diff --git a/sqlmodel/ext/asyncio/__init__.py b/sqlmodel/ext/asyncio/__init__.py index e69de29bb2..51b71e7e98 100644 --- a/sqlmodel/ext/asyncio/__init__.py +++ b/sqlmodel/ext/asyncio/__init__.py @@ -0,0 +1 @@ +from .session import AsyncSession as AsyncSession From 901f99b0d56df8e6c6e7aa3daa112e663d5e9122 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 12 Feb 2026 07:06:46 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/async/index.md | 2 +- docs_src/tutorial/async/tutorial001_py310.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/async/index.md b/docs/tutorial/async/index.md index 63cb26eacc..40017125d6 100644 --- a/docs/tutorial/async/index.md +++ b/docs/tutorial/async/index.md @@ -100,7 +100,7 @@ async def read_heroes(session: AsyncSession): ### Async Database Drivers -Make sure you use an asynchronous database driver. +Make sure you use an asynchronous database driver. * For **SQLite**, use `aiosqlite` with `sqlite+aiosqlite://`. * For **PostgreSQL**, use `asyncpg` with `postgresql+asyncpg://`. diff --git a/docs_src/tutorial/async/tutorial001_py310.py b/docs_src/tutorial/async/tutorial001_py310.py index 800d688803..b85fc265d0 100644 --- a/docs_src/tutorial/async/tutorial001_py310.py +++ b/docs_src/tutorial/async/tutorial001_py310.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, Depends +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