Skip to content

CRUD methods

TIP

This is reference documentation. For typical patterns and common tasks, see the how-to guides or Getting started.

All methods are defined on TableBaseMixin and exposed via MRO to every class that inherits it. UUIDTableBaseMixin overloads get_one() / get_exist_one() to accept uuid.UUID IDs.

Common type variable: T = TypeVar('T', bound='TableBaseMixin').

add()

python
@classmethod
async def add(
    cls: type[T],
    session: AsyncSession,
    instances: T | list[T],
    refresh: bool = True,
    commit: bool = True,
) -> T | list[T]

Bulk insert new records.

ParameterDefaultDescription
instancesA single instance or a list of instances
refreshTrueAfter commit, re-fetch via cls.get() to pick up DB-generated fields
commitTrueWhen False, only flush() (no commit())

Return type: matches the input shape — single instance in, single instance out; list in, list out.

save()

python
async def save(
    self: T,
    session: AsyncSession,
    load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
    refresh: bool = True,
    commit: bool = True,
    jti_subclasses: list[type[PolymorphicBaseMixin]] | Literal['all'] | None = None,
    optimistic_retry_count: int = 0,
) -> T

INSERT or UPDATE the current instance. SQLAlchemy decides based on whether the instance is already in the session.

ParameterDefaultDescription
loadNoneRelations to eagerly load after save (single or list)
refreshTrueAfter commit, re-fetch via cls.get() (avoids MissingGreenlet)
commitTrueWhen False, only flush — useful for batched operations
jti_subclassesNoneJTI relation eager-loading option (requires load); 'all' loads every subclass
optimistic_retry_count0Number of automatic retries on optimistic-lock conflict

Raises: OptimisticLockError (after retries are exhausted).

Always use the return value

session.commit() expires every object in the session. Always write user = await user.save(session); never discard the return value.

update()

python
async def update(
    self: T,
    session: AsyncSession,
    other: SQLModelBase,
    extra_data: dict[str, Any] | None = None,
    exclude_unset: bool = True,
    exclude: set[str] | None = None,
    load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
    refresh: bool = True,
    commit: bool = True,
    jti_subclasses: list[type[PolymorphicBaseMixin]] | Literal['all'] | None = None,
    optimistic_retry_count: int = 0,
) -> T

Partial-update the current instance using fields from other (PATCH semantics).

ParameterDefaultDescription
otherModel instance carrying new data (typically XxxUpdateRequest)
extra_dataNoneExtra dict layered on top of other
exclude_unsetTrueOnly update fields that were explicitly set on other
excludeNoneExclude these fields from the update
load, refresh, commit, jti_subclasses, optimistic_retry_countSame as save()

Raises: OptimisticLockError.

delete()

python
@classmethod
async def delete(
    cls: type[T],
    session: AsyncSession,
    instances: T | list[T] | None = None,
    *,
    condition: ColumnElement[bool] | bool | None = None,
    commit: bool = True,
) -> int

Delete by instance or by condition. The two modes are mutually exclusive — exactly one of instances and condition must be provided.

ParameterDefaultDescription
instancesNoneSingle instance or list (instance mode)
conditionNoneWHERE condition (condition mode, bulk delete)
commitTrueWhether to commit

Returns: number of deleted rows (int).

Raises: ValueError (when both or neither of instances / condition are provided).

get()

The most powerful query method, with @overload declarations giving precise return types per fetch_mode literal.

python
@classmethod
async def get(
    cls: type[T],
    session: AsyncSession,
    condition: ColumnElement[bool] | bool | None = None,
    *,
    offset: int | None = None,
    limit: int | None = None,
    fetch_mode: Literal["one", "first", "all"] = "first",
    join: type[TableBaseMixin] | tuple[type[TableBaseMixin], _OnClauseArgument] | None = None,
    options: list[ExecutableOption] | None = None,
    load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
    order_by: list[ColumnElement[Any]] | None = None,
    filter: ColumnElement[bool] | bool | None = None,
    with_for_update: bool = False,
    table_view: TableViewRequest | None = None,
    jti_subclasses: list[type[PolymorphicBaseMixin]] | Literal['all'] | None = None,
    populate_existing: bool = False,
    created_before_datetime: datetime | None = None,
    created_after_datetime: datetime | None = None,
    updated_before_datetime: datetime | None = None,
    updated_after_datetime: datetime | None = None,
) -> T | list[T] | None

fetch_mode and return types

fetch_modeReturn type0 rowsMultiple rows
"first" (default)T | NoneNoneReturns the first
"one"TNoResultFoundMultipleResultsFound
"all"list[T][]All rows

Parameters

ParameterTypeMeaning
conditionColumnElement[bool]Main WHERE condition
offset / limitintPagination (explicit args take precedence over table_view)
jointype or (type, on) tupleJOIN another table
optionslist[ExecutableOption]Custom SQLAlchemy options (e.g. selectinload)
loadQueryableAttribute or listEager-load relations (auto-builds nested chains)
order_bylist[ColumnElement]Sort expressions
filterColumnElement[bool]Additional WHERE condition
with_for_updateboolSELECT ... FOR UPDATE (row lock); locked instance ID is recorded in session.info[SESSION_FOR_UPDATE_KEY]
table_viewTableViewRequestDTO bundle for pagination + sorting + time filters
jti_subclasseslist[type] | 'all'JTI polymorphic subclass loading (requires load)
populate_existingboolForce-overwrite identity-map objects
created_before/after_datetimedatetimeHalf-open time filter
updated_before/after_datetimedatetimeHalf-open time filter

Raises:

  • ValueErrorjti_subclasses provided without load
  • ValueErrorjti_subclasses used on a nested relation chain
  • ValueErrorjti_subclasses target class is not a PolymorphicBaseMixin

Polymorphic query behavior

ScenarioBehavior
JTI model (is_jti=True)Auto-uses with_polymorphic(cls, '*') to JOIN every sub-table
STI model (is_sti=True)Auto-adds WHERE _polymorphic_name IN (...) filter
with_for_update + JTIUses FOR UPDATE OF <main_table> (avoids LEFT JOIN nullable-side restrictions)

get_one()

python
@classmethod
async def get_one(
    cls: type[T],
    session: AsyncSession,
    id: int,                        # UUIDTableBaseMixin overrides to uuid.UUID
    *,
    load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
    with_for_update: bool = False,
) -> T

Shortcut for get(cls.id == id, fetch_mode='one').

Raises: NoResultFound (record not found), MultipleResultsFound (multiple records — should never happen for unique IDs).

get_exist_one()

python
@classmethod
async def get_exist_one(
    cls: type[T],
    session: AsyncSession,
    id: int,                        # UUIDTableBaseMixin overrides to uuid.UUID
    load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
) -> T

Like get_one(), but the not-found exception is friendlier:

EnvironmentException
FastAPI installedHTTPException(status_code=404, detail="Not found")
FastAPI not installedRecordNotFoundError

The decision is made at module import time and cached as _HAS_FASTAPI.

count()

python
@classmethod
async def count(
    cls: type[T],
    session: AsyncSession,
    condition: ColumnElement[bool] | bool | None = None,
    *,
    created_before_datetime: datetime | None = None,
    created_after_datetime: datetime | None = None,
    updated_before_datetime: datetime | None = None,
    updated_after_datetime: datetime | None = None,
) -> int

Returns the number of records matching the condition. Backed by SELECT COUNT(*).

get_with_count()

python
@classmethod
async def get_with_count(
    cls: type[T],
    session: AsyncSession,
    condition: ColumnElement[bool] | bool | None = None,
    *,
    table_view: TableViewRequest | None = None,
    # ... all the same parameters as get()
) -> ListResponse[T]

Combination of count() + get(fetch_mode="all"), returning ListResponse[T]. Typically used by LIST endpoints.

Method cheat sheet

MethodTypeEquivalent SQLReturns
add()@classmethodINSERTT or list[T]
save()instance methodINSERT or UPDATErefreshed T
update()instance methodUPDATE (PATCH)refreshed T
delete()@classmethodDELETEint (rows deleted)
get()@classmethodSELECT ... WHERE ...T | list[T] | None
get_one()@classmethodSELECT WHERE id = ?T
get_exist_one()@classmethodSELECT WHERE id = ? + 404T
count()@classmethodSELECT COUNT(*)int
get_with_count()@classmethodCOUNT + SELECTListResponse[T]