Flask-SQLAlchemy Patterns — Core Concepts
How Flask-SQLAlchemy manages sessions
The most important thing Flask-SQLAlchemy does is manage database sessions automatically. A session tracks changes to your objects and writes them to the database when you commit.
Flask-SQLAlchemy creates a scoped session tied to the request lifecycle. When a request starts, you get a fresh session. When the request ends, the session is automatically removed — committing pending changes or rolling back on errors.
This means you rarely create sessions manually. You use db.session and trust the framework to handle lifecycle. But understanding what happens underneath prevents a whole category of bugs.
The N+1 query problem
The most common performance killer in ORM code. Consider this:
users = User.query.all() # 1 query: SELECT * FROM users
for user in users:
print(user.posts) # N queries: SELECT * FROM posts WHERE user_id = ?
For 100 users, that’s 101 database queries. The fix is eager loading — telling SQLAlchemy to fetch related data upfront:
users = User.query.options(joinedload(User.posts)).all() # 1 query with JOIN
Three loading strategies exist:
- joinedload — Uses a SQL JOIN, fetching everything in one query. Best for one-to-one or small one-to-many relationships.
- subqueryload — Runs a second query with a subquery. Better for large collections where JOINs would create too many duplicated rows.
- selectinload — Runs a second query with
WHERE id IN (...). Usually the best default for collections.
Query composition
Instead of writing complex queries inline, compose them from reusable pieces:
class User(db.Model):
# ...
@classmethod
def active(cls):
return cls.query.filter_by(is_active=True)
@classmethod
def created_after(cls, date):
return cls.query.filter(cls.created_at > date)
These methods return query objects that can be chained further. The database only executes when you call .all(), .first(), or iterate.
Session patterns that prevent bugs
Always use db.session.add() explicitly for new objects
user = User(name='Alice')
db.session.add(user)
db.session.commit()
Relying on cascade adds from relationships works but makes code harder to follow. Explicit adds make the intent clear.
Handle rollback on errors
try:
db.session.add(user)
db.session.commit()
except IntegrityError:
db.session.rollback()
raise
If an error occurs during commit and you don’t roll back, the session enters a broken state. All subsequent queries in that request will fail.
Use db.session.get() for primary key lookups
# Good: uses identity map, skips DB if already loaded
user = db.session.get(User, 42)
# Slower: always generates a query
user = User.query.filter_by(id=42).first()
db.session.get() checks the session’s identity map first. If the object was already loaded in this request, it returns it from memory.
Relationship configuration
Relationships define how models connect. The key decisions are loading strategy and cascade behavior:
class User(db.Model):
posts = db.relationship('Post',
back_populates='author',
lazy='select', # Default: load on access
cascade='all, delete-orphan' # Delete posts when user is deleted
)
The lazy parameter sets the default loading strategy. Override it per-query with .options() rather than changing the default, because different views need different strategies.
Common misconception
“The ORM is slow, raw SQL is always faster.” The ORM adds overhead per query (typically microseconds), but the real performance difference comes from which queries run. An ORM with proper eager loading runs 2 queries where naive code runs 101. The ORM’s overhead per query is irrelevant compared to eliminating 99 unnecessary queries.
When you do need raw SQL for truly complex queries, SQLAlchemy supports it: db.session.execute(text("SELECT ...")).
Pagination
Flask-SQLAlchemy provides built-in pagination:
page = request.args.get('page', 1, type=int)
users = User.query.paginate(page=page, per_page=20)
The paginate object contains .items (the results), .pages (total page count), .has_next, and .has_prev. This generates proper LIMIT/OFFSET SQL.
For large tables, offset-based pagination gets slow. Keyset pagination (filtering by the last seen ID) performs better but requires ordered results.
One thing to remember: Most Flask-SQLAlchemy performance problems come from invisible queries — the N+1 problem, unnecessary lazy loads, and missing indexes. Use Flask-SQLAlchemy’s query recording (SQLALCHEMY_RECORD_QUERIES) during development to see every query each request generates.
See Also
- Python Django Admin Get an intuitive feel for Django Admin so Python behavior stops feeling unpredictable.
- Python Django Basics Get an intuitive feel for Django Basics so Python behavior stops feeling unpredictable.
- Python Django Celery Integration Why your Django app needs a helper to handle slow jobs in the background.
- Python Django Channels Websockets How Django can send real-time updates to your browser without you refreshing the page.
- Python Django Custom Management Commands How to teach Django new tricks by creating your own command-line shortcuts.