Skip to main content

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:

python
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:

python
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:

python
@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:

python
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:

  1. Creates an order record
  2. Checks product availability
  3. Updates inventory
  4. Creates order items
  5. All in a single atomic transaction

Best Practices for Flask Transactions

  1. Keep transactions short: Long-running transactions can lead to database locks and performance issues.

  2. Error handling: Always include proper exception handling with rollbacks.

  3. Use context managers: They help ensure transactions are properly committed or rolled back.

  4. Avoid nested transactions when possible: While they're supported, they add complexity.

  5. Consider using session factories: For more complex applications, you might want separate session scopes.

python
# 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()
  1. Use explicit transaction boundaries for clarity:
python
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

  1. Implement a banking system with multiple account types and transfer functionality.
  2. Create a blogging platform where publishing a post involves updating multiple tables.
  3. Build an inventory management system with transaction support for receiving and shipping goods.
  4. Implement a user registration system that creates user records and associated profile data in a transaction.
  5. 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! :)