Database
Flask provides excellent database integration through Flask-SQLAlchemy, an extension that adds SQLAlchemy support to Flask applications.
Setup and Configuration
Installation
pip install Flask-SQLAlchemy
pip install Flask-Migrate # For migrations
Basic Configuration
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
# Database configuration
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Initialize extensions
db = SQLAlchemy(app)
migrate = Migrate(app, db)
Database URLs
# SQLite
'sqlite:///app.db'
# PostgreSQL
'postgresql://username:password@localhost/mydatabase'
# MySQL
'mysql://username:password@localhost/mydatabase'
# Environment-based configuration
import os
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
Models
Basic Model
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(120), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self):
return f'<User {self.username}>'
Column Types
class Example(db.Model):
# Numeric types
integer_col = db.Column(db.Integer)
big_integer_col = db.Column(db.BigInteger)
small_integer_col = db.Column(db.SmallInteger)
float_col = db.Column(db.Float)
numeric_col = db.Column(db.Numeric(10, 2)) # precision, scale
# String types
string_col = db.Column(db.String(100))
text_col = db.Column(db.Text)
unicode_col = db.Column(db.Unicode(100))
# Date/Time types
date_col = db.Column(db.Date)
time_col = db.Column(db.Time)
datetime_col = db.Column(db.DateTime)
# Other types
boolean_col = db.Column(db.Boolean)
binary_col = db.Column(db.LargeBinary)
json_col = db.Column(db.JSON) # PostgreSQL only
Column Constraints
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(
db.String(80),
unique=True,
nullable=False,
index=True
)
email = db.Column(
db.String(120),
unique=True,
nullable=False,
index=True
)
age = db.Column(db.Integer, db.CheckConstraint('age >= 0'))
status = db.Column(
db.String(20),
default='active',
server_default='active'
)
Relationships
One-to-Many
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), nullable=False)
posts = db.relationship('Post', backref='author', lazy=True)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
Many-to-Many
# Association table
tags = db.Table('post_tags',
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
tags = db.relationship('Tag', secondary=tags, backref='posts')
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
One-to-One
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), nullable=False)
profile = db.relationship('Profile', backref='user', uselist=False)
class Profile(db.Model):
id = db.Column(db.Integer, primary_key=True)
bio = db.Column(db.Text)
avatar = db.Column(db.String(200))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
Self-Referential Relationships
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), nullable=False)
# Manager relationship
manager_id = db.Column(db.Integer, db.ForeignKey('user.id'))
subordinates = db.relationship(
'User',
backref=db.backref('manager', remote_side=[id])
)
Database Operations
Create Database
# Create all tables
with app.app_context():
db.create_all()
# Drop all tables
with app.app_context():
db.drop_all()
CRUD Operations
Create
# Single record
user = User(username='john', email='john@example.com')
db.session.add(user)
db.session.commit()
# Multiple records
users = [
User(username='alice', email='alice@example.com'),
User(username='bob', email='bob@example.com')
]
db.session.add_all(users)
db.session.commit()
# Get the ID of newly created record
print(user.id) # Available after commit
Read
# Get by primary key
user = User.query.get(1)
user = User.query.get_or_404(1) # Raises 404 if not found
# Get first
user = User.query.first()
user = User.query.first_or_404()
# Get all
users = User.query.all()
# Filter
users = User.query.filter_by(username='john').all()
user = User.query.filter_by(username='john').first()
# Complex filters
users = User.query.filter(User.username.like('%john%')).all()
users = User.query.filter(User.age > 18).all()
Update
# Update single record
user = User.query.get(1)
user.email = 'newemail@example.com'
db.session.commit()
# Bulk update
User.query.filter_by(status='inactive').update({'status': 'active'})
db.session.commit()
Delete
# Delete single record
user = User.query.get(1)
db.session.delete(user)
db.session.commit()
# Bulk delete
User.query.filter_by(status='inactive').delete()
db.session.commit()
Querying
Basic Queries
# Filter operations
User.query.filter_by(username='john')
User.query.filter(User.username == 'john')
User.query.filter(User.age > 18)
User.query.filter(User.username.like('%john%'))
User.query.filter(User.username.ilike('%JOHN%')) # Case insensitive
# Multiple conditions
User.query.filter(User.age > 18, User.status == 'active')
User.query.filter((User.age > 18) & (User.status == 'active'))
User.query.filter((User.age < 18) | (User.age > 65))
Ordering and Limiting
# Order by
User.query.order_by(User.username).all()
User.query.order_by(User.username.desc()).all()
User.query.order_by(User.age, User.username).all()
# Limit and offset
User.query.limit(10).all()
User.query.offset(20).limit(10).all()
# Pagination
users = User.query.paginate(
page=1,
per_page=10,
error_out=False
)
Joins
# Inner join
posts = db.session.query(Post).join(User).all()
# Left join
posts = db.session.query(Post).outerjoin(User).all()
# Join with filter
posts = db.session.query(Post).join(User).filter(User.username == 'john').all()
# Multiple joins
results = db.session.query(Post).join(User).join(Category).all()
Subqueries
# Subquery
subquery = db.session.query(Post.user_id).filter(Post.published == True).subquery()
users_with_posts = User.query.filter(User.id.in_(subquery)).all()
# Exists
from sqlalchemy import exists
users = User.query.filter(
exists().where(Post.user_id == User.id)
).all()
Aggregation
from sqlalchemy import func
# Count
user_count = User.query.count()
post_count_by_user = db.session.query(
User.username,
func.count(Post.id)
).join(Post).group_by(User.id).all()
# Other aggregations
avg_age = db.session.query(func.avg(User.age)).scalar()
max_age = db.session.query(func.max(User.age)).scalar()
min_age = db.session.query(func.min(User.age)).scalar()
sum_ages = db.session.query(func.sum(User.age)).scalar()
Migrations
Flask-Migrate Setup
# Initialize migration repository
flask db init
# Create migration
flask db migrate -m "Initial migration"
# Apply migration
flask db upgrade
# Downgrade
flask db downgrade
Migration Commands
# Generate migration automatically
flask db migrate -m "Add user table"
# Create empty migration
flask db revision -m "Custom migration"
# Show current migration
flask db current
# Show migration history
flask db history
# Upgrade to specific revision
flask db upgrade <revision>
# Stamp database without running migrations
flask db stamp head
Custom Migration
# migrations/versions/xxx_custom_migration.py
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('user', sa.Column('last_login', sa.DateTime()))
op.create_index('ix_user_last_login', 'user', ['last_login'])
def downgrade():
op.drop_index('ix_user_last_login', 'user')
op.drop_column('user', 'last_login')
Session Management
Session Basics
# Manual session management
try:
user = User(username='john')
db.session.add(user)
db.session.commit()
except Exception as e:
db.session.rollback()
raise e
Context Managers
from contextlib import contextmanager
@contextmanager
def db_transaction():
try:
db.session.begin()
yield db.session
db.session.commit()
except Exception:
db.session.rollback()
raise
# Usage
with db_transaction() as session:
user = User(username='john')
session.add(user)
Raw SQL
Execute Raw SQL
# Raw query
result = db.session.execute(
'SELECT * FROM user WHERE age > :age',
{'age': 18}
)
users = result.fetchall()
# Using text()
from sqlalchemy import text
result = db.session.execute(
text('SELECT * FROM user WHERE age > :age'),
{'age': 18}
)
SQL Expressions
from sqlalchemy import and_, or_, not_
# Complex conditions
users = User.query.filter(
and_(
User.age > 18,
or_(
User.status == 'active',
User.status == 'pending'
)
)
).all()
Performance Optimization
Eager Loading
# N+1 problem solution
posts = Post.query.options(db.joinedload(Post.author)).all()
# Subquery load
posts = Post.query.options(db.subqueryload(Post.comments)).all()
# Select in load
posts = Post.query.options(db.selectinload(Post.tags)).all()
Query Optimization
# Only select specific columns
users = db.session.query(User.id, User.username).all()
# Use exists instead of count
has_posts = db.session.query(
exists().where(Post.user_id == user_id)
).scalar()
# Bulk operations
db.session.bulk_insert_mappings(User, user_data)
db.session.bulk_update_mappings(User, user_updates)
Indexing
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), index=True)
# Composite index
__table_args__ = (
db.Index('idx_name_email', 'username', 'email'),
)
Connection Pooling
Pool Configuration
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'poolclass': QueuePool,
'pool_size': 10,
'pool_recycle': 3600,
'pool_pre_ping': True
}
Error Handling
Common Exceptions
from sqlalchemy.exc import IntegrityError, DataError
try:
user = User(username='existing_user')
db.session.add(user)
db.session.commit()
except IntegrityError:
db.session.rollback()
# Handle duplicate key error
except DataError:
db.session.rollback()
# Handle data type error
Testing
Test Database Setup
import tempfile
import os
class TestConfig:
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
# Or temporary file
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'test.db')
@pytest.fixture
def app():
app = create_app(TestConfig)
with app.app_context():
db.create_all()
yield app
db.drop_all()
Database Fixtures
@pytest.fixture
def user():
user = User(username='testuser', email='test@example.com')
db.session.add(user)
db.session.commit()
return user
def test_user_creation(user):
assert user.username == 'testuser'
assert User.query.count() == 1