Skip to content

core.viewsets ​

Overview ​

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

Classes ​

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(queryset, select_related, prefetch_related)

Parameters ​

NameTypeRequiredDefaultDescription
querysetyes
select_relatedyes
prefetch_relatedyes

Source ​

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

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(serializer, model)

Parameters ​

NameTypeRequiredDefaultDescription
serializeryes
modelyes

Source ​

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

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(model, source)

Parameters ​

NameTypeRequiredDefaultDescription
modelyes
sourceyes

Source ​

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

Source ​

server/vueda/core/viewsets/__init__.py