Database Transaction Handling
This guide explains how database transactions are handled in the API, particularly focusing on error handling and transaction management.
Transaction Error Handling
The API uses SQLAlchemy for database operations, and transactions are managed through the SQLAlchemy session. When a transaction fails, it enters an "aborted" state, and PostgreSQL will reject any further commands until the transaction is either committed or rolled back.
Common Transaction Errors
One common error you might encounter is:
InternalError: (psycopg.errors.InFailedSqlTransaction) current transaction is aborted, commands ignored until end of transaction block
This error occurs when:
- A previous operation in the transaction failed
- The transaction was not rolled back
- Subsequent operations are attempted within the same transaction
How We Handle Transaction Errors
To address these issues, we've implemented several layers of transaction error handling:
- Session-level error handling in
app/api/deps.py: - The
get_dbdependency catches exceptions, rolls back the transaction, and re-raises the exception -
This ensures that failed transactions don't propagate to subsequent requests
-
CRUD-level error handling in
app/crud/base.py: - Base methods like
getandget_multiinclude try-except blocks - On error, they roll back the transaction and retry the operation
-
This provides a second layer of protection for common database operations
-
Specialized error handling in
app/crud/production.py: - Methods that load relationships (like
get_full,get_with_trackers, etc.) use eager loading withjoinedload - They include try-except blocks to handle errors and retry with simpler queries if needed
-
This prevents lazy loading errors when accessing relationships
-
Coordinates conversion for PostGIS geography:
- Storage and delivery locations use PostGIS geography for location data, but the API expects a coordinates object
- We've added a
_process_locationsmethod that converts PostGIS geography to coordinates - This ensures that all location types have the correct format for serialization
-
If the conversion fails, we provide default coordinates to prevent validation errors
-
Geography vs. Geometry type mismatch:
- The database schema uses
Geographytype for location columns, but the models were incorrectly usingGeometry - This mismatch caused errors with PostGIS functions like
ST_AsEWKBwhich have different signatures for geography and geometry types - We've updated all models to use
Geographyto match the database schema -
This ensures that the correct PostGIS functions are used for the data type
-
Location History Coordinates Processing:
- The location history endpoint was returning objects without the required
coordinatesfield - We've updated the endpoint to process each location history object and add the coordinates
- The coordinates are extracted from the PostGIS geography using
ST_XandST_Yfunctions - This ensures that the API response matches the expected schema
Best Practices
When working with database operations:
- Use transactions appropriately:
- Keep transactions as short as possible
- Commit or rollback transactions promptly
-
Don't leave transactions open across multiple operations
-
Handle errors at the appropriate level:
- Use try-except blocks around database operations
- Roll back transactions when errors occur
-
Log errors for debugging
-
Consider eager loading for relationships:
- Use
joinedloadto load relationships eagerly when you know you'll need them -
This prevents lazy loading errors when accessing relationships later
-
Monitor for transaction issues:
- Watch for error logs indicating transaction failures
- Check for performance issues that might be related to transaction management
Example: Handling Transaction Errors
def get_item_with_relationships(db: Session, item_id: int):
try:
# Eager loading of relationships
from sqlalchemy.orm import joinedload
return (
db.query(Item)
.options(
joinedload(Item.related_items),
joinedload(Item.tags)
)
.filter(Item.id == item_id)
.first()
)
except Exception as e:
# Roll back the transaction and try again with a simpler query
db.rollback()
print(f"Error in get_item_with_relationships: {e}")
return db.query(Item).filter(Item.id == item_id).first()
This pattern ensures that even if the eager loading query fails, we can still retrieve the basic item data.