Introduction to Database Modelling and Repository Features¶
In this tutorial, we will cover the integrated repository features in Litestar, starting with database modelling using the included SQLAlchemy declarative model helpers. These are a series of classes and mixins that incorporate commonly used functions/column types to make working with models easier.
Tip
The full code for this tutorial can be found below in the Full Code section.
Modelling¶
We’ll begin by modelling the entities and relationships between authors and books.
We’ll start by creating the Author table, utilizing the
UUIDBase class. To keep things
simple, our first model will encompass only three fields: id, name, and dob.
app.py¶1from sqlalchemy.orm import Mapped, mapped_column, relationship
2from litestar import Litestar, get
3 __tablename__ = "author"
4 name: Mapped[str]
5 dob: Mapped[date]
The book entity is not considered a “strong” entity and therefore always requires an
author to be created. We need to configure our SQLAlchemy classes so that it is aware
of this relationship. We will extend the Author model by incorporating a Book
relationship. This allows each Author record to possess multiple Book records.
By configuring it this way, SQLAlchemy will automatically include the necessary foreign
key constraints when using the author_id field in each Book record.
app.py¶ 1from sqlalchemy.orm import Mapped, mapped_column, relationship
2from litestar import Litestar, get
3 __tablename__ = "author"
4 name: Mapped[str]
5 dob: Mapped[date]
6 books: Mapped[list[Book]] = relationship(back_populates="author", lazy="selectin")
7
8class Book(base.UUIDAuditBase):
9 __tablename__ = "book"
10 title: Mapped[str]
11 author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id"))
By using the audit model, we can automatically record the time a record was created and last updated.
To implement this, we will define a new Book model via the
UUIDAuditBase class. Observe
that the only modification here is the parent class from which we inherit. This minor
change endows the book table with automatic timestamp columns
(created and updated) upon deployment!
Note
If your application requires integer-based primary keys, equivalent base model
and base audit model implementations can be found at
BigIntBase and
BigIntAuditBase
respectively.
Important
Spanner only:
Using monotonically changing primary keys is considered an
anti-pattern in Spanner and leads to performance problems. Additionally, Spanner
does not currently include an idiom comparable to the Sequence object. This
means the BigIntBase and BigIntAuditBase are not currently supported for
Spanner.
Additional features provided by the built-in base models include:
Synchronous and Asynchronous repository implementations have been tried and tested with various popular database engines. As of now, six database engines are supported: Postgres, SQLite, MySQL, DuckDB, Oracle, and Spanner.
Automatic table name deduction from model name. For instance, a model named
EventLogwould correspond to theevent_logdatabase table.A
GUIDdatabase type that establishes a native UUID in supported engines or aBinary(16)as a fallback.A
BigIntegervariantBigIntIdentitythat reverts to anIntegerfor unsupported variants.A custom
JsonBtype that uses nativeJSONBwhere possible andBinaryorBlobas an alternative.A custom
EncryptedStringencrypted string that supports multiple cryptography backends.
Let’s build on this as we look at the repository classes.
Full Code¶
Full Code (click to toggle)
app.py¶ 1from __future__ import annotations
2
3import uuid
4from datetime import date
5from uuid import UUID
6
7from sqlalchemy import ForeignKey, func, select
8from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
9from sqlalchemy.orm import Mapped, mapped_column, relationship
10
11from litestar import Litestar, get
12from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin, base
13
14
15# The SQLAlchemy base includes a declarative model for you to use in your models.
16# The `UUIDBase` class includes a `UUID` based primary key (`id`)
17class Author(base.UUIDBase):
18 __tablename__ = "author"
19 name: Mapped[str]
20 dob: Mapped[date]
21 books: Mapped[list[Book]] = relationship(back_populates="author", lazy="selectin")
22
23
24# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2
25# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the
26# record created, and `updated_at` is the last time the record was modified.
27class Book(base.UUIDAuditBase):
28 __tablename__ = "book"
29 title: Mapped[str]
30 author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id"))
31 author: Mapped[Author] = relationship(lazy="joined", innerjoin=True, viewonly=True)
32
33
34session_config = AsyncSessionConfig(expire_on_commit=False)
35sqlalchemy_config = SQLAlchemyAsyncConfig(
36 connection_string="sqlite+aiosqlite:///test.sqlite", session_config=session_config, create_all=True
37) # Create 'async_session' dependency.
38
39
40async def on_startup(app: Litestar) -> None:
41 """Adds some dummy data if no data is present."""
42 async with sqlalchemy_config.get_session() as session:
43 statement = select(func.count()).select_from(Author)
44 count = await session.execute(statement)
45 if not count.scalar():
46 author_id = uuid.uuid4()
47 session.add(Author(name="Stephen King", dob=date(1954, 9, 21), id=author_id))
48 session.add(Book(title="It", author_id=author_id))
49 await session.commit()
50
51
52@get(path="/authors")
53async def get_authors(db_session: AsyncSession, db_engine: AsyncEngine) -> list[Author]:
54 """Interact with SQLAlchemy engine and session."""
55 return list(await db_session.scalars(select(Author)))
56
57
58app = Litestar(
59 route_handlers=[get_authors],
60 on_startup=[on_startup],
61 debug=True,
62 plugins=[SQLAlchemyPlugin(config=sqlalchemy_config)],
63)