Skip to content

v2: split AutoCRUD read loading from mutation existence checks (read_options + _exists) #39

Description

@dmezzogori

Problem

AutoCRUD uses a single loading strategy for two different jobs:

  1. Read-for-serializationget / get_if_exist / get_multi feeding a response schema, which wants every relationship the schema touches eager-loaded.
  2. Existence checks inside mutationscreate / update / delete flows (and downstream apps' composed operations) that only need the row, and actively don't want relationship collections cached mid-mutation.

Because both go through the same loader, downstream apps end up fighting session-identity-map semantics. Concrete case: teban's crud_resources had to deliberately keep user.roles lazy (documented in its _resource_select docstring) because get_if_exist is also called mid-create before role assignment — which produced the recurring /api/v1/resources/ N+1 family (teban Sentry TEBAN-3/5/6/8/C/E) and a hand-rolled _refresh_resource_collections workaround.

The 1.11 line (perf/rbac-query-loading) mitigates the worst of it (select_options ClassVar applied on read paths, selectin RBAC relationships, expire-on-association-mutation), but the structural conflation remains.

Proposal (v2)

Split the two concerns in the AutoCRUD surface:

  • Declared read loader — first-class per-CRUD (or per-operation) loader options for serialization reads, e.g. evolving select_options into read_options with the ability to differ between get and get_multi.
  • Lightweight existence check — an internal _exists(entity_id, context) (or get_for_update) that base mutations and app-level composed mutations use instead of the full read loader: no eager loads, no caching of collections that the ongoing mutation is about to invalidate.
  • Base mutations (update, delete, create_if_not_exist) switch to the lightweight check; read endpoints keep the declared loader.

Relationship to feature/user-model-extensibility

That branch already reshapes the v2 CRUD surface (kernel-resolved user model, generic CRUDUsers with explicit model, router factories). The read/mutation split should ride the same major, and wants to be designed against that surface, not against 1.x:

  • read_options must be declarable for kernel-resolved models (e.g. a custom user model's relationships), so the option type likely needs to be lazy/callable rather than a class-level tuple evaluated at import time.
  • CRUDUsers' generic-model rework is the natural place to define how per-model loader declarations compose with subclassing.

Acceptance sketch

  • Mutations never populate relationship collections as a side effect of their existence checks.
  • Read paths load exactly what the declared read loader specifies; apps can assert this with kwik.testing.assert_max_queries.
  • teban can delete its _refresh_resource_collections / populate_existing workarounds entirely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions