Aptiwise
Aptiwise
Aptiwise DocumentationForm Layout Feature - Implementation CompleteForm Layout Feature Implementation Guide
Getting Started
User Guide
Development GuideProduction DeploymentProject StructureTesting Guide
API Development
Authentication & Security
Background Tasks
Caching
Configuration
Database Layer
CRUD OperationsDatabase MigrationsDatabase ModelsDatabase Schemas
Rate Limiting
Workflow Types & Patterns
User GuideDatabase Layer

CRUD Operations

CRUD Operations

This guide covers all CRUD (Create, Read, Update, Delete) operations available in the FastAPI Boilerplate using FastCRUD, a powerful library that provides consistent and efficient database operations.

Overview

The boilerplate uses FastCRUD for all database operations. FastCRUD provides:

  • Consistent API across all models
  • Type safety with generic type parameters
  • Automatic pagination support
  • Advanced filtering and joining capabilities
  • Soft delete support
  • Optimized queries with selective field loading

CRUD Class Structure

Each model has a corresponding CRUD class that defines the available operations:

# src/app/crud/crud_users.py
from fastcrud import FastCRUD
from app.models.user import User
from app.schemas.user import UserCreateInternal, UserUpdate, UserUpdateInternal, UserDelete, UserRead

CRUDUser = FastCRUD[
    User,  # Model class
    UserCreateInternal,  # Create schema
    UserUpdate,  # Update schema
    UserUpdateInternal,  # Internal update schema
    UserDelete,  # Delete schema
    UserRead,  # Read schema
]
crud_users = CRUDUser(User)

Read Operations

Get Single Record

Retrieve a single record by any field:

# Get user by ID
user = await crud_users.get(db=db, id=user_id)

# Get user by username
user = await crud_users.get(db=db, username="john_doe")

# Get user by email
user = await crud_users.get(db=db, email="john@example.com")

# Get with specific fields only
user = await crud_users.get(
    db=db,
    schema_to_select=UserRead,  # Only select fields defined in UserRead
    id=user_id,
)

Real usage from the codebase:

# From src/app/services/v1/users.py
db_user = await crud_users.get(
    db=db,
    schema_to_select=UserRead,
    username=username,
    is_deleted=False,
)

Get Multiple Records

Retrieve multiple records with filtering and pagination:

# Get all users
users = await crud_users.get_multi(db=db)

# Get with pagination
users = await crud_users.get_multi(
    db=db,
    offset=0,  # Skip first 0 records
    limit=10,  # Return maximum 10 records
)

# Get with filtering
active_users = await crud_users.get_multi(
    db=db, is_deleted=False, offset=compute_offset(page, items_per_page), limit=items_per_page  # Filter condition
)

Pagination response structure:

{
    "data": [
        {"id": 1, "username": "john", "email": "john@example.com"},
        {"id": 2, "username": "jane", "email": "jane@example.com"},
    ],
    "total_count": 25,
    "has_more": true,
    "page": 1,
    "items_per_page": 10,
}

Check Existence

Check if a record exists without fetching it:

# Check if user exists
user_exists = await crud_users.exists(db=db, email="john@example.com")
# Returns True or False

# Check if username is available
username_taken = await crud_users.exists(db=db, username="john_doe")

Real usage example:

# From src/app/services/v1/users.py - checking before creating
email_row = await crud_users.exists(db=db, email=user.email)
if email_row:
    raise DuplicateValueException("Email is already registered")

Count Records

Get count of records matching criteria:

# Count all users
total_users = await crud_users.count(db=db)

# Count active users
active_count = await crud_users.count(db=db, is_deleted=False)

# Count by specific criteria
admin_count = await crud_users.count(db=db, is_superuser=True)

Create Operations

Basic Creation

Create new records using Pydantic schemas:

# Create user
user_data = UserCreateInternal(username="john_doe", email="john@example.com", hashed_password="hashed_password_here")

created_user = await crud_users.create(db=db, object=user_data)

Real creation example:

# From src/app/services/v1/users.py
user_internal_dict = user.model_dump()
user_internal_dict["hashed_password"] = get_password_hash(password=user_internal_dict["password"])
del user_internal_dict["password"]

user_internal = UserCreateInternal(**user_internal_dict)
created_user = await crud_users.create(db=db, object=user_internal)

Create with Relationships

When creating records with foreign keys:

# Create post for a user
post_data = PostCreateInternal(
    title="My First Post", content="This is the content of my post", created_by_user_id=user.id  # Foreign key reference
)

created_post = await crud_posts.create(db=db, object=post_data)

Update Operations

Basic Updates

Update records by any field:

# Update user by ID
update_data = UserUpdate(email="newemail@example.com")
await crud_users.update(db=db, object=update_data, id=user_id)

# Update by username
await crud_users.update(db=db, object=update_data, username="john_doe")

# Update multiple fields
update_data = UserUpdate(email="newemail@example.com", profile_image_url="https://newimage.com/photo.jpg")
await crud_users.update(db=db, object=update_data, id=user_id)

Conditional Updates

Update with validation:

# From real endpoint - check before updating
if values.username != db_user.username:
    existing_username = await crud_users.exists(db=db, username=values.username)
    if existing_username:
        raise DuplicateValueException("Username not available")

await crud_users.update(db=db, object=values, username=username)

Bulk Updates

Update multiple records at once:

# Update all users with specific criteria
update_data = {"is_active": False}
await crud_users.update(db=db, object=update_data, is_deleted=True)

Delete Operations

Soft Delete

For models with soft delete fields (like User, Post):

# Soft delete - sets is_deleted=True, deleted_at=now()
await crud_users.delete(db=db, username="john_doe")

# The record stays in the database but is marked as deleted
user = await crud_users.get(db=db, username="john_doe", is_deleted=True)

Hard Delete

Permanently remove records from the database:

# Permanently delete from database
await crud_users.db_delete(db=db, username="john_doe")

# The record is completely removed

Real deletion example:

# From src/app/services/v1/users.py
# Regular users get soft delete
await crud_users.delete(db=db, username=username)

# Superusers can hard delete
await crud_users.db_delete(db=db, username=username)

Advanced Operations

Joined Queries

Get data from multiple related tables:

# Get posts with user information
posts_with_users = await crud_posts.get_multi_joined(
    db=db,
    join_model=User,
    join_on=Post.created_by_user_id == User.id,
    schema_to_select=PostRead,
    join_schema_to_select=UserRead,
    join_prefix="user_",
)

Result structure:

{
    "id": 1,
    "title": "My Post",
    "content": "Post content",
    "user_id": 123,
    "user_username": "john_doe",
    "user_email": "john@example.com",
}

Custom Filtering

Advanced filtering with SQLAlchemy expressions:

from sqlalchemy import and_, or_

# Complex filters
users = await crud_users.get_multi(
    db=db, filter_criteria=[and_(User.is_deleted == False, User.created_at > datetime(2024, 1, 1))]
)

Optimized Field Selection

Select only needed fields for better performance:

# Only select id and username
users = await crud_users.get_multi(db=db, schema_to_select=UserRead, limit=100)  # Use schema to define fields

# Or specify fields directly
users = await crud_users.get_multi(db=db, schema_to_select=["id", "username", "email"], limit=100)

Practical Examples

Complete CRUD Workflow

Here's a complete example showing all CRUD operations:

from sqlalchemy.ext.asyncio import AsyncSession
from app.crud.crud_users import crud_users
from app.schemas.user import UserCreateInternal, UserUpdate, UserRead


async def user_management_example(db: AsyncSession):
    # 1. CREATE
    user_data = UserCreateInternal(username="demo_user", email="demo@example.com", hashed_password="hashed_password")
    new_user = await crud_users.create(db=db, object=user_data)
    print(f"Created user: {new_user.id}")

    # 2. READ
    user = await crud_users.get(db=db, id=new_user.id, schema_to_select=UserRead)
    print(f"Retrieved user: {user.username}")

    # 3. UPDATE
    update_data = UserUpdate(email="updated@example.com")
    await crud_users.update(db=db, object=update_data, id=new_user.id)
    print("User updated")

    # 4. DELETE (soft delete)
    await crud_users.delete(db=db, id=new_user.id)
    print("User soft deleted")

    # 5. VERIFY DELETION
    deleted_user = await crud_users.get(db=db, id=new_user.id, is_deleted=True)
    print(f"User deleted at: {deleted_user.deleted_at}")

Pagination Helper

Using FastCRUD's pagination utilities:

from fastcrud.paginated import compute_offset, paginated_response


async def get_paginated_users(db: AsyncSession, page: int = 1, items_per_page: int = 10):
    users_data = await crud_users.get_multi(
        db=db,
        offset=compute_offset(page, items_per_page),
        limit=items_per_page,
        is_deleted=False,
        schema_to_select=UserRead,
    )

    return paginated_response(crud_data=users_data, page=page, items_per_page=items_per_page)

Error Handling

Proper error handling with CRUD operations:

from app.core.exceptions.http_exceptions import NotFoundException, DuplicateValueException


async def safe_user_creation(db: AsyncSession, user_data: UserCreate):
    # Check for duplicates
    if await crud_users.exists(db=db, email=user_data.email):
        raise DuplicateValueException("Email already registered")

    if await crud_users.exists(db=db, username=user_data.username):
        raise DuplicateValueException("Username not available")

    # Create user
    try:
        user_internal = UserCreateInternal(**user_data.model_dump())
        created_user = await crud_users.create(db=db, object=user_internal)
        return created_user
    except Exception as e:
        # Handle database errors
        await db.rollback()
        raise e

Performance Tips

1. Use Schema Selection

Always specify schema_to_select to avoid loading unnecessary data:

# Good - only loads needed fields
user = await crud_users.get(db=db, id=user_id, schema_to_select=UserRead)

# Avoid - loads all fields
user = await crud_users.get(db=db, id=user_id)

2. Batch Operations

For multiple operations, use transactions:

async def batch_user_updates(db: AsyncSession, updates: List[dict]):
    try:
        for update in updates:
            await crud_users.update(db=db, object=update["data"], id=update["id"])
        await db.commit()
    except Exception:
        await db.rollback()
        raise

3. Use Exists for Checks

Use exists() instead of get() when you only need to check existence:

# Good - faster, doesn't load data
if await crud_users.exists(db=db, email=email):
    raise DuplicateValueException("Email taken")

# Avoid - slower, loads unnecessary data
user = await crud_users.get(db=db, email=email)
if user:
    raise DuplicateValueException("Email taken")

Next Steps

  • Database Migrations - Managing database schema changes
  • API Development - Using CRUD in API endpoints
  • Caching - Optimizing CRUD with caching

Database Layer

Previous Page

Database Migrations

Next Page

On this page

CRUD OperationsOverviewCRUD Class StructureRead OperationsGet Single RecordGet Multiple RecordsCheck ExistenceCount RecordsCreate OperationsBasic CreationCreate with RelationshipsUpdate OperationsBasic UpdatesConditional UpdatesBulk UpdatesDelete OperationsSoft DeleteHard DeleteAdvanced OperationsJoined QueriesCustom FilteringOptimized Field SelectionPractical ExamplesComplete CRUD WorkflowPagination HelperError HandlingPerformance Tips1. Use Schema Selection2. Batch Operations3. Use Exists for ChecksNext Steps