mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-09-19 03:09:56 +00:00
revert: drop populate_existing reload in create()/update()
This commit is contained in:
@@ -270,32 +270,17 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
return cls.default_load_options
|
return cls.default_load_options
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _capture_pk_values(
|
async def _reload_with_options(
|
||||||
cls: type[Self], instance: DeclarativeBase
|
cls: type[Self], session: AsyncSession, instance: DeclarativeBase
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Capture PK values off instance — call before commit expires attributes."""
|
|
||||||
return {
|
|
||||||
cast(str, col.key): getattr(instance, cast(str, col.key))
|
|
||||||
for col in cls.model.__mapper__.primary_key
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def _reload_with_options_by_pk(
|
|
||||||
cls: type[Self], session: AsyncSession, pk_values: dict[str, Any]
|
|
||||||
) -> ModelType:
|
) -> ModelType:
|
||||||
"""Re-query by previously captured PK values, with default_load_options applied."""
|
"""Re-query instance by PK with default_load_options applied."""
|
||||||
# Only called when cls.default_load_options is set (see call sites).
|
mapper = cls.model.__mapper__
|
||||||
pk_filters = [
|
pk_filters = [
|
||||||
getattr(cls.model, key) == value for key, value in pk_values.items()
|
getattr(cls.model, cast(str, col.key))
|
||||||
|
== getattr(instance, cast(str, col.key))
|
||||||
|
for col in mapper.primary_key
|
||||||
]
|
]
|
||||||
q = select(cls.model).where(and_(*pk_filters))
|
return await cls.get(session, filters=pk_filters)
|
||||||
q = q.execution_options(populate_existing=True)
|
|
||||||
q = q.options(*cast(Sequence[ExecutableOption], cls.default_load_options))
|
|
||||||
result = await session.execute(q)
|
|
||||||
item = result.unique().scalar_one_or_none()
|
|
||||||
if item is None: # pragma: no cover — row was just flushed in this transaction
|
|
||||||
raise NotFoundError()
|
|
||||||
return cast(ModelType, item)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _resolve_m2m(
|
async def _resolve_m2m(
|
||||||
@@ -849,14 +834,9 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
setattr(db_model, rel_attr, related_instances)
|
setattr(db_model, rel_attr, related_instances)
|
||||||
|
|
||||||
session.add(db_model)
|
session.add(db_model)
|
||||||
pk_values: dict[str, Any] | None = None
|
await session.refresh(db_model)
|
||||||
if cls.default_load_options:
|
if cls.default_load_options:
|
||||||
await session.flush()
|
db_model = await cls._reload_with_options(session, db_model)
|
||||||
pk_values = cls._capture_pk_values(db_model)
|
|
||||||
if pk_values is not None:
|
|
||||||
db_model = await cls._reload_with_options_by_pk(session, pk_values)
|
|
||||||
else:
|
|
||||||
await session.refresh(db_model)
|
|
||||||
result = cast(ModelType, db_model)
|
result = cast(ModelType, db_model)
|
||||||
if schema:
|
if schema:
|
||||||
return Response(data=schema.model_validate(result))
|
return Response(data=schema.model_validate(result))
|
||||||
@@ -1233,15 +1213,9 @@ class AsyncCrud(Generic[ModelType]):
|
|||||||
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
|
m2m_resolved = await cls._resolve_m2m(session, obj, only_set=True)
|
||||||
for rel_attr, related_instances in m2m_resolved.items():
|
for rel_attr, related_instances in m2m_resolved.items():
|
||||||
setattr(db_model, rel_attr, related_instances)
|
setattr(db_model, rel_attr, related_instances)
|
||||||
|
await session.refresh(db_model)
|
||||||
pk_values: dict[str, Any] | None = None
|
if cls.default_load_options:
|
||||||
if cls.default_load_options:
|
db_model = await cls._reload_with_options(session, db_model)
|
||||||
await session.flush()
|
|
||||||
pk_values = cls._capture_pk_values(db_model)
|
|
||||||
if pk_values is not None:
|
|
||||||
db_model = await cls._reload_with_options_by_pk(session, pk_values)
|
|
||||||
else:
|
|
||||||
await session.refresh(db_model)
|
|
||||||
if schema:
|
if schema:
|
||||||
return Response(data=schema.model_validate(db_model))
|
return Response(data=schema.model_validate(db_model))
|
||||||
return db_model
|
return db_model
|
||||||
|
|||||||
@@ -466,6 +466,30 @@ class TestDefaultLoadOptionsIntegration:
|
|||||||
assert updated.role is not None
|
assert updated.role is not None
|
||||||
assert updated.role.name == "admin"
|
assert updated.role.name == "admin"
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_does_not_expire_already_loaded_relationships(
|
||||||
|
self, db_session: AsyncSession
|
||||||
|
):
|
||||||
|
"""create()'s reload must not blow away loaded state on related objects."""
|
||||||
|
UserWithDefaultLoad = CrudFactory(
|
||||||
|
User, default_load_options=[selectinload(User.role)]
|
||||||
|
)
|
||||||
|
role = await RoleCrud.create(db_session, RoleCreate(name="admin"))
|
||||||
|
role = await RoleCrud.get(
|
||||||
|
db_session,
|
||||||
|
filters=[Role.id == role.id],
|
||||||
|
load_options=[selectinload(Role.users)],
|
||||||
|
)
|
||||||
|
assert role.users == []
|
||||||
|
|
||||||
|
await UserWithDefaultLoad.create(
|
||||||
|
db_session,
|
||||||
|
UserCreate(username="alice", email="alice@test.com", role_id=role.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
# must not trigger a lazy load
|
||||||
|
assert role.users == []
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_load_options_overrides_default_load_options(
|
async def test_load_options_overrides_default_load_options(
|
||||||
self, db_session: AsyncSession
|
self, db_session: AsyncSession
|
||||||
|
|||||||
Reference in New Issue
Block a user