More fixes and additions for forms. We are also haunted by detached sessions constantly.
This commit is contained in:
parent
979a329d6a
commit
a3f2c794f5
6 changed files with 160 additions and 49 deletions
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any, Callable, Dict, Iterable, List, Tuple, 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, raiseload, selectinload, with_polymorphic, Mapper, RelationshipProperty, class_mapper
|
||||
from sqlalchemy.orm import Load, Session, raiseload, selectinload, with_polymorphic, Mapper, RelationshipProperty, class_mapper, ColumnProperty
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
from sqlalchemy.orm.util import AliasedClass
|
||||
from sqlalchemy.sql import operators
|
||||
|
|
@ -12,6 +12,19 @@ from crudkit.core.spec import CRUDSpec
|
|||
from crudkit.core.types import OrderSpec, SeekWindow
|
||||
from crudkit.backend import BackendInfo, make_backend_info
|
||||
|
||||
def _is_rel(model_cls, name: str) -> bool:
|
||||
try:
|
||||
prop = model_cls.__mapper__.relationships.get(name)
|
||||
return isinstance(prop, RelationshipProperty)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _is_instrumented_column(attr) -> bool:
|
||||
try:
|
||||
return hasattr(attr, "property") and isinstance(attr.property, ColumnProperty)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _loader_options_for_fields(root_alias, model_cls, fields: list[str]) -> list[Load]:
|
||||
"""
|
||||
For bare MANYTOONE names in fields (e.g. "location"), selectinload the relationship
|
||||
|
|
@ -103,43 +116,47 @@ class CRUDService(Generic[T]):
|
|||
=> selectinload(root.location).selectinload(Room.room_function)
|
||||
"""
|
||||
opts: List[Any] = []
|
||||
|
||||
root_mapper: Mapper[Any] = cast(Mapper[Any], inspect(self.model))
|
||||
|
||||
for path, names in (rel_field_names or {}).items():
|
||||
if not path:
|
||||
continue
|
||||
|
||||
current_alias = root_alias
|
||||
current_mapper = root_mapper
|
||||
rel_props: List[RelationshipProperty] = []
|
||||
|
||||
valid = True
|
||||
for step in path:
|
||||
rel = current_mapper.relationships.get(step)
|
||||
if rel is None:
|
||||
if not isinstance(rel, RelationshipProperty):
|
||||
valid = False
|
||||
break
|
||||
rel_props.append(rel)
|
||||
current_mapper = cast(Mapper[Any], inspect(rel.entity.entity))
|
||||
if not valid:
|
||||
if not valid or not rel_props:
|
||||
continue
|
||||
|
||||
target_cls = current_mapper.class_
|
||||
first = rel_props[0]
|
||||
base_loader = selectinload(getattr(root_alias, first.key))
|
||||
for i in range(1, len(rel_props)):
|
||||
prev_target_cls = rel_props[i - 1].mapper.class_
|
||||
hop_attr = getattr(prev_target_cls, rel_props[i].key)
|
||||
base_loader = base_loader.selectinload(hop_attr)
|
||||
|
||||
target_cls = rel_props[-1].mapper.class_
|
||||
|
||||
requires = getattr(target_cls, "__crudkit_field_requires__", None)
|
||||
if not isinstance(requires, dict):
|
||||
continue
|
||||
|
||||
for field_name in names:
|
||||
needed: Iterable[str] = requires.get(field_name, [])
|
||||
needed: Iterable[str] = requires.get(field_name, []) or []
|
||||
for rel_need in needed:
|
||||
loader = selectinload(getattr(root_alias, rel_props[0].key))
|
||||
for rp in rel_props[1:]:
|
||||
loader = loader.selectinload(getattr(getattr(root_alias, rp.parent.class_.__name__.lower(), None) or rp.parent.class_, rp.key))
|
||||
|
||||
loader = loader.selectinload(getattr(target_cls, rel_need))
|
||||
opts.append(loader)
|
||||
rel_prop2 = target_cls.__mapper__.relationships.get(rel_need)
|
||||
if not isinstance(rel_prop2, RelationshipProperty):
|
||||
continue
|
||||
dep_attr = getattr(target_cls, rel_prop2.key)
|
||||
opts.append(base_loader.selectinload(dep_attr))
|
||||
|
||||
return opts
|
||||
|
||||
|
|
@ -225,12 +242,18 @@ class CRUDService(Generic[T]):
|
|||
|
||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||
|
||||
seen_rel_roots = set()
|
||||
for path, names in (rel_field_names or {}).items():
|
||||
if "label" in names:
|
||||
rel_name = path[0]
|
||||
if not path:
|
||||
continue
|
||||
rel_name = path[0]
|
||||
if rel_name in seen_rel_roots:
|
||||
continue
|
||||
if _is_rel(self.model, rel_name):
|
||||
rel_attr = getattr(root_alias, rel_name, None)
|
||||
if rel_attr is not None:
|
||||
query = query.options(selectinload(rel_attr))
|
||||
seen_rel_roots.add(rel_name)
|
||||
|
||||
# Soft delete filter
|
||||
if self.supports_soft_delete and not _is_truthy(params.get("include_deleted")):
|
||||
|
|
@ -251,8 +274,8 @@ class CRUDService(Generic[T]):
|
|||
only_cols = [c for c in root_fields if isinstance(c, InstrumentedAttribute)]
|
||||
if only_cols:
|
||||
query = query.options(Load(root_alias).load_only(*only_cols))
|
||||
for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
query = query.options(eager)
|
||||
# for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
# query = query.options(eager)
|
||||
|
||||
for opt in self._resolve_required_includes(root_alias, rel_field_names):
|
||||
query = query.options(opt)
|
||||
|
|
@ -387,6 +410,20 @@ class CRUDService(Generic[T]):
|
|||
if params:
|
||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||
|
||||
if rel_field_names:
|
||||
seen_rel_roots = set()
|
||||
for path, names in rel_field_names.items():
|
||||
if not path:
|
||||
continue
|
||||
rel_name = path[0]
|
||||
if rel_name in seen_rel_roots:
|
||||
continue
|
||||
if _is_rel(self.model, rel_name):
|
||||
rel_attr = getattr(root_alias, rel_name, None)
|
||||
if rel_attr is not None:
|
||||
query = query.options(selectinload(rel_attr))
|
||||
seen_rel_roots.add(rel_name)
|
||||
|
||||
fields = (params or {}).get("fields") if isinstance(params, dict) else None
|
||||
if fields:
|
||||
for opt in _loader_options_for_fields(root_alias, self.model, fields):
|
||||
|
|
@ -396,8 +433,8 @@ class CRUDService(Generic[T]):
|
|||
if only_cols:
|
||||
query = query.options(Load(root_alias).load_only(*only_cols))
|
||||
|
||||
for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
query = query.options(eager)
|
||||
# for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
# query = query.options(eager)
|
||||
|
||||
if params:
|
||||
fields = params.get("fields") or []
|
||||
|
|
@ -454,12 +491,26 @@ class CRUDService(Generic[T]):
|
|||
if params:
|
||||
root_fields, rel_field_names, root_field_names = spec.parse_fields()
|
||||
|
||||
if rel_field_names:
|
||||
seen_rel_roots = set()
|
||||
for path, names in rel_field_names.items():
|
||||
if not path:
|
||||
continue
|
||||
rel_name = path[0]
|
||||
if rel_name in seen_rel_roots:
|
||||
continue
|
||||
if _is_rel(self.model, rel_name):
|
||||
rel_attr = getattr(root_alias, rel_name, None)
|
||||
if rel_attr is not None:
|
||||
query = query.options(selectinload(rel_attr))
|
||||
seen_rel_roots.add(rel_name)
|
||||
|
||||
only_cols = [c for c in root_fields if isinstance(c, InstrumentedAttribute)]
|
||||
if only_cols:
|
||||
query = query.options(Load(root_alias).load_only(*only_cols))
|
||||
|
||||
for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
query = query.options(eager)
|
||||
# for eager in spec.get_eager_loads(root_alias, fields_map=rel_field_names):
|
||||
# query = query.options(eager)
|
||||
|
||||
if params:
|
||||
fields = params.get("fields") or []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue