Transaction Management¶
PySpring Model provides declarative transaction management through the @Transactional decorator, modeled after Spring's @Transactional annotation.
Basic usage¶
All CrudRepository built-in methods are already transactional. You can also use @Transactional on your own methods:
from py_spring_model import Transactional
class UserService:
user_repository: UserRepository
@Transactional
def create_user(self, name: str, email: str) -> User:
user = User(name=name, email=email)
return self.user_repository.save(user)
When used without parameters, @Transactional defaults to Propagation.REQUIRED.
Propagation types¶
Control how transactions interact with each other by specifying a propagation type:
from py_spring_model import Transactional, Propagation
@Transactional(propagation=Propagation.REQUIRES_NEW)
def write_audit_log(self, message: str) -> None:
# Always runs in a new, independent transaction
...
REQUIRED (default)¶
Join the existing transaction if one exists. Create a new transaction if none exists.
@Transactional # or @Transactional(propagation=Propagation.REQUIRED)
def create_user(self, name: str) -> User:
...
This is the most common propagation type. Multiple @Transactional(propagation=Propagation.REQUIRED) calls in the same call chain share one transaction.
REQUIRES_NEW¶
Always create a new, independent transaction. If an existing transaction is active, it is suspended until the new transaction completes.
@Transactional(propagation=Propagation.REQUIRES_NEW)
def write_audit_log(self, message: str) -> None:
...
Use this for operations that must commit independently, such as audit logging that should persist even if the outer transaction rolls back.
SUPPORTS¶
Run within the existing transaction if one is active. If no transaction exists, run without one.
@Transactional(propagation=Propagation.SUPPORTS)
def read_user(self, id: int) -> Optional[User]:
...
Useful for read-only operations that can work with or without a transaction.
MANDATORY¶
Must run within an existing transaction. Raises TransactionRequiredError if no transaction is active.
@Transactional(propagation=Propagation.MANDATORY)
def update_balance(self, user_id: int, amount: float) -> None:
...
Use this to enforce that a method is always called within a transactional context.
NOT_SUPPORTED¶
Suspend the existing transaction (if any) and run without one.
@Transactional(propagation=Propagation.NOT_SUPPORTED)
def send_notification(self, user_id: int) -> None:
...
NEVER¶
Must not run within a transaction. Raises ExistingTransactionError if a transaction is active.
NESTED¶
Run within a savepoint if a transaction exists. If no transaction exists, create a new one (behaves like REQUIRED).
If the nested operation fails, only the savepoint is rolled back — the outer transaction can continue.
Propagation summary¶
| Propagation | Existing Tx | No Existing Tx |
|---|---|---|
REQUIRED |
Join | Create new |
REQUIRES_NEW |
Suspend, create new | Create new |
SUPPORTS |
Join | Run without |
MANDATORY |
Join | Raise error |
NOT_SUPPORTED |
Suspend, run without | Run without |
NEVER |
Raise error | Run without |
NESTED |
Create savepoint | Create new |
Transaction flow¶
sequenceDiagram
participant Client
participant ServiceA
participant ServiceB
participant Session
Client->>ServiceA: @Transactional (REQUIRED)
ServiceA->>Session: Create transaction
ServiceA->>ServiceB: @Transactional (REQUIRED)
Note over ServiceB,Session: Joins existing transaction
ServiceB->>Session: Execute query
ServiceB-->>ServiceA: Return
ServiceA->>Session: Commit (at outermost boundary)
ServiceA-->>Client: Return
Error handling¶
If an exception occurs within a @Transactional method:
- The transaction (or savepoint for
NESTED) is rolled back - The exception propagates to the caller
- For
REQUIRED, rollback only happens at the outermost transactional boundary
@Transactional
def transfer_funds(self, from_id: int, to_id: int, amount: float) -> None:
self.debit(from_id, amount)
self.credit(to_id, amount) # If this fails, debit is also rolled back
Exceptions¶
| Exception | Raised by | When |
|---|---|---|
TransactionRequiredError |
MANDATORY |
No active transaction exists |
ExistingTransactionError |
NEVER |
An active transaction exists |