SQLAlchemy ORM#
SQLAlchemy’s Object-Relational Mapper (ORM) provides a high-level abstraction that allows you to work with database tables as Python classes and rows as objects. The ORM builds on top of SQLAlchemy Core and adds features like identity mapping, unit of work pattern, and relationship management. This approach lets you write database code in a more Pythonic way, focusing on objects and their relationships rather than SQL statements. The ORM is ideal for applications with complex domain models where you want to leverage object-oriented programming patterns.
Define Models with Declarative Base#
The declarative system is the most common way to define ORM models in SQLAlchemy.
You create a base class using declarative_base() and then define your models
as subclasses. Each model class represents a database table, with class attributes
defining columns. The __tablename__ attribute specifies the table name. This
approach keeps your model definitions clean and readable while providing full
access to SQLAlchemy’s features.
>>> from sqlalchemy import create_engine, Column, Integer, String
>>> from sqlalchemy.orm import declarative_base
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... email = Column(String(100))
... def __repr__(self):
... return f"User(id={self.id}, name='{self.name}')"
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
Session Basics#
The Session is the primary interface for persistence operations in the ORM.
It manages a “holding zone” for objects you’ve loaded or associated with it, and
handles the communication with the database. Sessions track changes to objects
and synchronize them with the database when you call commit(). The recommended
pattern is to use sessionmaker to create a session factory, then create sessions
as needed. Always close sessions when done to release database connections.
>>> from sqlalchemy import create_engine, Column, Integer, String
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> try:
... user = User(name="Alice")
... session.add(user)
... session.commit()
... print(f"Created user with id: {user.id}")
... finally:
... session.close()
Created user with id: 1
Add and Commit Objects#
To persist new objects to the database, add them to the session with add() or
add_all() for multiple objects. Objects remain in a “pending” state until you
call commit(), which flushes all pending changes to the database in a transaction.
If an error occurs, call rollback() to undo all changes since the last commit.
After commit, auto-generated values like primary keys are available on the objects.
>>> from sqlalchemy import create_engine, Column, Integer, String
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> # Add single object
>>> user1 = User(name="Alice")
>>> session.add(user1)
>>> # Add multiple objects
>>> users = [User(name="Bob"), User(name="Carol")]
>>> session.add_all(users)
>>> session.commit()
>>> print([u.id for u in [user1] + users])
[1, 2, 3]
>>> session.close()
Query Objects#
SQLAlchemy 2.0 uses select() with session.execute() for queries, replacing
the legacy session.query() API. The select() construct accepts model classes
or specific columns. Use scalars() to get model instances directly, or execute()
for row tuples. The result supports iteration, all() for a list, first() for
the first result, and one() when exactly one result is expected.
>>> from sqlalchemy import create_engine, Column, Integer, String, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... age = Column(Integer)
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> session.add_all([
... User(name="Alice", age=30),
... User(name="Bob", age=25),
... User(name="Carol", age=35)])
>>> session.commit()
>>> # Get all users
>>> users = session.execute(select(User)).scalars().all()
>>> print([u.name for u in users])
['Alice', 'Bob', 'Carol']
>>> # Filter with where()
>>> user = session.execute(select(User).where(User.age > 28)).scalars().first()
>>> print(user.name)
Alice
>>> session.close()
Filter Queries#
The where() method accepts filter conditions using column comparisons. SQLAlchemy
overloads Python operators to generate SQL: == becomes =, != becomes <>,
and so on. For complex conditions, use and_(), or_(), and not_() from
SQLAlchemy. Columns also provide methods like in_(), like(), between(),
is_(), and isnot() for SQL-specific operations.
>>> from sqlalchemy import create_engine, Column, Integer, String, select, and_, or_
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... age = Column(Integer)
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> session.add_all([
... User(name="Alice", age=30),
... User(name="Bob", age=25),
... User(name="Carol", age=35),
... User(name="Fred", age=30)])
>>> session.commit()
>>> # AND condition
>>> stmt = select(User).where(and_(User.age >= 30, User.name.like("A%")))
>>> print([u.name for u in session.execute(stmt).scalars()])
['Alice']
>>> # OR condition
>>> stmt = select(User).where(or_(User.name == "Alice", User.name == "Bob"))
>>> print([u.name for u in session.execute(stmt).scalars()])
['Alice', 'Bob']
>>> # IN clause
>>> stmt = select(User).where(User.age.in_([25, 35]))
>>> print([u.name for u in session.execute(stmt).scalars()])
['Bob', 'Carol']
>>> session.close()
Update Objects#
To update objects, simply modify their attributes and call commit(). The session
tracks changes to loaded objects automatically through a mechanism called “dirty
tracking”. When you commit, SQLAlchemy generates UPDATE statements only for changed
attributes. You can also use bulk updates with update() for efficiency when
modifying many rows without loading them into memory.
>>> from sqlalchemy import create_engine, Column, Integer, String, select, update
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> session.add(User(name="Alice"))
>>> session.commit()
>>> # Update via object modification
>>> user = session.execute(select(User).where(User.name == "Alice")).scalars().first()
>>> user.name = "Alicia"
>>> session.commit()
>>> # Verify update
>>> user = session.execute(select(User)).scalars().first()
>>> print(user.name)
Alicia
>>> session.close()
Delete Objects#
To delete objects, use session.delete() followed by commit(). The session
will generate a DELETE statement for the object. For bulk deletes without loading
objects, use the delete() construct with session.execute(). Be careful with
cascading deletes when objects have relationships - SQLAlchemy can automatically
delete related objects based on your cascade configuration.
>>> from sqlalchemy import create_engine, Column, Integer, String, select, delete
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> session.add_all([User(name="Alice"), User(name="Bob"), User(name="Carol")])
>>> session.commit()
>>> # Delete via object
>>> user = session.execute(select(User).where(User.name == "Bob")).scalars().first()
>>> session.delete(user)
>>> session.commit()
>>> # Verify deletion
>>> users = session.execute(select(User)).scalars().all()
>>> print([u.name for u in users])
['Alice', 'Carol']
>>> session.close()
One-to-Many Relationship#
Relationships define how tables are connected. A one-to-many relationship means one
record in the parent table can have multiple related records in the child table.
Use relationship() on the parent side and ForeignKey on the child side.
The back_populates parameter creates a bidirectional relationship, allowing
navigation from both sides. SQLAlchemy handles the foreign key management automatically.
>>> from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker, relationship
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... posts = relationship("Post", back_populates="author")
>>> class Post(Base):
... __tablename__ = "posts"
... id = Column(Integer, primary_key=True)
... title = Column(String(100))
... user_id = Column(Integer, ForeignKey("users.id"))
... author = relationship("User", back_populates="posts")
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> user = User(name="Alice")
>>> user.posts.append(Post(title="First Post"))
>>> user.posts.append(Post(title="Second Post"))
>>> session.add(user)
>>> session.commit()
>>> # Access relationship
>>> user = session.execute(select(User)).scalars().first()
>>> print([p.title for p in user.posts])
['First Post', 'Second Post']
>>> session.close()
Many-to-Many Relationship#
Many-to-many relationships require an association table that contains foreign keys
to both related tables. Define the association table using Table, then use
relationship() with the secondary parameter pointing to it. Both sides can
have a relationship, and SQLAlchemy manages the association table entries automatically
when you add or remove items from the relationship collections.
>>> from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Table, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker, relationship
>>> Base = declarative_base()
>>> # Association table
>>> student_course = Table(
... "student_course", Base.metadata,
... Column("student_id", Integer, ForeignKey("students.id"), primary_key=True),
... Column("course_id", Integer, ForeignKey("courses.id"), primary_key=True))
>>> class Student(Base):
... __tablename__ = "students"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... courses = relationship("Course", secondary=student_course, back_populates="students")
>>> class Course(Base):
... __tablename__ = "courses"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... students = relationship("Student", secondary=student_course, back_populates="courses")
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> math = Course(name="Math")
>>> physics = Course(name="Physics")
>>> alice = Student(name="Alice", courses=[math, physics])
>>> bob = Student(name="Bob", courses=[math])
>>> session.add_all([alice, bob])
>>> session.commit()
>>> # Query relationships
>>> math = session.execute(select(Course).where(Course.name == "Math")).scalars().first()
>>> print([s.name for s in math.students])
['Alice', 'Bob']
>>> session.close()
Self-Referential Relationship#
Self-referential relationships connect a table to itself, useful for hierarchical
data like organizational charts, categories, or threaded comments. Use ForeignKey
pointing to the same table and relationship() with remote_side to indicate
which side is the “parent”. This pattern allows you to model tree structures where
each node can have a parent and multiple children.
>>> from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker, relationship
>>> Base = declarative_base()
>>> 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")
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> ceo = Employee(name="CEO")
>>> session.add(ceo)
>>> session.flush()
>>> manager = Employee(name="Manager", manager_id=ceo.id)
>>> session.add(manager)
>>> session.flush()
>>> worker1 = Employee(name="Worker1", manager_id=manager.id)
>>> worker2 = Employee(name="Worker2", manager_id=manager.id)
>>> session.add_all([worker1, worker2])
>>> session.commit()
>>> # Navigate hierarchy
>>> mgr = session.execute(select(Employee).where(Employee.name == "Manager")).scalars().first()
>>> print(f"Manager: {mgr.name}, Boss: {mgr.manager.name}")
Manager: Manager, Boss: CEO
>>> print(f"Subordinates: {[e.name for e in mgr.subordinates]}")
Subordinates: ['Worker1', 'Worker2']
>>> session.close()
Cascade Deletes#
Cascade options control what happens to related objects when a parent is deleted
or modified. The cascade parameter on relationship() accepts a comma-separated
string of cascade rules. Common options include "all, delete-orphan" which deletes
children when the parent is deleted and when children are removed from the collection.
This ensures referential integrity and prevents orphaned records.
>>> from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker, relationship
>>> Base = declarative_base()
>>> class Parent(Base):
... __tablename__ = "parents"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... children = relationship("Child", back_populates="parent",
... cascade="all, delete-orphan")
>>> class Child(Base):
... __tablename__ = "children"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... parent_id = Column(Integer, ForeignKey("parents.id"))
... parent = relationship("Parent", back_populates="children")
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> parent = Parent(name="Parent1")
>>> parent.children = [Child(name="Child1"), Child(name="Child2")]
>>> session.add(parent)
>>> session.commit()
>>> # Delete parent - children are also deleted
>>> session.delete(parent)
>>> session.commit()
>>> children = session.execute(select(Child)).scalars().all()
>>> print(len(children))
0
>>> session.close()
Eager Loading#
By default, SQLAlchemy uses lazy loading for relationships, executing a new query
when you access related objects. This can cause the “N+1 query problem” when iterating
over many objects. Eager loading fetches related objects in the same query using
JOIN or subqueries. Use joinedload() for single objects or small collections,
and selectinload() for larger collections to avoid cartesian products.
>>> from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker, relationship, joinedload
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... posts = relationship("Post", back_populates="author")
>>> class Post(Base):
... __tablename__ = "posts"
... id = Column(Integer, primary_key=True)
... title = Column(String(100))
... user_id = Column(Integer, ForeignKey("users.id"))
... author = relationship("User", back_populates="posts")
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> user = User(name="Alice")
>>> user.posts = [Post(title="Post1"), Post(title="Post2")]
>>> session.add(user)
>>> session.commit()
>>> # Eager load posts with user in single query
>>> stmt = select(User).options(joinedload(User.posts))
>>> user = session.execute(stmt).scalars().unique().first()
>>> print([p.title for p in user.posts]) # No additional query
['Post1', 'Post2']
>>> session.close()
Hybrid Properties#
Hybrid properties allow you to define Python properties that work both at the instance
level (in Python) and at the class level (in SQL queries). This is useful for computed
attributes that you want to filter or sort by in database queries. Use the
@hybrid_property decorator and optionally @property.expression to customize
the SQL expression.
>>> from sqlalchemy import create_engine, Column, Integer, String, select
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> from sqlalchemy.ext.hybrid import hybrid_property
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... first_name = Column(String(50))
... last_name = Column(String(50))
...
... @hybrid_property
... def full_name(self):
... return f"{self.first_name} {self.last_name}"
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> session.add(User(first_name="Alice", last_name="Smith"))
>>> session.commit()
>>> user = session.execute(select(User)).scalars().first()
>>> print(user.full_name)
Alice Smith
>>> session.close()
Event Hooks#
SQLAlchemy provides an event system that lets you hook into various ORM operations
like before/after insert, update, or delete. Use @event.listens_for() decorator
to register event handlers. Events are useful for auditing, validation, automatic
timestamps, or triggering side effects. Common events include before_insert,
after_insert, before_update, after_update, before_delete, and
after_delete.
>>> from sqlalchemy import create_engine, Column, Integer, String, DateTime, select, event
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> from datetime import datetime
>>> Base = declarative_base()
>>> class User(Base):
... __tablename__ = "users"
... id = Column(Integer, primary_key=True)
... name = Column(String(50))
... created_at = Column(DateTime)
... updated_at = Column(DateTime)
>>> @event.listens_for(User, "before_insert")
... def set_created_at(mapper, connection, target):
... target.created_at = datetime.now()
... target.updated_at = datetime.now()
>>> @event.listens_for(User, "before_update")
... def set_updated_at(mapper, connection, target):
... target.updated_at = datetime.now()
>>> engine = create_engine("sqlite:///:memory:")
>>> Base.metadata.create_all(engine)
>>> Session = sessionmaker(bind=engine)
>>> session = Session()
>>> user = User(name="Alice")
>>> session.add(user)
>>> session.commit()
>>> print(user.created_at is not None)
True
>>> session.close()