1. One-to-many and many-to-many relationships
Start with the contract: make inputs, outputs and failure behavior explicit before adding infrastructure. This keeps the feature easy to reason about and gives tests a stable boundary.
2. Loading strategies and the N+1 problem
Apply the pattern through a small vertical slice. Keep framework wiring at the edge and business decisions in focused functions or services that can be tested without starting the whole application.
class Project(Base):
__tablename__ = "projects"
id: Mapped[int] = mapped_column(primary_key=True)
tasks: Mapped[list["Task"]] = relationship(back_populates="project")
stmt = select(Project).options(selectinload(Project.tasks))3. Atomic transactions and Alembic migrations
Treat failure paths as part of the design. Add bounded resource usage, meaningful errors and a verification step so the behavior remains dependable under real production conditions.
4. Hands-on Exercise
Build the feature
Add projects and task ownership, generate and review an Alembic migration, then prove rollback keeps the database consistent when a multi-write operation fails.
Definition of done
- The happy path works through the real HTTP boundary.
- At least one failure path is handled and tested.
- Configuration and secrets stay outside source code.
- The README explains how to run and verify the result.
5. Knowledge Check
What problem does selectinload() solve?
Show answer
It loads related rows in a bounded extra query instead of triggering one query per parent row.