---
url: "https://vueda.dev/v3/reference/api/py/vueda.core.viewsets.md"
title: "core.viewsets"
---

# core.viewsets

## Overview

ViewSet base classes with atomic transactions, row-level filtering, and flex-fields.

## Classes

- [AtomicCreateModelViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/AtomicCreateModelViewSetMixin.md)
- [AtomicDestroyModelViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/AtomicDestroyModelViewSetMixin.md)
- [AtomicModelViewSet](/v3/reference/api/py/vueda.core.viewsets/AtomicModelViewSet.md)
- [AtomicModelViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/AtomicModelViewSetMixin.md)
- [AtomicUpdateModelViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/AtomicUpdateModelViewSetMixin.md)
- [DeactivateActionViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/DeactivateActionViewSetMixin.md)
- [FlexFieldsMixin](/v3/reference/api/py/vueda.core.viewsets/FlexFieldsMixin.md)
- [ListRowLevelViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/ListRowLevelViewSetMixin.md)
- [NoExtraFieldsForViewSetMixin](/v3/reference/api/py/vueda.core.viewsets/NoExtraFieldsForViewSetMixin.md)
- [PerActionSerializerMixin](/v3/reference/api/py/vueda.core.viewsets/PerActionSerializerMixin.md)
- [VuedaReadOnlyViewSet](/v3/reference/api/py/vueda.core.viewsets/VuedaReadOnlyViewSet.md)
- [VuedaViewSet](/v3/reference/api/py/vueda.core.viewsets/VuedaViewSet.md)
- [WarningConfirmationMixin](/v3/reference/api/py/vueda.core.viewsets/WarningConfirmationMixin.md)

## `apply_prefetch_plan` {#apply_prefetch_plan}

Apply the ``select_related`` and ``prefetch_related`` paths of a plan from
:func:`build_prefetch_plan` to ``queryset``, so that a prefetch alone loads each relation that it
prefetches.

Django skips a to-one prefetch for a row whose relation is already cached, and a
``select_related`` join caches every to-one relation on its path. A join through a prefetched
relation would therefore replace the prefetched objects with plain joined ones and drop what the
``Prefetch`` queryset adds to them, such as the ``formatted_name`` annotation or an annotation of
the viewset's own.

The function collects the queryset's own joins and prefetches together with the plan's. It splits
each join at every relation on its path that a prefetch with a queryset loads:

- The part before the first prefetched relation stays a join on ``queryset``.
- A part after a prefetched relation is loaded with that relation's prefetch. When the plan built
  that prefetch, the part becomes a join on the ``Prefetch`` queryset, which costs no query. When
  ``queryset`` already carried the prefetch, the part becomes a string lookup after it, so the
  viewset's own ``Prefetch`` queryset keeps its own joins only. Each relation that such a lookup
  reaches costs one query per request.

Each surviving ``Prefetch`` queryset gets the same split, whether the plan built it or
``queryset`` already carried it, and whether its relation is to-one or to-many. Its joins are
split at each relation that a lookup prefetches under its relation, such as ``customer`` for a
``cart_set`` queryset when the plan also prefetches ``cart_set__customer``. The split applies at
every depth. A viewset's own ``Prefetch`` is rebuilt with the split queryset only when one of
its joins passes through such a relation. Its filters, annotations, and ordering stay as
declared.

A prefetch without a queryset, such as the string ``"customer__user"``, loads the same plain
objects that a join loads. A join through it stays, and Django skips that prefetch for each row
whose relation the join already cached.

The function first drops each plan prefetch that ``queryset`` already covers. A relation that
``queryset`` already prefetches with a ``Prefetch`` queryset of its own keeps that ``Prefetch``.
A relation that ``queryset`` prefetches without a queryset, such as with the string
``"customer"``, loads through the plan's ``Prefetch``, so its ``formatted_name`` annotation
reaches the response. The function then orders all the lookups so that each lookup runs before
the lookups that reuse its rows. A prefetch with ``to_attr`` stores its rows under its own
attribute, so a join on the same relation leaves it unaffected and stays.

A bare ``select_related()`` with no paths follows every non-null foreign key and records no path
to split. When the plan adds a join, Django replaces the bare ``select_related()`` with the
plan's paths, so the queryset joins only those paths. Without a join in the plan, the bare
``select_related()`` stays, and its joins can replace a prefetched to-one relation.

Reads ``queryset.query.select_related``, a private Django attribute: ``False``, ``True`` for a
bare ``select_related()``, or a nested dict of relation names.

:param queryset: The queryset to apply the plan to.
:type queryset: django.db.models.QuerySet
:param select_related: The plan's ``select_related`` paths.
:type select_related: List[str]
:param prefetch_related: The plan's ``prefetch_related`` lookups.
:type prefetch_related: List[Union[str, django.db.models.Prefetch]]
:return: ``queryset`` with the plan applied.
:rtype: django.db.models.QuerySet

### Signature {#apply_prefetch_plan-signature}

`apply_prefetch_plan(queryset, select_related, prefetch_related)`

#### Parameters {#apply_prefetch_plan-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="apply_prefetch_plan-param-queryset">queryset</span> |  | yes |  |  |
| <span id="apply_prefetch_plan-param-select_related">select_related</span> |  | yes |  |  |
| <span id="apply_prefetch_plan-param-prefetch_related">prefetch_related</span> |  | yes |  |  |

### Source {#apply_prefetch_plan-source}

`server/vueda/core/viewsets/__init__.py:1122`

## `build_prefetch_plan` {#build_prefetch_plan}

Derive the ``select_related``/``prefetch_related`` paths needed for ``serializer``'s resolved
fields against ``model``, so a list or retrieve response's query count no longer grows with the
row count for whatever a request's ``?e=`` actually expanded.

Walks ``serializer.fields`` rather than re-deriving expansion rules, so it depends on that
resolution already having happened: for the root ``serializer``, the caller must have already
applied the request's query-param resolution (``vueda.core.serializers.ensure_flex_fields_applied``
-- ``VuedaViewSet.get_queryset`` does this before calling here); a recursive call on a nested
child gets this for free, because a child's expand/fields/omit values were passed as constructor
kwargs and ``rest_flex_fields2`` applies those automatically the moment ``.fields`` is accessed.
Either way, a nested serializer only appears in ``.fields`` when the response will actually
traverse it. A field never named in ``?e=`` (or excluded by a ``permit_{action}_expands``
restriction) never becomes one, so it is never planned.

``?f=`` (sparse fields) and ``?om=`` (omit) play no part in this either way, through two
separate mechanisms that both happen to have the same effect:
``FlexFieldsWriteableNestedSerializerMixin.apply_flex_fields`` re-admits an already-requested
expand's own name into the sparse-fields set, as a side effect of defaulting its sub-fields to a
wildcard when none are requested; ``VuedaExpandableFieldsSerializerMixin.apply_flex_fields``
does the same for omit, as a side effect of always hiding ``available_actions`` from an expanded
object. Neither parameter can drop an expand this plan would otherwise cover, nor can either one
add one the request never named in ``?e=``. The plan simply mirrors whatever survives in
``.fields``.

Depth is bounded the same way: a serializer with nothing further expanded at some level has no
further nested serializer fields, so the recursion here terminates exactly where
``rest_flex_fields2``'s own (already-validated) expansion depth does, with no second bound to
maintain.

A field's ``source`` (defaulting to its name, honoring an explicit ``source=`` in its
``expandable_fields`` declaration) is resolved against ``model``'s actual relations via
:func:`resolve_relation_path`, not against the nested serializer's own declared ``Meta.model``,
so a plan is only produced for a source that genuinely names a relation chain.

A ``GenericForeignKey`` expand (``GenericForeignKeySerializer``) resolves its concrete
serializer per-instance at representation time, after any queryset planning could run, so it is
intentionally not planned here -- it keeps resolving lazily, same as it does today. A
``GenericRelation`` (a reverse collection onto a fixed, known model) is a normal to-many
relation and is planned like any other.

A to-one relation loads through ``select_related``, together with the paths that the related
model's ``formatted_name_select_related`` declares. The exception is a related model whose
``formatted_name`` is an annotation (``formatted_name_annotation_path`` returns a path), because a
join loads the related model's columns but not its annotations. That relation loads through a
``Prefetch`` whose queryset annotates ``formatted_name``, which costs one query for the related
rows. The queryset starts from the related model's ``_base_manager``, the manager that Django
uses to load a related object, so a default manager that filters rows cannot hide the related
object. When the relation's ``source`` crosses other to-one relations first, those relations load
through a join.

A to-many relation always loads through a ``Prefetch`` whose queryset starts from the related
model's default manager and annotates ``formatted_name`` and ``object_revision``.

The child serializer's own paths join the plan prefixed with the relation's path, for a to-one
and a to-many relation alike. A nested ``Prefetch`` keeps its queryset under the prefixed path.
The plan holds each path as its own entry, so the child's paths stay in the plan when a different
``Prefetch`` loads the relation itself. That ``Prefetch`` can be one that the viewset's queryset
lists in ``prefetch_related`` or the one that a second expand of the same relation builds.
:func:`apply_prefetch_plan` places each path on whichever ``Prefetch`` loads the relation:

- A join under a ``Prefetch`` that the plan built goes onto that ``Prefetch`` queryset.
- A join under a viewset's own ``Prefetch`` loads after it, at one query per relation for each
  request.
- A nested prefetch runs after the ``Prefetch`` that loads the relation.

A prefetch inside the queryset of a viewset's ``Prefetch``, such as ``"cart_items"`` in
``Prefetch("cart_set", queryset=Cart.objects.prefetch_related("cart_items"))``, loads its
relation while that queryset runs. The child's paths under that relation then load one query per
row.

Returns ``(select_related, prefetch_related)``: the plan's join paths and its prefetch lookups.
Pass them to :func:`apply_prefetch_plan`, which keeps a join from replacing the objects that a
prefetch loads.

### Signature {#build_prefetch_plan-signature}

`build_prefetch_plan(serializer, model)`

#### Parameters {#build_prefetch_plan-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="build_prefetch_plan-param-serializer">serializer</span> |  | yes |  |  |
| <span id="build_prefetch_plan-param-model">model</span> |  | yes |  |  |

### Source {#build_prefetch_plan-source}

`server/vueda/core/viewsets/__init__.py:860`

## `resolve_relation_path` {#resolve_relation_path}

Walk a serializer field's dotted ``source`` against ``model``'s relations, one segment per dot,
the same traversal DRF's own attribute resolution performs. Returns ``(orm_path, leaf_model,
is_to_one)``: ``orm_path`` is ``source`` with dots replaced by the ``__`` lookup separator,
``leaf_model`` is the model the last segment relates to, and ``is_to_one`` says whether every
segment in the chain is single-valued (forward foreign key or one-to-one, either direction) --
the condition under which the whole chain can live in ``select_related`` rather than needing
``prefetch_related``.

Returns ``None`` when ``source`` does not resolve to a chain of relations -- a plain column, a
dotted path through a Python property or method, or a path through a ``GenericForeignKey``.
Such a source cannot be turned into a queryset plan and is left for the ORM to resolve lazily
at representation time, same as it does today.

### Signature {#resolve_relation_path-signature}

`resolve_relation_path(model, source)`

#### Parameters {#resolve_relation_path-parameters}

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| <span id="resolve_relation_path-param-model">model</span> |  | yes |  |  |
| <span id="resolve_relation_path-param-source">source</span> |  | yes |  |  |

### Source {#resolve_relation_path-source}

`server/vueda/core/viewsets/__init__.py:831`

## Source

`server/vueda/core/viewsets/__init__.py`
