diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 7162c4fea9d..9be17a4a84c 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -340,38 +340,37 @@ class RegisterLookupMixin: _unregister_class_lookup = classmethod(_unregister_class_lookup) -def select_related_descend(field, restricted, requested, select_mask, reverse=False): +def select_related_descend(field, restricted, requested, select_mask): """ - Return True if this field should be used to descend deeper for - select_related() purposes. Used by both the query construction code - (compiler.get_related_selections()) and the model instance creation code - (compiler.klass_info). + Return whether `field` should be used to descend deeper for + `select_related()` purposes. Arguments: - * field - the field to be checked - * restricted - a boolean field, indicating if the field list has been - manually restricted using a requested clause) - * requested - The select_related() dictionary. - * select_mask - the dictionary of selected fields. - * reverse - boolean, True if we are checking a reverse select related + * `field` - the field to be checked. Can be either a `Field` or + `ForeignObjectRel` instance. + * `restricted` - a boolean field, indicating if the field list has been + manually restricted using a select_related() clause. + * `requested` - the select_related() dictionary. + * `select_mask` - the dictionary of selected fields. """ + # Only relationships can be descended. if not field.remote_field: return False - if field.remote_field.parent_link and not reverse: + # Forward MTI parent links should not be explicitly descended as they are + # always JOIN'ed against (unless excluded by `select_mask`). + if getattr(field.remote_field, "parent_link", False): return False - if restricted: - if reverse and field.related_query_name() not in requested: - return False - if not reverse and field.name not in requested: - return False - if not restricted and field.null: + # When `select_related()` is used without a `*requested` mask all + # relationships are descended unless they are nullable. + if not restricted: + return not field.null + # When `select_related(*requested)` is used only fields that are part of + # `requested` should be descended. + if field.name not in requested: return False - if ( - restricted - and select_mask - and field.name in requested - and field not in select_mask - ): + # Prevent invalid usages of `select_related()` and `only()`/`defer()` such + # as `select_related("a").only("b")` and `select_related("a").defer("a")`. + if select_mask and field not in select_mask: raise FieldError( f"Field {field.model._meta.object_name}.{field.name} cannot be both " "deferred and traversed using select_related at the same time." diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 7541817ba09..7377e555c3a 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -1259,11 +1259,10 @@ class SQLCompiler: ] for related_object, related_field, model in related_fields: if not select_related_descend( - related_field, + related_object, restricted, requested, select_mask, - reverse=True, ): continue @@ -1280,7 +1279,7 @@ class SQLCompiler: "model": model, "field": related_field, "reverse": True, - "local_setter": related_field.remote_field.set_cached_value, + "local_setter": related_object.set_cached_value, "remote_setter": related_field.set_cached_value, "from_parent": from_parent, } @@ -1296,7 +1295,7 @@ class SQLCompiler: select_fields.append(len(select)) select.append((col, None)) klass_info["select_fields"] = select_fields - next = requested.get(related_field.related_query_name(), {}) + next = requested.get(related_field_name, {}) next_klass_infos = self.get_related_selections( select, related_select_mask, diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index c5c58b1788c..a7bc0610c81 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -791,44 +791,44 @@ class Query(BaseExpression): if select_mask is None: select_mask = {} select_mask[opts.pk] = {} - # All concrete fields that are not part of the defer mask must be - # loaded. If a relational field is encountered it gets added to the - # mask for it be considered if `select_related` and the cycle continues - # by recursively calling this function. - for field in opts.concrete_fields: + # All concrete fields and related objects that are not part of the + # defer mask must be included. If a relational field is encountered it + # gets added to the mask for it be considered if `select_related` and + # the cycle continues by recursively calling this function. + for field in opts.concrete_fields + opts.related_objects: field_mask = mask.pop(field.name, None) - field_att_mask = mask.pop(field.attname, None) + field_att_mask = None + if field_attname := getattr(field, "attname", None): + field_att_mask = mask.pop(field_attname, None) if field_mask is None and field_att_mask is None: select_mask.setdefault(field, {}) elif field_mask: if not field.is_relation: raise FieldError(next(iter(field_mask))) + # Virtual fields such as many-to-many and generic foreign keys + # cannot be effectively deferred. Historically, they were + # allowed to be passed to QuerySet.defer(). Ignore such field + # references until a layer of validation at mask alteration + # time is eventually implemented. + if field.many_to_many: + continue field_select_mask = select_mask.setdefault(field, {}) - related_model = field.remote_field.model._meta.concrete_model + related_model = field.related_model._meta.concrete_model self._get_defer_select_mask( related_model._meta, field_mask, field_select_mask ) - # Remaining defer entries must be references to reverse relationships. - # The following code is expected to raise FieldError if it encounters - # a malformed defer entry. + # Remaining defer entries must be references to filtered relations + # otherwise they are surfaced as missing field errors. for field_name, field_mask in mask.items(): if filtered_relation := self._filtered_relations.get(field_name): relation = opts.get_field(filtered_relation.relation_name) field_select_mask = select_mask.setdefault((field_name, relation), {}) + related_model = relation.related_model._meta.concrete_model + self._get_defer_select_mask( + related_model._meta, field_mask, field_select_mask + ) else: - relation = opts.get_field(field_name) - # While virtual fields such as many-to-many and generic foreign - # keys cannot be effectively deferred we've historically - # allowed them to be passed to QuerySet.defer(). Ignore such - # field references until a layer of validation at mask - # alteration time will be implemented eventually. - if not hasattr(relation, "field"): - continue - field_select_mask = select_mask.setdefault(relation, {}) - related_model = relation.related_model._meta.concrete_model - self._get_defer_select_mask( - related_model._meta, field_mask, field_select_mask - ) + opts.get_field(field_name) return select_mask def _get_only_select_mask(self, opts, mask, select_mask=None):