def _build_requested_prefetches(self, prefetches, requirements, model, fields, filters, is_root_level): """Build a prefetch dictionary based on request requirements.""" meta = Meta(model) for name, field in six.iteritems(fields): original_field = field if isinstance(field, dfields.DynamicRelationField): field = field.serializer if isinstance(field, serializers.ListSerializer): field = field.child if not isinstance(field, serializers.ModelSerializer): continue source = field.source or name if '.' in source: raise ValidationError('Nested relationship values ' 'are not supported') if source == '*': # ignore custom getter/setter continue if source in prefetches: # ignore duplicated sources continue related_queryset = getattr(original_field, 'queryset', None) if callable(related_queryset): related_queryset = related_queryset(field) is_id_only = getattr(field, 'id_only', lambda: False)() is_remote = meta.is_field_remote(source) is_gui_root = self.view.get_format() == 'admin' and is_root_level if (related_queryset is None and is_id_only and not is_remote and not is_gui_root): # full representation and remote fields # should all trigger prefetching continue # Popping the source here (during explicit prefetch construction) # guarantees that implicitly required prefetches that follow will # not conflict. required = requirements.pop(source, None) query_name = Meta.get_query_name(original_field.model_field) prefetch_queryset = self._build_queryset(serializer=field, filters=filters.get( query_name, {}), queryset=related_queryset, requirements=required) # There can only be one prefetch per source, even # though there can be multiple fields pointing to # the same source. This could break in some cases, # but is mostly an issue on writes when we use all # fields by default. prefetches[source] = Prefetch(source, queryset=prefetch_queryset) return prefetches
def _build_queryset(self, serializer=None, filters=None, queryset=None, requirements=None): """Build a queryset that pulls in all data required by this request. Handles nested prefetching of related data and deferring fields at the queryset level. Arguments: serializer: An optional serializer to use a base for the queryset. If no serializer is passed, the `get_serializer` method will be used to initialize the base serializer for the viewset. filters: An optional TreeMap of nested filters. queryset: An optional base queryset. requirements: An optional TreeMap of nested requirements. """ is_root_level = False if serializer: if queryset is None: queryset = serializer.Meta.model.objects else: serializer = self.view.get_serializer() is_root_level = True model = serializer.get_model() if not model: return queryset meta = Meta(model) prefetches = {} # build a nested Prefetch queryset # based on request parameters and serializer fields fields = serializer.fields if requirements is None: requirements = TreeMap() self._get_implicit_requirements(fields, requirements) if filters is None: filters = self._get_requested_filters() # build nested Prefetch queryset self._build_requested_prefetches(prefetches, requirements, model, fields, filters, is_root_level) # build remaining prefetches out of internal requirements # that are not already covered by request requirements self._build_implicit_prefetches(model, prefetches, requirements) # use requirements at this level to limit fields selected # only do this for GET requests where we are not requesting the # entire fieldset is_gui = self.view.get_format() == 'admin' if ('*' not in requirements and not self.view.is_update() and not self.view.is_delete() and not is_gui): id_fields = getattr(serializer, 'get_id_fields', lambda: [])() # only include local model fields only = [ field for field in set(id_fields + list(requirements.keys())) if meta.is_field(field) and not meta.is_field_remote(field) ] queryset = queryset.only(*only) # add request filters query = self._filters_to_query(filters) if query: # Convert internal django ValidationError to # APIException-based one in order to resolve validation error # from 500 status code to 400. try: queryset = queryset.filter(query) except InternalValidationError as e: raise ValidationError( dict(e) if hasattr(e, 'error_dict') else list(e)) except Exception as e: # Some other Django error in parsing the filter. # Very likely a bad query, so throw a ValidationError. err_msg = getattr(e, 'message', '') raise ValidationError(err_msg) # A serializer can have this optional function # to dynamically apply additional filters on # any queries that will use that serializer # You could use this to have (for example) different # serializers for different subsets of a model or to # implement permissions which work even in sideloads if hasattr(serializer, 'filter_queryset'): queryset = serializer.filter_queryset(queryset) # add prefetches and remove duplicates if necessary prefetch = prefetches.values() queryset = queryset.prefetch_related(*prefetch) if has_joins(queryset) or not is_root_level: queryset = queryset.distinct() if self.DEBUG: queryset._using_prefetches = prefetches return queryset