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..40017125d6 --- /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..b85fc265d0 --- /dev/null +++ b/docs_src/tutorial/async/tutorial001_py310.py @@ -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 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