Final touches and recap#

There is one more improvement that we can make to our application. Currently, we utilize both the SQLAlchemyInitPlugin and the SQLAlchemySerializationPlugin, but there is a shortcut for this configuration: the SQLAlchemyPlugin is a combination of the two, so we can simplify our configuration by using it instead.

Here is our final application:

 1from typing import AsyncGenerator, List, Optional
 2
 3from sqlalchemy import select
 4from sqlalchemy.exc import IntegrityError, NoResultFound
 5from sqlalchemy.ext.asyncio import AsyncSession
 6from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
 7
 8from litestar import Litestar, get, post, put
 9from litestar.exceptions import ClientException, NotFoundException
10from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
11from litestar.status_codes import HTTP_409_CONFLICT
12
13
14class Base(DeclarativeBase): ...
15
16
17class TodoItem(Base):
18    __tablename__ = "todo_items"
19
20    title: Mapped[str] = mapped_column(primary_key=True)
21    done: Mapped[bool]
22
23
24async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]:
25    try:
26        async with db_session.begin():
27            yield db_session
28    except IntegrityError as exc:
29        raise ClientException(
30            status_code=HTTP_409_CONFLICT,
31            detail=str(exc),
32        ) from exc
33
34
35async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem:
36    query = select(TodoItem).where(TodoItem.title == todo_name)
37    result = await session.execute(query)
38    try:
39        return result.scalar_one()
40    except NoResultFound as e:
41        raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e
42
43
44async def get_todo_list(done: Optional[bool], session: AsyncSession) -> List[TodoItem]:
45    query = select(TodoItem)
46    if done is not None:
47        query = query.where(TodoItem.done.is_(done))
48
49    result = await session.execute(query)
50    return list(result.scalars().all())
51
52
53@get("/")
54async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> List[TodoItem]:
55    return await get_todo_list(done, transaction)
56
57
58@post("/")
59async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem:
60    transaction.add(data)
61    return data
62
63
64@put("/{item_title:str}")
65async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem:
66    todo_item = await get_todo_by_title(item_title, transaction)
67    todo_item.title = data.title
68    todo_item.done = data.done
69    return todo_item
70
71
72db_config = SQLAlchemyAsyncConfig(
73    connection_string="sqlite+aiosqlite:///todo.sqlite",
74    metadata=Base.metadata,
75    create_all=True,
76    before_send_handler="autocommit",
77)
78
79app = Litestar(
80    [get_list, add_item, update_item],
81    dependencies={"transaction": provide_transaction},
82    plugins=[SQLAlchemyPlugin(db_config)],
83)
 1from typing import Optional
 2from collections.abc import AsyncGenerator
 3
 4from sqlalchemy import select
 5from sqlalchemy.exc import IntegrityError, NoResultFound
 6from sqlalchemy.ext.asyncio import AsyncSession
 7from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
 8
 9from litestar import Litestar, get, post, put
10from litestar.exceptions import ClientException, NotFoundException
11from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
12from litestar.status_codes import HTTP_409_CONFLICT
13
14
15class Base(DeclarativeBase): ...
16
17
18class TodoItem(Base):
19    __tablename__ = "todo_items"
20
21    title: Mapped[str] = mapped_column(primary_key=True)
22    done: Mapped[bool]
23
24
25async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]:
26    try:
27        async with db_session.begin():
28            yield db_session
29    except IntegrityError as exc:
30        raise ClientException(
31            status_code=HTTP_409_CONFLICT,
32            detail=str(exc),
33        ) from exc
34
35
36async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem:
37    query = select(TodoItem).where(TodoItem.title == todo_name)
38    result = await session.execute(query)
39    try:
40        return result.scalar_one()
41    except NoResultFound as e:
42        raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e
43
44
45async def get_todo_list(done: Optional[bool], session: AsyncSession) -> list[TodoItem]:
46    query = select(TodoItem)
47    if done is not None:
48        query = query.where(TodoItem.done.is_(done))
49
50    result = await session.execute(query)
51    return list(result.scalars().all())
52
53
54@get("/")
55async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> list[TodoItem]:
56    return await get_todo_list(done, transaction)
57
58
59@post("/")
60async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem:
61    transaction.add(data)
62    return data
63
64
65@put("/{item_title:str}")
66async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem:
67    todo_item = await get_todo_by_title(item_title, transaction)
68    todo_item.title = data.title
69    todo_item.done = data.done
70    return todo_item
71
72
73db_config = SQLAlchemyAsyncConfig(
74    connection_string="sqlite+aiosqlite:///todo.sqlite",
75    metadata=Base.metadata,
76    create_all=True,
77    before_send_handler="autocommit",
78)
79
80app = Litestar(
81    [get_list, add_item, update_item],
82    dependencies={"transaction": provide_transaction},
83    plugins=[SQLAlchemyPlugin(db_config)],
84)
 1from collections.abc import AsyncGenerator
 2
 3from sqlalchemy import select
 4from sqlalchemy.exc import IntegrityError, NoResultFound
 5from sqlalchemy.ext.asyncio import AsyncSession
 6from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
 7
 8from litestar import Litestar, get, post, put
 9from litestar.exceptions import ClientException, NotFoundException
10from litestar.plugins.sqlalchemy import SQLAlchemyAsyncConfig, SQLAlchemyPlugin
11from litestar.status_codes import HTTP_409_CONFLICT
12
13
14class Base(DeclarativeBase): ...
15
16
17class TodoItem(Base):
18    __tablename__ = "todo_items"
19
20    title: Mapped[str] = mapped_column(primary_key=True)
21    done: Mapped[bool]
22
23
24async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]:
25    try:
26        async with db_session.begin():
27            yield db_session
28    except IntegrityError as exc:
29        raise ClientException(
30            status_code=HTTP_409_CONFLICT,
31            detail=str(exc),
32        ) from exc
33
34
35async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem:
36    query = select(TodoItem).where(TodoItem.title == todo_name)
37    result = await session.execute(query)
38    try:
39        return result.scalar_one()
40    except NoResultFound as e:
41        raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e
42
43
44async def get_todo_list(done: bool | None, session: AsyncSession) -> list[TodoItem]:
45    query = select(TodoItem)
46    if done is not None:
47        query = query.where(TodoItem.done.is_(done))
48
49    result = await session.execute(query)
50    return list(result.scalars().all())
51
52
53@get("/")
54async def get_list(transaction: AsyncSession, done: bool | None = None) -> list[TodoItem]:
55    return await get_todo_list(done, transaction)
56
57
58@post("/")
59async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem:
60    transaction.add(data)
61    return data
62
63
64@put("/{item_title:str}")
65async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem:
66    todo_item = await get_todo_by_title(item_title, transaction)
67    todo_item.title = data.title
68    todo_item.done = data.done
69    return todo_item
70
71
72db_config = SQLAlchemyAsyncConfig(
73    connection_string="sqlite+aiosqlite:///todo.sqlite",
74    metadata=Base.metadata,
75    create_all=True,
76    before_send_handler="autocommit",
77)
78
79app = Litestar(
80    [get_list, add_item, update_item],
81    dependencies={"transaction": provide_transaction},
82    plugins=[SQLAlchemyPlugin(db_config)],
83)

Recap#

In this tutorial, we have learned how to use the SQLAlchemy plugin to create a simple application that uses a database to store and retrieve data.

In the final application TodoItem is defined, representing a TODO item. It extends from the DeclarativeBase class provided by SQLAlchemy:

 1from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
 2
 3
 4class Base(DeclarativeBase): ...
 5
 6
 7class TodoItem(Base):
 8    __tablename__ = "todo_items"
 9
10    title: Mapped[str] = mapped_column(primary_key=True)
11    done: Mapped[bool]

Next, we define a dependency that centralizes our database transaction management and error handling. This dependency depends on the db_session dependency, which is provided by the SQLAlchemy plugin, and is made available to our handlers via the transaction argument:

 1from typing import AsyncGenerator
 2
 3from sqlalchemy.exc import IntegrityError
 4from sqlalchemy.ext.asyncio import AsyncSession
 5
 6from litestar.exceptions import ClientException
 7from litestar.status_codes import HTTP_409_CONFLICT
 8
 9
10async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]:
11    try:
12        async with db_session.begin():
13            yield db_session
14    except IntegrityError as exc:
15        raise ClientException(
16            status_code=HTTP_409_CONFLICT,
17            detail=str(exc),
18        ) from exc
 1from collections.abc import AsyncGenerator
 2
 3from sqlalchemy.exc import IntegrityError
 4from sqlalchemy.ext.asyncio import AsyncSession
 5
 6from litestar.exceptions import ClientException
 7from litestar.status_codes import HTTP_409_CONFLICT
 8
 9
10async def provide_transaction(db_session: AsyncSession) -> AsyncGenerator[AsyncSession, None]:
11    try:
12        async with db_session.begin():
13            yield db_session
14    except IntegrityError as exc:
15        raise ClientException(
16            status_code=HTTP_409_CONFLICT,
17            detail=str(exc),
18        ) from exc

We also define a couple of utility functions, that help us to retrieve our TODO items from the database:

 1from typing import List, Optional
 2
 3from sqlalchemy import select
 4from sqlalchemy.exc import NoResultFound
 5from sqlalchemy.ext.asyncio import AsyncSession
 6
 7from litestar.exceptions import NotFoundException
 8
 9
10async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem:
11    query = select(TodoItem).where(TodoItem.title == todo_name)
12    result = await session.execute(query)
13    try:
14        return result.scalar_one()
15    except NoResultFound as e:
16        raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e
17
18
19async def get_todo_list(done: Optional[bool], session: AsyncSession) -> List[TodoItem]:
20    query = select(TodoItem)
21    if done is not None:
22        query = query.where(TodoItem.done.is_(done))
23
24    result = await session.execute(query)
25    return list(result.scalars().all())
 1from typing import Optional
 2
 3from sqlalchemy import select
 4from sqlalchemy.exc import NoResultFound
 5from sqlalchemy.ext.asyncio import AsyncSession
 6
 7from litestar.exceptions import NotFoundException
 8
 9
10async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem:
11    query = select(TodoItem).where(TodoItem.title == todo_name)
12    result = await session.execute(query)
13    try:
14        return result.scalar_one()
15    except NoResultFound as e:
16        raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e
17
18
19async def get_todo_list(done: Optional[bool], session: AsyncSession) -> list[TodoItem]:
20    query = select(TodoItem)
21    if done is not None:
22        query = query.where(TodoItem.done.is_(done))
23
24    result = await session.execute(query)
25    return list(result.scalars().all())
 1from sqlalchemy import select
 2from sqlalchemy.exc import NoResultFound
 3from sqlalchemy.ext.asyncio import AsyncSession
 4
 5from litestar.exceptions import NotFoundException
 6
 7
 8async def get_todo_by_title(todo_name: str, session: AsyncSession) -> TodoItem:
 9    query = select(TodoItem).where(TodoItem.title == todo_name)
10    result = await session.execute(query)
11    try:
12        return result.scalar_one()
13    except NoResultFound as e:
14        raise NotFoundException(detail=f"TODO {todo_name!r} not found") from e
15
16
17async def get_todo_list(done: bool | None, session: AsyncSession) -> list[TodoItem]:
18    query = select(TodoItem)
19    if done is not None:
20        query = query.where(TodoItem.done.is_(done))
21
22    result = await session.execute(query)
23    return list(result.scalars().all())

We define our route handlers, which are the interface through which TODO items can be created, retrieved and updated:

 1from typing import List, Optional
 2
 3from sqlalchemy.ext.asyncio import AsyncSession
 4
 5from litestar import get, post, put
 6
 7
 8@get("/")
 9async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> List[TodoItem]:
10    return await get_todo_list(done, transaction)
11
12
13@post("/")
14async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem:
15    transaction.add(data)
16    return data
17
18
19@put("/{item_title:str}")
20async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem:
21    todo_item = await get_todo_by_title(item_title, transaction)
22    todo_item.title = data.title
23    todo_item.done = data.done
24    return todo_item
 1from typing import Optional
 2
 3from sqlalchemy.ext.asyncio import AsyncSession
 4
 5from litestar import get, post, put
 6
 7
 8@get("/")
 9async def get_list(transaction: AsyncSession, done: Optional[bool] = None) -> list[TodoItem]:
10    return await get_todo_list(done, transaction)
11
12
13@post("/")
14async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem:
15    transaction.add(data)
16    return data
17
18
19@put("/{item_title:str}")
20async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem:
21    todo_item = await get_todo_by_title(item_title, transaction)
22    todo_item.title = data.title
23    todo_item.done = data.done
24    return todo_item
 1from sqlalchemy.ext.asyncio import AsyncSession
 2
 3from litestar import get, post, put
 4
 5
 6@get("/")
 7async def get_list(transaction: AsyncSession, done: bool | None = None) -> list[TodoItem]:
 8    return await get_todo_list(done, transaction)
 9
10
11@post("/")
12async def add_item(data: TodoItem, transaction: AsyncSession) -> TodoItem:
13    transaction.add(data)
14    return data
15
16
17@put("/{item_title:str}")
18async def update_item(item_title: str, data: TodoItem, transaction: AsyncSession) -> TodoItem:
19    todo_item = await get_todo_by_title(item_title, transaction)
20    todo_item.title = data.title
21    todo_item.done = data.done
22    return todo_item

Finally, we define our application, using the SQLAlchemyPlugin to configure SQLAlchemy and manage the engine and session lifecycle, and register our transaction dependency.

1from litestar import Litestar
2from litestar.plugins.sqlalchemy import SQLAlchemyPlugin
3
4app = Litestar(
5    [get_list, add_item, update_item],
6    dependencies={"transaction": provide_transaction},
7    plugins=[SQLAlchemyPlugin(db_config)],
8)