SQLAlchemy 2.0 Select API migration for with_polymorphic loading
0 reputation · 06 Aug 2024, 11:58 UTC
0 reputation · 06 Aug 2024, 11:58 UTC
SQLAlchemy 2.0 replaces the legacy Session.query() pattern with a unified select() function. While most ORM operations have direct equivalents in the new Select object, certain advanced loading strategies used in polymorphic inheritance require a different approach.
In the legacy Query API, with_polymorphic() allowed for the explicit loading of multiple subclasses in a single query. In the 2.0-style API, there is no direct Select.with_polymorphic() method. Developers must currently rely on with_loader_criteria() or explicit joins to achieve similar results, but these methods handle the underlying SQL generation differently.
When migrating a codebase from version 1.4 (with future=True) to 2.0, the lack of a dedicated polymorphic method on the Select object creates uncertainty regarding the most efficient way to maintain complex inheritance queries without reverting to legacy patterns.
with_loader_criteria() intended to be the primary replacement for with_polymorphic() in all 2.0 use cases?with_loader_criteria() does not provide the necessary join control?with_loader_criteria() is not the replacement for with_polymorphic(). They serve different purposes: with_polymorphic() changes the entity mapping (which columns are selected and how rows are instantiated), while with_loader_criteria() only adds filtering clauses to loader queries.
Apply with_polymorphic() to the loader option, not the Select object:
from sqlalchemy.orm import selectinload, joinedload
# For collections — prefer selectinload to avoid wide JOINs
stmt = select(Parent).options(
selectinload(Parent.children).with_polymorphic([ChildA, ChildB])
)
# For single-parent relationships — joinedload is fine
stmt = select(Parent).options(
joinedload(Parent.child).with_polymorphic([ChildA, ChildB])
)
The with_polymorphic() method here is on the loader strategy (SelectInLoad/JoinedLoad), not on Select.
Use the standalone with_polymorphic() function from sqlalchemy.orm as the select() target:
from sqlalchemy.orm import with_polymorphic
PolyParent = with_polymorphic(Parent, [ChildA, ChildB])
stmt = select(PolyParent).where(PolyParent.type == 'child_a')
results = session.execute(stmt).scalars().all()
This returns an AliasedClass that maps all subclass columns in one query.
contains_eager()When you need manual JOINs (e.g., custom ON clauses, lateral joins):
from sqlalchemy.orm import contains_eager, aliased
ChildA_alias = aliased(ChildA)
ChildB_alias = aliased(ChildB)
stmt = (
select(Parent)
.join(ChildA_alias, Parent.id == ChildA_alias.parent_id, isouter=True)
.join(ChildB_alias, Parent.id == ChildB_alias.parent_id, isouter=True)
.options(
contains_eager(Parent.child_a, alias=ChildA_alias),
contains_eager(Parent.child_b, alias=ChildB_alias),
)
)
| Scenario | Recommended Pattern |
|---|---|
| Loading a collection/relationship with subclasses | selectinload(...).with_polymorphic([...]) |
| Loading a single related object with subclasses | joinedload(...).with_polymorphic([...]) |
| Querying the polymorphic entity directly (no parent relationship) | select(with_polymorphic(Parent, [...])) |
| Custom JOIN conditions or lateral joins | Manual join() + contains_eager() |
| Filtering which subclasses participate in a loader query | with_loader_criteria() (adds WHERE/ON, not entity mapping) |
with_polymorphic() on a loader or standalone function includes subclass columns in the SELECT and instantiates the correct subclass type. with_loader_criteria() does not.with_loader_criteria() applies globally to the session unless scoped with include_aliases or propagate_to_loaders. Loader options are per-query.joinedload.with_polymorphic() produces LEFT OUTER JOINs for each subclass — wide hierarchies can explode row counts. selectinload issues a second SELECT with IN clause, usually more efficient for collections.import sqlalchemy; print(sqlalchemy.__version__) (must be ≥ 2.0.0)create_engine(..., echo=True) to compare generated SQL against legacy Query.with_polymorphic()isinstance(obj, ChildA) after loadingAre you loading polymorphic subclasses through a relationship (e.g., Parent.children) or querying the base entity directly without a parent relationship? The answer determines whether you use loader options (pattern 1) or the standalone with_polymorphic() function (pattern 2).
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.