491 lines
12 KiB
Markdown
491 lines
12 KiB
Markdown
# 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](https://github.com/igorbenav/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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:**
|
|
|
|
```python
|
|
# From src/app/api/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:
|
|
|
|
```python
|
|
# 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, # Filter condition
|
|
offset=compute_offset(page, items_per_page),
|
|
limit=items_per_page
|
|
)
|
|
```
|
|
|
|
**Pagination response structure:**
|
|
|
|
```python
|
|
{
|
|
"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:
|
|
|
|
```python
|
|
# 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:**
|
|
|
|
```python
|
|
# From src/app/api/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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:**
|
|
|
|
```python
|
|
# From src/app/api/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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# 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):
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
# Permanently delete from database
|
|
await crud_users.db_delete(db=db, username="john_doe")
|
|
|
|
# The record is completely removed
|
|
```
|
|
|
|
**Real deletion example:**
|
|
|
|
```python
|
|
# From src/app/api/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:
|
|
|
|
```python
|
|
# 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:
|
|
```python
|
|
{
|
|
"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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# Only select id and username
|
|
users = await crud_users.get_multi(
|
|
db=db,
|
|
schema_to_select=UserRead, # Use schema to define fields
|
|
limit=100
|
|
)
|
|
|
|
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
# 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](migrations.md)** - Managing database schema changes
|
|
- **[API Development](../api/index.md)** - Using CRUD in API endpoints
|
|
- **[Caching](../caching/index.md)** - Optimizing CRUD with caching |