initial commit
This commit is contained in:
491
docs/user-guide/database/crud.md
Normal file
491
docs/user-guide/database/crud.md
Normal file
@ -0,0 +1,491 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user