SQLAlchemy ORM
SQLAlchemy ORM comprehensively, covering everything from basic concepts to advanced patterns.
1. Core Concepts
Section titled “1. Core Concepts”What is SQLAlchemy ORM?
Section titled “What is SQLAlchemy ORM?”- Object-Relational Mapping: Maps Python classes to database tables
- Session: Manages persistence operations for mapped objects
- Unit of Work: Tracks changes to objects and flushes them to database
- Identity Map: Ensures only one instance per database row
Key Components:
Section titled “Key Components:”- Engine: Database connectivity
- Session: Database conversation
- Model: Python class representing a table
- Query: API for database queries
2. Engine & Session Setup
Section titled “2. Engine & Session Setup”from sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmaker, Sessionfrom sqlalchemy.ext.declarative import declarative_base
# Engine Configurationengine = create_engine( # SQLite example "sqlite:///example.db",
# PostgreSQL example # "postgresql://user:password@localhost/mydatabase",
# MySQL example # "mysql+pymysql://user:password@localhost/mydatabase",
# Connection options echo=True, # Log SQL statements future=True, # Use 2.0 style APIs pool_size=5, # Connection pool size max_overflow=10, # Additional connections beyond pool_size pool_pre_ping=True, # Check connection validity)
# Session FactorySessionLocal = sessionmaker( bind=engine, autoflush=True, # Autoflush before queries autocommit=False, # Don't autocommit expire_on_commit=True, # Expire objects after commit)
# Base class for modelsBase = declarative_base()
# Context manager for sessionsdef get_db(): db = SessionLocal() try: yield db finally: db.close()3. Declarative Base & Models
Section titled “3. Declarative Base & Models”Basic Model Structure
Section titled “Basic Model Structure”from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Float, Numericfrom sqlalchemy.sql import funcfrom datetime import datetime
class User(Base): __tablename__ = 'users' # Table name __table_args__ = ( # Table-level constraints {'schema': 'public'}, # Schema name )
# Primary key column id = Column(Integer, primary_key=True, autoincrement=True)
# String columns with constraints username = Column(String(50), unique=True, nullable=False, index=True) email = Column(String(100), unique=True, nullable=False)
# Text column for large content bio = Column(Text, nullable=True)
# Numeric columns age = Column(Integer, nullable=True) salary = Column(Numeric(10, 2), nullable=True) # Precision: 10 digits, 2 decimal places rating = Column(Float, nullable=True)
# Boolean column is_active = Column(Boolean, default=True, nullable=False)
# DateTime columns created_at = Column(DateTime, default=datetime.utcnow, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Custom methods def __repr__(self): return f"<User(id={self.id}, username='{self.username}')>"
def to_dict(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns}Advanced Column Types and Constraints
Section titled “Advanced Column Types and Constraints”from sqlalchemy import Enum, JSON, ARRAY, LargeBinaryfrom enum import Enum as PyEnum
class UserRole(PyEnum): ADMIN = "admin" USER = "user" MODERATOR = "moderator"
class AdvancedUser(Base): __tablename__ = 'advanced_users'
id = Column(Integer, primary_key=True)
# Enum column role = Column(Enum(UserRole), default=UserRole.USER, nullable=False)
# JSON column for flexible data preferences = Column(JSON, default=dict)
# Array column (PostgreSQL specific) tags = Column(ARRAY(String), default=[])
# Binary data avatar = Column(LargeBinary, nullable=True)
# Computed column (SQLAlchemy 1.3.11+) # full_name = Column(String, computed='first_name + " " + last_name')Table Configuration Options
Section titled “Table Configuration Options”from sqlalchemy import UniqueConstraint, CheckConstraint, Index
class ConfiguredUser(Base): __tablename__ = 'configured_users'
# Table-level constraints __table_args__ = ( # Unique constraint across multiple columns UniqueConstraint('email', 'company_id', name='uq_email_company'),
# Check constraint CheckConstraint('age >= 0', name='check_age_positive'),
# Index on multiple columns Index('ix_email_active', 'email', 'is_active'),
# Table comment {'comment': 'Users table with advanced configuration'} )
id = Column(Integer, primary_key=True) email = Column(String(100), nullable=False) company_id = Column(Integer, nullable=False) age = Column(Integer, nullable=True) is_active = Column(Boolean, default=True)4. Basic CRUD Operations
Section titled “4. Basic CRUD Operations”Create (Insert)
Section titled “Create (Insert)”from sqlalchemy.orm import Session
# Create a new sessiondb = SessionLocal()
# Create single objectnew_user = User( username="john_doe", email="john@example.com", age=30, salary=50000.00)db.add(new_user)db.commit() # Persist to databasedb.refresh(new_user) # Refresh to get database-generated values (like ID)
print(f"Created user with ID: {new_user.id}")
# Create multiple objectsusers = [ User(username="alice", email="alice@example.com"), User(username="bob", email="bob@example.com"), User(username="charlie", email="charlie@example.com"),]db.add_all(users)db.commit()
# Bulk insert (faster for large datasets)user_data = [ {"username": "user1", "email": "user1@example.com"}, {"username": "user2", "email": "user2@example.com"},]db.bulk_insert_mappings(User, user_data)db.commit()Read (Select)
Section titled “Read (Select)”# Get by primary keyuser = db.get(User, 1) # Returns None if not founduser = db.query(User).get(1) # Alternative syntax
# Get first matching recorduser = db.query(User).filter(User.username == "john_doe").first()
# Get all recordsall_users = db.query(User).all()
# Get one or raise exceptionuser = db.query(User).filter(User.username == "john_doe").one()
# Check if existsexists = db.query(User.id).filter(User.username == "john_doe").first() is not NoneUpdate
Section titled “Update”# Update single objectuser = db.query(User).filter(User.username == "john_doe").first()if user: user.age = 31 user.salary = 55000.00 db.commit()
# Bulk updatedb.query(User).filter(User.is_active == False).update( {"is_active": True}, synchronize_session=False)db.commit()
# Update with returning values (PostgreSQL)updated_count = db.query(User).filter(User.age < 25).update( {"salary": User.salary * 1.1}, synchronize_session=False)db.commit()print(f"Updated {updated_count} users")Delete
Section titled “Delete”# Delete single objectuser = db.query(User).filter(User.username == "john_doe").first()if user: db.delete(user) db.commit()
# Bulk deletedeleted_count = db.query(User).filter(User.is_active == False).delete( synchronize_session=False)db.commit()print(f"Deleted {deleted_count} users")5. Relationships
Section titled “5. Relationships”One-to-Many Relationship
Section titled “One-to-Many Relationship”from sqlalchemy import ForeignKeyfrom sqlalchemy.orm import relationship
class Department(Base): __tablename__ = 'departments'
id = Column(Integer, primary_key=True) name = Column(String(50), nullable=False)
# Relationship (one department has many employees) employees = relationship("Employee", back_populates="department")
def __repr__(self): return f"<Department(id={self.id}, name='{self.name}')>"
class Employee(Base): __tablename__ = 'employees'
id = Column(Integer, primary_key=True) name = Column(String(50), nullable=False) department_id = Column(Integer, ForeignKey('departments.id'), nullable=False)
# Relationship (many employees belong to one department) department = relationship("Department", back_populates="employees")
def __repr__(self): return f"<Employee(id={self.id}, name='{self.name}')>"
# Usagedb = SessionLocal()
# Create department with employeesit_dept = Department(name="IT")it_dept.employees = [ Employee(name="Alice"), Employee(name="Bob")]db.add(it_dept)db.commit()
# Query with relationshipsdept = db.query(Department).filter(Department.name == "IT").first()print(f"Department: {dept.name}")print("Employees:", [emp.name for emp in dept.employees])
# Query employees with departmentemp = db.query(Employee).filter(Employee.name == "Alice").first()print(f"Employee: {emp.name}, Department: {emp.department.name}")Many-to-Many Relationship
Section titled “Many-to-Many Relationship”# Association table for many-to-manystudent_course_association = Table( 'student_courses', Base.metadata, Column('student_id', Integer, ForeignKey('students.id'), primary_key=True), Column('course_id', Integer, ForeignKey('courses.id'), primary_key=True), Column('enrolled_at', DateTime, default=datetime.utcnow))
class Student(Base): __tablename__ = 'students'
id = Column(Integer, primary_key=True) name = Column(String(50), nullable=False)
courses = relationship( "Course", secondary=student_course_association, back_populates="students" )
class Course(Base): __tablename__ = 'courses'
id = Column(Integer, primary_key=True) title = Column(String(100), nullable=False)
students = relationship( "Student", secondary=student_course_association, back_populates="courses" )
# Usagedb = SessionLocal()
# Create students and coursesmath = Course(title="Mathematics")physics = Course(title="Physics")
alice = Student(name="Alice", courses=[math, physics])bob = Student(name="Bob", courses=[math])
db.add_all([alice, bob, math, physics])db.commit()
# Query many-to-manystudent = db.query(Student).filter(Student.name == "Alice").first()print(f"Student: {student.name}")print("Courses:", [course.title for course in student.courses])
course = db.query(Course).filter(Course.title == "Mathematics").first()print(f"Course: {course.title}")print("Students:", [student.name for student in course.students])One-to-One Relationship
Section titled “One-to-One Relationship”class UserProfile(Base): __tablename__ = 'user_profiles'
id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey('users.id'), unique=True, nullable=False) bio = Column(Text) website = Column(String(200))
user = relationship("User", back_populates="profile", uselist=False)
class User(Base): __tablename__ = 'users'
id = Column(Integer, primary_key=True) username = Column(String(50), unique=True)
profile = relationship("UserProfile", back_populates="user", uselist=False)
# Usageuser = User(username="john")profile = UserProfile(bio="Software developer", website="example.com")user.profile = profile
db.add(user)db.commit()
# Access one-to-oneuser = db.query(User).filter(User.username == "john").first()print(f"User: {user.username}, Bio: {user.profile.bio}")6. Querying
Section titled “6. Querying”Basic Query Methods
Section titled “Basic Query Methods”from sqlalchemy import and_, or_, not_
db = SessionLocal()
# Filteringusers = db.query(User).filter(User.age > 25).all()users = db.query(User).filter(User.username.in_(["alice", "bob"])).all()
# Multiple filtersusers = db.query(User).filter( User.age > 25, User.is_active == True).all()
# OR conditionsusers = db.query(User).filter( or_(User.age < 25, User.age > 60)).all()
# NOT conditionsusers = db.query(User).filter( not_(User.is_active)).all()
# LIKE and ILIKE (case-insensitive)users = db.query(User).filter(User.username.like("j%")).all() # Starts with jusers = db.query(User).filter(User.username.ilike("j%")).all() # Case-insensitive
# NULL checksusers = db.query(User).filter(User.bio.is_(None)).all() # IS NULLusers = db.query(User).filter(User.bio.is_not(None)).all() # IS NOT NULLSorting and Limiting
Section titled “Sorting and Limiting”# Order byusers = db.query(User).order_by(User.username).all()users = db.query(User).order_by(User.age.desc()).all()
# Multiple sort criteriausers = db.query(User).order_by(User.is_active.desc(), User.username).all()
# Limit and offset (pagination)users = db.query(User).order_by(User.id).limit(10).offset(20).all()
# Distinctdistinct_ages = db.query(User.age).distinct().all()Aggregation and Grouping
Section titled “Aggregation and Grouping”from sqlalchemy import func
# Countuser_count = db.query(func.count(User.id)).scalar()active_users = db.query(func.count(User.id)).filter(User.is_active == True).scalar()
# Average, Sum, Min, Maxavg_age = db.query(func.avg(User.age)).scalar()total_salary = db.query(func.sum(User.salary)).scalar()min_age = db.query(func.min(User.age)).scalar()max_age = db.query(func.max(User.age)).scalar()
# Group byage_groups = db.query( User.age, func.count(User.id)).group_by(User.age).all()
# Having clauseage_groups = db.query( User.age, func.count(User.id).label('user_count')).group_by(User.age).having(func.count(User.id) > 5).all()7. Advanced Query Techniques
Section titled “7. Advanced Query Techniques”# Implicit join (using relationships)employees = db.query(Employee, Department).join(Employee.department).all()
# Explicit joinemployees = db.query(Employee, Department).join(Department, Employee.department_id == Department.id).all()
# Left outer joinemployees = db.query(Employee, Department).outerjoin(Employee.department).all()
# Multiple joinsresult = (db.query(Student, Course) .join(student_course_association) .join(Course) .filter(Course.title == "Mathematics") .all())
# Self-join (for hierarchical data)class Employee(Base): __tablename__ = 'employees' id = Column(Integer, primary_key=True) name = Column(String(50)) manager_id = Column(Integer, ForeignKey('employees.id')) manager = relationship("Employee", remote_side=[id], backref="subordinates")
# Query with self-joinmanagers_with_subordinates = (db.query(Employee) .join(Employee.subordinates) .group_by(Employee.id) .having(func.count(Employee.subordinates) > 0) .all())Subqueries
Section titled “Subqueries”from sqlalchemy import select
# Subquery in FROM clausesubq = (db.query( Department.id.label('dept_id'), func.count(Employee.id).label('emp_count')).group_by(Department.id).subquery())
dept_counts = db.query(Department.name, subq.c.emp_count).join( subq, Department.id == subq.c.dept_id).all()
# Subquery in WHERE clausesubq = db.query(Employee.department_id).filter(Employee.salary > 50000).subquery()high_paying_depts = db.query(Department).filter(Department.id.in_(subq)).all()
# Correlated subquerysubq = (db.query(func.count(Employee.id)) .filter(Employee.department_id == Department.id) .correlate(Department) .scalar_subquery())
depts_with_count = db.query(Department.name, subq.label('employee_count')).all()Eager Loading (to avoid N+1 queries)
Section titled “Eager Loading (to avoid N+1 queries)”from sqlalchemy.orm import joinedload, selectinload, subqueryload
# Eager load relationships to avoid additional queries
# joinedload (uses JOIN)employees = (db.query(Employee) .options(joinedload(Employee.department)) .all())# Single query with JOIN
# selectinload (uses IN clause, better for collections)departments = (db.query(Department) .options(selectinload(Department.employees)) .all())
# Multiple eager loadsemployees = (db.query(Employee) .options( joinedload(Employee.department), selectinload(Employee.projects) ) .all())
# Load only specific columnsemployees = (db.query(Employee) .options( joinedload(Employee.department).load_only(Department.name) ) .all())8. Session Management
Section titled “8. Session Management”Session States
Section titled “Session States”db = SessionLocal()
# Transient - not in session, not in databasenew_user = User(username="new_user")print(db.is_modified(new_user)) # False
# Pending - added to session, not flusheddb.add(new_user)print(new_user in db) # Trueprint(db.is_modified(new_user, include_pending=True)) # True
# Flushed - SQL generated but not committeddb.flush()print(new_user.id) # Now has ID from database
# Committed - changes persisteddb.commit()
# Expired - attributes need to be reloadedprint(new_user.username) # Triggers lazy load
# Detached - session closed, object no longer associateddb.close()print(new_user in db) # FalseSession Lifecycle Patterns
Section titled “Session Lifecycle Patterns”# Pattern 1: Context managerwith SessionLocal() as session: user = session.get(User, 1) user.username = "updated" session.commit()
# Pattern 2: Explicit commit/rollbacksession = SessionLocal()try: user = User(username="test") session.add(user) session.commit()except Exception: session.rollback() raisefinally: session.close()
# Pattern 3: Using scoped sessions (for web applications)from sqlalchemy.orm import scoped_session
session_factory = sessionmaker(bind=engine)ScopedSession = scoped_session(session_factory)
# In web requestdef handle_request(): session = ScopedSession() try: # Use session user = session.get(User, 1) return user finally: ScopedSession.remove()9. Transactions
Section titled “9. Transactions”Basic Transaction Management
Section titled “Basic Transaction Management”db = SessionLocal()
try: # Start transaction implicitly
# Operation 1 user1 = User(username="user1") db.add(user1)
# Operation 2 user2 = User(username="user2") db.add(user2)
# Commit both operations db.commit()
except Exception as e: # Rollback on error db.rollback() print(f"Transaction failed: {e}")
finally: db.close()Nested Transactions (SAVEPOINT)
Section titled “Nested Transactions (SAVEPOINT)”db = SessionLocal()
try: user = User(username="main_user") db.add(user) db.flush() # Flush to get user ID
# Create savepoint savepoint = db.begin_nested()
try: # Nested operations profile = UserProfile(user_id=user.id, bio="Test bio") db.add(profile) db.flush()
# If this fails, only rollback to savepoint if some_condition: raise ValueError("Something went wrong")
savepoint.commit()
except Exception: savepoint.rollback() print("Nested transaction rolled back") # Main transaction continues
db.commit()
except Exception: db.rollback()Transaction Isolation Levels
Section titled “Transaction Isolation Levels”from sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmaker
# Set isolation level at engine levelengine = create_engine( "postgresql://user:pass@localhost/db", isolation_level="REPEATABLE_READ")
# Or set at session levelsession = sessionmaker(bind=engine)with session() as s: s.connection(execution_options={"isolation_level": "SERIALIZABLE"}) # Transaction with specific isolation level10. Advanced Patterns
Section titled “10. Advanced Patterns”Hybrid Properties and Expressions
Section titled “Hybrid Properties and Expressions”from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
class User(Base): __tablename__ = 'users'
id = Column(Integer, primary_key=True) first_name = Column(String(50)) last_name = Column(String(50)) birth_date = Column(DateTime)
@hybrid_property def full_name(self): return f"{self.first_name} {self.last_name}"
@full_name.expression def full_name(cls): return cls.first_name + ' ' + cls.last_name
@hybrid_property def age(self): if self.birth_date: return (datetime.now() - self.birth_date).days // 365 return None
@age.expression def age(cls): return func.extract('year', func.age(cls.birth_date))
@hybrid_method def is_older_than(self, years): return self.age > years if self.age else False
@is_older_than.expression def is_older_than(cls, years): return func.extract('year', func.age(cls.birth_date)) > years
# Usagedb.query(User).filter(User.full_name == "John Doe").all()db.query(User).filter(User.age > 25).all()db.query(User).filter(User.is_older_than(30)).all()Custom Query Class
Section titled “Custom Query Class”from sqlalchemy.orm import Query
class UserQuery(Query): def active(self): return self.filter(User.is_active == True)
def by_age_range(self, min_age, max_age): return self.filter(User.age >= min_age, User.age <= max_age)
def with_profile(self): return self.options(joinedload(User.profile))
# Use custom query classSessionLocal = sessionmaker(bind=engine, query_cls=UserQuery)db = SessionLocal()
# Use custom methodsactive_users = db.query(User).active().all()young_users = db.query(User).active().by_age_range(18, 25).with_profile().all()Event Listeners
Section titled “Event Listeners”from sqlalchemy import event
# Model event listeners@event.listens_for(User, 'before_insert')def before_insert_listener(mapper, connection, target): target.created_at = datetime.utcnow() target.updated_at = datetime.utcnow()
@event.listens_for(User, 'before_update')def before_update_listener(mapper, connection, target): target.updated_at = datetime.utcnow()
# Session event listeners@event.listens_for(SessionLocal, 'after_begin')def after_begin_listener(session, transaction, connection): print("Session began")
@event.listens_for(SessionLocal, 'before_commit')def before_commit_listener(session): print("About to commit")
# Engine event listeners@event.listens_for(engine, 'connect')def set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close()Polymorphic Inheritance
Section titled “Polymorphic Inheritance”class Person(Base): __tablename__ = 'person' id = Column(Integer, primary_key=True) name = Column(String(50)) type = Column(String(20))
__mapper_args__ = { 'polymorphic_identity': 'person', 'polymorphic_on': type }
class Employee(Person): __tablename__ = 'employee' id = Column(Integer, ForeignKey('person.id'), primary_key=True) employee_id = Column(String(20))
__mapper_args__ = { 'polymorphic_identity': 'employee', }
class Manager(Person): __tablename__ = 'manager' id = Column(Integer, ForeignKey('person.id'), primary_key=True) department = Column(String(50))
__mapper_args__ = { 'polymorphic_identity': 'manager', }
# Usageemployee = Employee(name="John", employee_id="E123")manager = Manager(name="Jane", department="IT")
db.add_all([employee, manager])db.commit()
# Query polymorphicallypeople = db.query(Person).all() # Returns both employees and managersemployees = db.query(Employee).all() # Returns only employeesThis comprehensive guide covers SQLAlchemy ORM from basic to advanced concepts. The key to mastering SQLAlchemy is practice and understanding the session lifecycle and relationship patterns.