Transaction Management¶
Pico-SQLAlchemy provides a robust transaction management system inspired by enterprise frameworks (like Spring Data), but adapted for Python's asyncio ecosystem.
It supports Implicit Transactions (Zero-Boilerplate) and Explicit Transactions (Fine-grained control).
1. Implicit Transactions (Zero-Boilerplate)¶
To reduce code verbosity, pico-sqlalchemy applies transactional behavior automatically based on component types.
Repositories (@repository)¶
By default, any public async method within a class decorated with @repository runs inside a Read-Write transaction.
- Propagation:
REQUIRED(Joins existing or creates new). - Mode: Read-Write.
- Use Case: Standard CRUD operations (
save,update,delete).
@repository
class UserRepository:
# Implicitly transactional (Read-Write)
async def save(self, user: User):
...
Declarative Queries (@query)¶
Methods decorated with @query are automatically wrapped in a Read-Only transaction.
- Propagation:
REQUIRED(Joins existing or creates new). - Mode: Read-Only.
- Use Case: Fetching data efficiently.
@repository(entity=User)
class UserRepository:
# Implicitly transactional (Read-Only)
@query(expr="active = true")
async def find_active(self):
...
2. Configuration Priority¶
Since multiple rules can apply to a single method (e.g., a method in a repository that also has @transactional), the library follows a strict priority order (highest wins).
| Priority | Decorator | Default Behavior | Description |
|---|---|---|---|
| 1 (High) | @transactional | User Defined | Explicit configuration always overrides implicit rules. |
| 2 | @query | read_only=True | Specific query definition implies read-only intent. |
| 3 (Low) | @repository | read_only=False | General repository methods assume write capability by default. |
Examples¶
Scenario A: Overriding Repository Default You want a complex reporting method inside a repository to be Read-Only for performance, but it doesn't use @query.
@repository
class ReportRepository:
# Default is Read-Write...
# Override to Read-Only!
@transactional(read_only=True)
async def generate_complex_stats(self):
# ... logic ...
Scenario B: Overriding Query Default Rare case where a @query (usually read-only) involves a stored procedure that writes data.
@query(sql="CALL update_stats()")
@transactional(read_only=False) # Force Read-Write
async def update_statistics(self):
...
3. The @transactional Decorator¶
Use @transactional when you need to define boundaries in your Service Layer or when you need to override defaults in Repositories.
from pico_sqlalchemy import transactional
class UserService:
@transactional(propagation="REQUIRES_NEW")
async def create_user(self, name: str):
# ...
Configuration Options¶
| Parameter | Type | Default | Description |
|---|---|---|---|
propagation | str | "REQUIRED" | Defines behavior regarding existing transactions. |
read_only | bool | False | If True, avoids explicit commit (optimization). |
isolation_level | str | None | Sets DB isolation (e.g., "SERIALIZABLE"). |
rollback_for | tuple | (Exception,) | Exception types that trigger rollback. |
no_rollback_for | tuple | () | Exception types that ignore rollback. |
4. Propagation Levels¶
The library implements strict propagation logic:
REQUIRED(Default): Joins an existing transaction or creates a new one.REQUIRES_NEW: Suspends the current transaction (if any) and starts a fresh, independent transaction.SUPPORTS: Joins an existing transaction if available; otherwise, executes without a transaction context.MANDATORY: Requires an existing transaction; raisesRuntimeErrorotherwise.NEVER: Requires no active transaction; raisesRuntimeErrorif one exists.NOT_SUPPORTED: Suspends the current transaction and executes non-transactionally.
5. Rollback Rules¶
By default, any exception (inheriting from Exception) triggers a rollback().
Customizing Rollback¶
@transactional(
rollback_for=(MyCriticalError,),
no_rollback_for=(ValidationWarning,)
)
async def business_logic():
# ...
Note: If an exception matches no_rollback_for, the library skips the explicit rollback(), but the commit() is also skipped (the session closes naturally).
6. Accessing the Session¶
Within any transactional method (Implicit or Explicit), the AsyncSession is bound to the current context.