Files
fastapi-toolsets/docs/module/crud.md
d3vyce 6714ceeb92 chore: documentation (#76)
* chore: update docstring example to use python code block

* docs: add documentation

* feat: add docs build + fix other workdlows

* fix: add missing return type
2026-02-19 16:43:38 +01:00

3.2 KiB

CRUD

Generic async CRUD operations for SQLAlchemy models with search, pagination, and many-to-many support.

Overview

The crud module provides AsyncCrud, an abstract base class with a full suite of async database operations, and CrudFactory, a convenience function to instantiate it for a given model.

Creating a CRUD class

from fastapi_toolsets.crud import CrudFactory
from myapp.models import User

UserCrud = CrudFactory(
    User,
    searchable_fields=[User.username, User.email],
)

CrudFactory dynamically creates a class named AsyncUserCrud with User as its model.

Basic operations

# Create
user = await UserCrud.create(session, obj=UserCreateSchema(username="alice"))

# Get one (raises NotFoundError if not found)
user = await UserCrud.get(session, filters=[User.id == user_id])

# Get first or None
user = await UserCrud.first(session, filters=[User.email == email])

# Get multiple
users = await UserCrud.get_multi(session, filters=[User.is_active == True])

# Update
user = await UserCrud.update(session, obj=UserUpdateSchema(username="bob"), filters=[User.id == user_id])

# Delete
await UserCrud.delete(session, filters=[User.id == user_id])

# Count / exists
count = await UserCrud.count(session, filters=[User.is_active == True])
exists = await UserCrud.exists(session, filters=[User.email == email])

Pagination

result = await UserCrud.paginate(
    session,
    filters=[User.is_active == True],
    order_by=[User.created_at.desc()],
    page=1,
    items_per_page=20,
    search="alice",
    search_fields=[User.username, User.email],
)
# result.data: list of users
# result.pagination: Pagination(total_count, items_per_page, page, has_more)

Declare searchable fields on the CRUD class. Relationship traversal is supported via tuples:

PostCrud = CrudFactory(
    Post,
    searchable_fields=[
        Post.title,
        Post.content,
        (Post.author, User.username),  # search across relationship
    ],
)

Many-to-many relationships

Use m2m_fields to map schema fields containing lists of IDs to SQLAlchemy relationships. The CRUD class resolves and validates all IDs before persisting:

PostCrud = CrudFactory(
    Post,
    m2m_fields={"tag_ids": Post.tags},
)

# schema: PostCreateSchema(title="Hello", tag_ids=[1, 2, 3])
post = await PostCrud.create(session, obj=PostCreateSchema(...))

Upsert

Atomic INSERT ... ON CONFLICT DO UPDATE using PostgreSQL:

await UserCrud.upsert(
    session,
    obj=UserCreateSchema(email="alice@example.com", username="alice"),
    index_elements=[User.email],
    set_={"username"},
)

as_response

Pass as_response=True to any write operation to get a Response[ModelType] back directly:

response = await UserCrud.create(session, obj=schema, as_response=True)
# response: Response[User]

:material-api: API Reference