Flask Transactions
Database transactions are a fundamental concept in ensuring data integrity within your Flask applications. In this tutorial, you'll learn how to implement and manage transactions to create robust database operations that maintain consistency even when errors occur.
What Are Database Transactions?
A transaction is a sequence of database operations that are treated as a single logical unit of work. For a transaction to be complete, all operations within it must succeed; if any operation fails, the entire transaction is rolled back, leaving the database in its original state.
Transactions follow the ACID properties:
- Atomicity: All operations complete successfully or none of them do
- Consistency: The database remains in a consistent state before and after the transaction
- Isolation: Concurrent transactions don't interfere with each other
- Durability: Once a transaction is committed, the changes are permanent
Implementing Transactions in Flask with SQLAlchemy
Flask applications typically use SQLAlchemy as an ORM (Object Relational Mapper) to interact with databases. SQLAlchemy provides built-in support for transactions.
Basic Transaction Example
Let's start with a simple example of transferring money between two accounts:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.exc import SQLAlchemyError
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///banking.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class Account(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
balance = db.Column(db.Float, default=0.0)
@app.route('/transfer/<int:from_id>/<int:to_id>/<float:amount>')
def transfer_money(from_id, to_id, amount):
try:
# Start a transaction
from_account = Account.query.get_or_404(from_id)
to_account = Account.query.get_or_404(to_id)
# Check if sender has sufficient balance
if from_account.balance < amount:
return "Insufficient funds", 400
# Perform the transfer
from_account.balance -= amount
to_account.balance += amount
# Commit the transaction
db.session.commit()
return f"Successfully transferred ${amount}"
except SQLAlchemyError as e:
# Roll back the transaction on error
db.session.rollback()
return f"Transaction failed: {str(e)}", 500
This example demonstrates a basic transaction that transfers money from one account to another. If any part fails, the rollback()
method reverts all changes.
Using Context Managers for Transactions
In more complex scenarios, you might want to use explicit transaction management with context managers:
from contextlib import contextmanager
@contextmanager
def transaction(session):
try:
yield session
session.commit()
except:
session.rollback()
raise
@app.route('/create-accounts')
def create_accounts():
try:
with transaction(db.session):
# Create multiple accounts in a single transaction
account1 = Account(name='Alice', balance=1000.0)
account2 = Account(name='Bob', balance=500.0)
db.session.add(account1)
db.session.add(account2)
# If no exceptions occur, the transaction is committed automatically
return "Accounts created successfully"
except Exception as e:
return f"Failed to create accounts: {str(e)}", 500
This approach provides a clean way to handle transactions and ensures proper cleanup using Python's context manager protocol.
Nested Transactions
SQLAlchemy supports nested transactions via the savepoint
feature. This allows you to create checkpoints within a larger transaction:
@app.route('/complex-operation')
def complex_operation():
# Start the outer transaction
outer_transaction = db.session.begin()
try:
# Create a new user
new_user = User(username="johndoe", email="[email protected]")
db.session.add(new_user)
# Create a nested transaction (savepoint)
nested = db.session.begin_nested()
try:
# These operations can be rolled back independently
profile = Profile(user_id=new_user.id, bio="A new user")
db.session.add(profile)
if some_condition_fails():
# Only rolls back to the savepoint
nested.rollback()
else:
# Commits the savepoint
nested.commit()
except:
# Only rolls back to the savepoint
nested.rollback()
# More operations in the outer transaction
log_entry = Log(action="User created")
db.session.add(log_entry)
# Commit the entire transaction
outer_transaction.commit()
return "Complex operation completed"
except:
# Rolls back the entire transaction
outer_transaction.rollback()
return "Complex operation failed", 500
Real-World Example: Order Processing System
Let's build a more practical example of an e-commerce order processing system:
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
price = db.Column(db.Float, nullable=False)
stock = db.Column(db.Integer, default=0)
class Order(db.Model):
id = db.Column(db.Integer, primary_key=True)
customer_id = db.Column(db.Integer, nullable=False)
status = db.Column(db.String(50), default="pending")
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class OrderItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
order_id = db.Column(db.Integer, db.ForeignKey('order.id'), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False)
quantity = db.Column(db.Integer, nullable=False)
price = db.Column(db.Float, nullable=False)
@app.route('/place-order', methods=['POST'])
def place_order():
data = request.json
customer_id = data.get('customer_id')
items = data.get('items', []) # List of {product_id, quantity}
if not items:
return "No items in order", 400
try:
# Start transaction
order = Order(customer_id=customer_id)
db.session.add(order)
# We need to flush to get the order ID before creating order items
db.session.flush()
total_amount = 0
for item in items:
product_id = item['product_id']
quantity = item['quantity']
# Get product and check stock
product = Product.query.get(product_id)
if not product:
raise ValueError(f"Product {product_id} not found")
if product.stock < quantity:
raise ValueError(f"Insufficient stock for product {product.name}")
# Update stock
product.stock -= quantity
# Create order item
order_item = OrderItem(
order_id=order.id,
product_id=product_id,
quantity=quantity,
price=product.price
)
db.session.add(order_item)
total_amount += product.price * quantity
# Commit the transaction
db.session.commit()
return {
"order_id": order.id,
"total_amount": total_amount,
"status": "success"
}
except Exception as e:
db.session.rollback()
return {"status": "error", "message": str(e)}, 500
This example showcases a complete order processing system that:
- Creates an order record
- Checks product availability
- Updates inventory
- Creates order items
- All in a single atomic transaction
Best Practices for Flask Transactions
-
Keep transactions short: Long-running transactions can lead to database locks and performance issues.
-
Error handling: Always include proper exception handling with rollbacks.
-
Use context managers: They help ensure transactions are properly committed or rolled back.
-
Avoid nested transactions when possible: While they're supported, they add complexity.
-
Consider using session factories: For more complex applications, you might want separate session scopes.
# Session factory approach
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
engine = create_engine('sqlite:///app.db')
Session = scoped_session(sessionmaker(bind=engine))
def some_operation():
session = Session()
try:
# Use session for database operations
session.commit()
except:
session.rollback()
raise
finally:
session.close()
- Use explicit transaction boundaries for clarity:
def complex_operation():
session = db.session
# Start transaction explicitly
transaction = session.begin()
try:
# Database operations
session.commit()
return "Success"
except Exception as e:
transaction.rollback()
return f"Error: {str(e)}"
Summary
Flask transactions are essential for maintaining data integrity in your applications. By following ACID principles and implementing proper transaction management, you can ensure that database operations are reliable and consistent, even in the face of errors or concurrent access.
Key points to remember:
- Transactions ensure all-or-nothing operations
- SQLAlchemy provides powerful transaction management tools
- Always include error handling with rollbacks
- Keep transactions as short as possible for best performance
- Use context managers for clean transaction management
Additional Resources
Exercises
- Implement a banking system with multiple account types and transfer functionality.
- Create a blogging platform where publishing a post involves updating multiple tables.
- Build an inventory management system with transaction support for receiving and shipping goods.
- Implement a user registration system that creates user records and associated profile data in a transaction.
- Add transaction logging to track all changes made during database transactions for auditing purposes.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)