SQLAlchemy Query API Guide
This guide provides comprehensive examples of how to implement the modern SQLAlchemy 2.x query API in the NOW Learning Management System.
Table of Contents
- Basic Imports
- SELECT Operations
- UPDATE Operations
- DELETE Operations
- Database Session Operations
- Advanced Patterns
- Migration Patterns
Basic Imports
Always ensure you have the necessary imports for modern SQLAlchemy operations:
from sqlalchemy import func, select, delete, update
from now_lms.db import database, select # select is pre-imported from db module
SELECT Operations
Basic Select
# Single record
user = database.session.execute(select(Usuario).filter_by(usuario='john_doe')).scalars().first()
# All records
users = database.session.execute(select(Usuario)).scalars().all()
# With filtering
active_users = database.session.execute(
select(Usuario).filter(Usuario.activo == True)
).scalars().all()
Select with Order By
# Ordered results
courses = database.session.execute(
select(Curso).order_by(Curso.nombre.asc())
).scalars().all()
# Multiple order criteria
sections = database.session.execute(
select(CursoSeccion)
.filter_by(curso=course_code)
.order_by(CursoSeccion.indice, CursoSeccion.nombre)
).scalars().all()
Select with Joins
# Inner join
evaluations = database.session.execute(
select(Evaluation)
.join(CursoSeccion)
.filter(CursoSeccion.curso == course_code)
).scalars().all()
# Left outer join
results = database.session.execute(
select(Curso)
.outerjoin(DocenteCurso)
.filter(DocenteCurso.usuario == user_id)
).scalars().all()
Select Specific Columns
# Select only specific columns (not all columns from table)
course_names = database.session.execute(
select(Curso.codigo, Curso.nombre)
.filter(Curso.publico == True)
).all()
# With functions
user_count = database.session.execute(
select(func.count(Usuario.id))
.filter(Usuario.activo == True)
).scalar()
# Group by with aggregation
course_stats = database.session.execute(
select(Curso.categoria, func.count(Curso.id))
.group_by(Curso.categoria)
).all()
Select First or 404 Pattern
# Modern replacement for first_or_404()
from flask import abort
post = database.session.execute(
select(BlogPost).filter(BlogPost.slug == slug)
).scalars().first()
if not post:
abort(404)
Count Operations
# Count records
total_courses = database.session.execute(
select(func.count(Curso.id))
.filter(Curso.publico == True)
).scalar()
# Count with conditions
active_students = database.session.execute(
select(func.count(EstudianteCurso.id))
.filter(EstudianteCurso.vigente == True)
).scalar()
Pagination
# Modern pagination (using database.paginate instead of query().paginate())
posts = database.paginate(
select(BlogPost).filter(BlogPost.published == True).order_by(BlogPost.fecha.desc()),
page=page,
per_page=per_page,
error_out=False
)
Subqueries and EXISTS
# Subquery
subquery = select(DocenteCurso.curso).filter(DocenteCurso.usuario == user_id)
instructor_courses = database.session.execute(
select(Curso).filter(Curso.codigo.in_(subquery))
).scalars().all()
# EXISTS
from sqlalchemy import exists
has_course = database.session.execute(
select(exists().where(
EstudianteCurso.usuario == user_id,
EstudianteCurso.curso == course_code
))
).scalar()
UPDATE Operations
Basic Update
# Update single record
database.session.execute(
update(Usuario)
.where(Usuario.usuario == user_id)
.values(ultimo_acceso=datetime.now())
)
database.session.commit()
# Update multiple records
database.session.execute(
update(Curso)
.where(Curso.categoria == old_category)
.values(categoria=new_category)
)
database.session.commit()
Update with Conditions
# Conditional update
database.session.execute(
update(CursoSeccion)
.where(
CursoSeccion.curso == course_code,
CursoSeccion.publico == False
)
.values(publico=True)
)
database.session.commit()
Update Using ORM Objects
# For single record updates, you can still use ORM approach
user = database.session.execute(
select(Usuario).filter_by(usuario=user_id)
).scalars().first()
if user:
user.ultimo_acceso = datetime.now()
database.session.commit()
DELETE Operations
Basic Delete
# Delete records
database.session.execute(
delete(Categoria).where(Categoria.id == category_id)
)
database.session.commit()
# Delete with multiple conditions
database.session.execute(
delete(EstudianteCurso).where(
EstudianteCurso.usuario == user_id,
EstudianteCurso.vigente == False
)
)
database.session.commit()
Delete with Joins (using subquery)
# Delete related records
subquery = select(CursoSeccion.id).filter(CursoSeccion.curso == course_code)
database.session.execute(
delete(CursoRecurso).where(CursoRecurso.seccion.in_(subquery))
)
database.session.commit()
Database Session Operations
Core Session Methods
# Add new record
new_user = Usuario(usuario='new_user', email='user@example.com')
database.session.add(new_user)
database.session.commit()
# Add multiple records
database.session.add_all([user1, user2, user3])
database.session.commit()
# Flush (write to database but don't commit transaction)
database.session.add(new_course)
database.session.flush() # Assigns ID but doesn't commit
# new_course.id is now available
# Commit transaction
database.session.commit()
# Rollback transaction
try:
database.session.add(new_record)
database.session.commit()
except Exception:
database.session.rollback()
raise
Database Schema Operations
# Drop all tables (development/testing only)
database.drop_all()
# Create all tables
database.create_all()
# Reflect existing database
database.reflect()
Session State Management
# Remove object from session
database.session.expunge(user_object)
# Refresh object from database
database.session.refresh(user_object)
# Merge detached object
merged_user = database.session.merge(detached_user)
database.session.commit()
# Check if object is in session
if user in database.session:
print("User is in session")
Advanced Patterns
Complex Filtering
# Multiple OR conditions
from sqlalchemy import or_
courses = database.session.execute(
select(Curso).filter(
or_(
Curso.categoria == 'programming',
Curso.categoria == 'web-development'
)
)
).scalars().all()
# IN clause
valid_statuses = ['active', 'pending', 'trial']
users = database.session.execute(
select(Usuario).filter(Usuario.status.in_(valid_statuses))
).scalars().all()
# LIKE patterns
search_results = database.session.execute(
select(Curso).filter(Curso.nombre.like(f'%{search_term}%'))
).scalars().all()
Date and Time Filtering
from datetime import datetime, timedelta
# Date ranges
recent_posts = database.session.execute(
select(BlogPost).filter(
BlogPost.fecha >= datetime.now() - timedelta(days=30)
)
).scalars().all()
# Date comparisons
expired_coupons = database.session.execute(
select(Coupon).filter(Coupon.fecha_expiracion < datetime.now())
).scalars().all()
Raw SQL When Needed
# For complex queries that are difficult with ORM
result = database.session.execute(
database.text(
"SELECT c.nombre, COUNT(ec.id) as student_count "
"FROM curso c LEFT JOIN estudiante_curso ec ON c.codigo = ec.curso "
"GROUP BY c.codigo, c.nombre "
"HAVING COUNT(ec.id) > :min_students"
),
{"min_students": 10}
).fetchall()
Migration Patterns
Common Migration Patterns from Old to New API
# ❌ OLD (Deprecated)
database.session.query(Usuario).filter_by(usuario=id_usuario).first()
# ✅ NEW (Modern)
database.session.execute(select(Usuario).filter_by(usuario=id_usuario)).scalars().first()
# ❌ OLD (Deprecated)
database.session.query(Curso).all()
# ✅ NEW (Modern)
database.session.execute(select(Curso)).scalars().all()
# ❌ OLD (Deprecated)
database.session.query(func.count(Usuario.id)).scalar()
# ✅ NEW (Modern)
database.session.execute(select(func.count(Usuario.id))).scalar()
# ❌ OLD (Deprecated)
database.session.query(Curso).filter(...).paginate(page=page, per_page=per_page)
# ✅ NEW (Modern)
database.paginate(select(Curso).filter(...), page=page, per_page=per_page)
# ❌ OLD (Deprecated)
database.session.query(Usuario).filter_by(id=user_id).delete()
# ✅ NEW (Modern)
database.session.execute(delete(Usuario).where(Usuario.id == user_id))
Choosing Between .scalars().first() vs .scalar_one_or_none()
# Use .scalars().first() when:
# - You want the first result even if multiple exist
# - Original code used .first()
# - Multiple results are acceptable/expected
result = database.session.execute(
select(Configuration).filter_by(key='theme')
).scalars().first()
# Use .scalar_one_or_none() when:
# - You expect exactly zero or one result
# - You want an exception if multiple results exist
# - Data integrity requires uniqueness
user = database.session.execute(
select(Usuario).filter_by(email=email)
).scalar_one_or_none()
Avoid singleton-comparison errors
Incorrect way
.filter(DocenteCurso.vigente == True)
.filter(DocenteCurso.vigente.is_(True))
Best Practices
- Always use .scalars() when you want SQLAlchemy model objects
- Use .scalar() for single values (counts, aggregations)
- Prefer .scalars().first() over .scalar_one_or_none() for compatibility
- Always commit after modifications unless in a transaction context
- Handle None results explicitly when using .first()
- Use select() for all read operations
- Use update() and delete() for bulk operations
- Import select from now_lms.db (pre-configured)
Error Handling
from sqlalchemy.exc import IntegrityError, MultipleResultsFound
try:
# Operation that might fail
database.session.execute(
update(Usuario).where(Usuario.id == user_id).values(email=new_email)
)
database.session.commit()
except IntegrityError as e:
database.session.rollback()
# Handle duplicate email or other constraint violations
flash("Email already exists", "error")
except MultipleResultsFound as e:
# Handle when scalar_one_or_none() finds multiple results
log.error(f"Multiple results found: {e}")
raise
This guide covers the essential patterns for modern SQLAlchemy 2.x usage in the NOW LMS project. Always prefer the modern syntax for new code and migrate legacy code gradually following these patterns.