2025, Dec 31 17:00

How to Stack SQLAlchemy joinedload Calls to Eager Load Nested Relationships in FastAPI Jinja

Avoid lazy-loading errors in FastAPI Jinja templates by stacking SQLAlchemy joinedload calls. Learn to eager load nested relationships and fix AttributeError.

When rendering HTML with Jinja in a FastAPI app, it’s common to pass ORM entities into the template. If those entities rely on lazy relationships, attempting to touch related attributes during template rendering can backfire. The safe approach is to fetch everything you know you’ll need up front, using eager loading. A frequent stumbling block here is how to eager load across multiple relationships with SQLAlchemy’s joinedload.

Problem demonstration

The goal is to load a parent row and traverse several relationships in one go. An attempt like the following triggers an error because of how attributes are referenced inside joinedload:

from sqlalchemy.orm import joinedload

qry = qry.options(
    joinedload(Asset.acquisitions),
    joinedload(Asset.acquisitions.ticket),
    joinedload(Asset.acquisitions.ticket.shop),
)

Running this raises an AttributeError similar to:

AttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with Asset.acquisitions has an attribute 'ticket'

What’s actually going wrong

joinedload expects a relationship-bound attribute at each step. Writing Asset.acquisitions.ticket tries to access ticket on the InstrumentedAttribute that represents Asset.acquisitions, which is not a mapped class. In other words, you can’t chain dotted attributes inside a single joinedload call. The traversal has to be expressed as a sequence of joinedload calls, each anchored to the correct mapped class for the next hop.

The fix: stack joinedload calls

To eager load across multiple relations, chain joinedload step by step. Each subsequent joinedload is invoked on the previous one and references the next relationship using the appropriate model class.

from sqlalchemy.orm import joinedload

qry = qry.options(
    joinedload(Asset.acquisitions)
        .joinedload(Acquisition.ticket)
        .joinedload(Ticket.shop)
)

This expresses the path explicitly: start with Asset.acquisitions, then Acquisition.ticket, then Ticket.shop. You can find the same pattern in the SQLAlchemy docs: relationship loading.

Why this matters

If you know a template will access related data, it’s safer to prepare the graph before rendering. That avoids database access during template evaluation and prevents errors that arise when lazy loading isn’t available at that moment. Preloading with joinedload also keeps the code intention clear: the view layer receives objects that are ready to be used as-is.

Closing thoughts

Keep database work in the request handler and pass fully prepared objects to Jinja. When traversing multiple relationships, don’t dot through attributes inside joinedload; instead, stack joinedload calls, each referencing the relationship on the correct mapped class. If you’re considering how many columns are selected across these joins, that’s a separate concern; the approach above focuses on loading the related rows correctly in a single, predictable pass.