Work begins on proper one to many support.
This commit is contained in:
parent
811b534b89
commit
ffa49f13e9
3 changed files with 82 additions and 24 deletions
|
|
@ -5,7 +5,7 @@ from flask import current_app
|
|||
from typing import Any, Callable, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast
|
||||
from sqlalchemy import and_, func, inspect, or_, text
|
||||
from sqlalchemy.engine import Engine, Connection
|
||||
from sqlalchemy.orm import Load, Session, with_polymorphic, Mapper, contains_eager
|
||||
from sqlalchemy.orm import Load, Session, with_polymorphic, Mapper, contains_eager, selectinload
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
from sqlalchemy.sql import operators
|
||||
from sqlalchemy.sql.elements import UnaryExpression, ColumnElement
|
||||
|
|
@ -49,7 +49,7 @@ def _unwrap_ob(ob):
|
|||
is_desc = False
|
||||
dir_attr = getattr(ob, "_direction", None)
|
||||
if dir_attr is not None:
|
||||
is_desc = (dir_attr is operators.desc_op) or (getattr(op, "name", "").upper() == "DESC")
|
||||
is_desc = (dir_attr is operators.desc_op) or (getattr(dir_attr, "name", "").upper() == "DESC")
|
||||
elif isinstance(ob, UnaryExpression):
|
||||
op = getattr(ob, "operator", None)
|
||||
is_desc = (op is operators.desc_op) or (getattr(op, "name", "").upper() == "DESC")
|
||||
|
|
@ -231,7 +231,7 @@ class CRUDService(Generic[T]):
|
|||
# Parse all inputs so join_paths are populated
|
||||
filters = spec.parse_filters()
|
||||
order_by = spec.parse_sort()
|
||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||
root_fields, rel_field_names, root_field_names, collection_field_names = spec.parse_fields()
|
||||
spec.parse_includes()
|
||||
join_paths = tuple(spec.get_join_paths())
|
||||
|
||||
|
|
@ -243,12 +243,25 @@ class CRUDService(Generic[T]):
|
|||
if only_cols:
|
||||
query = query.options(Load(root_alias).load_only(*only_cols))
|
||||
|
||||
# JOIN all resolved paths, hydrate from the join
|
||||
# JOIN all resolved paths; for collections use selectinload (never join)
|
||||
used_contains_eager = False
|
||||
for _base_alias, rel_attr, target_alias in join_paths:
|
||||
query = query.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
query = query.options(contains_eager(rel_attr, alias=target_alias))
|
||||
used_contains_eager = True
|
||||
for base_alias, rel_attr, target_alias in join_paths:
|
||||
is_collection = bool(getattr(getattr(rel_attr, "property", None), "uselist", False))
|
||||
if is_collection:
|
||||
opt = selectinload(rel_attr)
|
||||
# narroe child columns it requested (e.g., updates.id,updates.timestamp)
|
||||
child_names = (collection_field_names or {}).get(rel_attr.key, [])
|
||||
if child_names:
|
||||
target_cls = rel_attr.property.mapper.class_
|
||||
cols = [getattr(target_cls, n, None) for n in child_names]
|
||||
cols = [c for c in cols if isinstance(c, InstrumentedAttribute)]
|
||||
if cols:
|
||||
opt = opt.load_only(*cols)
|
||||
query = query.options(opt)
|
||||
else:
|
||||
query = query.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
query = query.options(contains_eager(rel_attr, alias=target_alias))
|
||||
used_contains_eager = True
|
||||
|
||||
# Filters AFTER joins → no cartesian products
|
||||
if filters:
|
||||
|
|
@ -346,8 +359,10 @@ class CRUDService(Generic[T]):
|
|||
base = session.query(getattr(root_alias, "id"))
|
||||
base = self._apply_not_deleted(base, root_alias, params)
|
||||
# same joins as above for correctness
|
||||
for _base_alias, rel_attr, target_alias in join_paths:
|
||||
base = base.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
for base_alias, rel_attr, target_alias in join_paths:
|
||||
# do not join collections for COUNT mirror
|
||||
if not bool(getattr(getattr(rel_attr, "property", None), "uselist", False)):
|
||||
base = base.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
if filters:
|
||||
base = base.filter(*filters)
|
||||
total = session.query(func.count()).select_from(
|
||||
|
|
@ -428,7 +443,7 @@ class CRUDService(Generic[T]):
|
|||
filters = spec.parse_filters()
|
||||
# no ORDER BY for get()
|
||||
if params:
|
||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||
root_fields, rel_field_names, root_field_names, collection_field_names = spec.parse_fields()
|
||||
spec.parse_includes()
|
||||
|
||||
join_paths = tuple(spec.get_join_paths())
|
||||
|
|
@ -438,12 +453,24 @@ class CRUDService(Generic[T]):
|
|||
if only_cols:
|
||||
query = query.options(Load(root_alias).load_only(*only_cols))
|
||||
|
||||
# JOIN all discovered paths up front; hydrate via contains_eager
|
||||
# JOIN non-collections only; collections via selectinload
|
||||
used_contains_eager = False
|
||||
for _base_alias, rel_attr, target_alias in join_paths:
|
||||
query = query.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
query = query.options(contains_eager(rel_attr, alias=target_alias))
|
||||
used_contains_eager = True
|
||||
for base_alias, rel_attr, target_alias in join_paths:
|
||||
is_collection = bool(getattr(getattr(rel_attr, "property", None), "uselist", False))
|
||||
if is_collection:
|
||||
opt = selectinload(rel_attr)
|
||||
child_names = (collection_field_names or {}).get(rel_attr.key, [])
|
||||
if child_names:
|
||||
target_cls = rel_attr.property.mapper.class_
|
||||
cols = [getattr(target_cls, n, None) for n in child_names]
|
||||
cols = [c for c in cols if isinstance(c, InstrumentedAttribute)]
|
||||
if cols:
|
||||
opt = opt.load_only(*cols)
|
||||
query = query.options(opt)
|
||||
else:
|
||||
query = query.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
query = query.options(contains_eager(rel_attr, alias=target_alias))
|
||||
used_contains_eager = True
|
||||
|
||||
# Apply filters (joins are in place → no cartesian products)
|
||||
if filters:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue