mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #28586 -- Added model field fetch modes.
May your database queries be much reduced with minimal effort. co-authored-by: Andreas Pelme <andreas@pelme.se> co-authored-by: Simon Charette <charette.s@gmail.com> co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
This commit is contained in:
		
				
					committed by
					
						 Jacob Walls
						Jacob Walls
					
				
			
			
				
	
			
			
			
						parent
						
							f6bd90c840
						
					
				
				
					commit
					e097e8a12f
				
			| @@ -16,6 +16,7 @@ from django.db.models.fields.related import ( | |||||||
|     ReverseManyToOneDescriptor, |     ReverseManyToOneDescriptor, | ||||||
|     lazy_related_operation, |     lazy_related_operation, | ||||||
| ) | ) | ||||||
|  | from django.db.models.query import prefetch_related_objects | ||||||
| from django.db.models.query_utils import PathInfo | from django.db.models.query_utils import PathInfo | ||||||
| from django.db.models.sql import AND | from django.db.models.sql import AND | ||||||
| from django.db.models.sql.where import WhereNode | from django.db.models.sql.where import WhereNode | ||||||
| @@ -253,6 +254,15 @@ class GenericForeignKeyDescriptor: | |||||||
|                 return rel_obj |                 return rel_obj | ||||||
|             else: |             else: | ||||||
|                 rel_obj = None |                 rel_obj = None | ||||||
|  |  | ||||||
|  |         instance._state.fetch_mode.fetch(self, instance) | ||||||
|  |         return self.field.get_cached_value(instance) | ||||||
|  |  | ||||||
|  |     def fetch_one(self, instance): | ||||||
|  |         f = self.field.model._meta.get_field(self.field.ct_field) | ||||||
|  |         ct_id = getattr(instance, f.attname, None) | ||||||
|  |         pk_val = getattr(instance, self.field.fk_field) | ||||||
|  |         rel_obj = None | ||||||
|         if ct_id is not None: |         if ct_id is not None: | ||||||
|             ct = self.field.get_content_type(id=ct_id, using=instance._state.db) |             ct = self.field.get_content_type(id=ct_id, using=instance._state.db) | ||||||
|             try: |             try: | ||||||
| @@ -262,7 +272,11 @@ class GenericForeignKeyDescriptor: | |||||||
|             except ObjectDoesNotExist: |             except ObjectDoesNotExist: | ||||||
|                 pass |                 pass | ||||||
|         self.field.set_cached_value(instance, rel_obj) |         self.field.set_cached_value(instance, rel_obj) | ||||||
|         return rel_obj |  | ||||||
|  |     def fetch_many(self, instances): | ||||||
|  |         is_cached = self.field.is_cached | ||||||
|  |         missing_instances = [i for i in instances if not is_cached(i)] | ||||||
|  |         return prefetch_related_objects(missing_instances, self.field.name) | ||||||
|  |  | ||||||
|     def __set__(self, instance, value): |     def __set__(self, instance, value): | ||||||
|         ct = None |         ct = None | ||||||
|   | |||||||
| @@ -132,6 +132,12 @@ class FieldError(Exception): | |||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FieldFetchBlocked(FieldError): | ||||||
|  |     """On-demand fetching of a model field blocked.""" | ||||||
|  |  | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| NON_FIELD_ERRORS = "__all__" | NON_FIELD_ERRORS = "__all__" | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ from django.db.models.expressions import ( | |||||||
|     WindowFrame, |     WindowFrame, | ||||||
|     WindowFrameExclusion, |     WindowFrameExclusion, | ||||||
| ) | ) | ||||||
|  | from django.db.models.fetch_modes import FETCH_ONE, FETCH_PEERS, RAISE | ||||||
| from django.db.models.fields import *  # NOQA | from django.db.models.fields import *  # NOQA | ||||||
| from django.db.models.fields import __all__ as fields_all | from django.db.models.fields import __all__ as fields_all | ||||||
| from django.db.models.fields.composite import CompositePrimaryKey | from django.db.models.fields.composite import CompositePrimaryKey | ||||||
| @@ -105,6 +106,9 @@ __all__ += [ | |||||||
|     "GeneratedField", |     "GeneratedField", | ||||||
|     "JSONField", |     "JSONField", | ||||||
|     "OrderWrt", |     "OrderWrt", | ||||||
|  |     "FETCH_ONE", | ||||||
|  |     "FETCH_PEERS", | ||||||
|  |     "RAISE", | ||||||
|     "Lookup", |     "Lookup", | ||||||
|     "Transform", |     "Transform", | ||||||
|     "Manager", |     "Manager", | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ from django.db.models import NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, | |||||||
| from django.db.models.constants import LOOKUP_SEP | from django.db.models.constants import LOOKUP_SEP | ||||||
| from django.db.models.deletion import CASCADE, Collector | from django.db.models.deletion import CASCADE, Collector | ||||||
| from django.db.models.expressions import DatabaseDefault | from django.db.models.expressions import DatabaseDefault | ||||||
|  | from django.db.models.fetch_modes import FETCH_ONE | ||||||
| from django.db.models.fields.composite import CompositePrimaryKey | from django.db.models.fields.composite import CompositePrimaryKey | ||||||
| from django.db.models.fields.related import ( | from django.db.models.fields.related import ( | ||||||
|     ForeignObjectRel, |     ForeignObjectRel, | ||||||
| @@ -466,6 +467,14 @@ class ModelStateFieldsCacheDescriptor: | |||||||
|         return res |         return res | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ModelStateFetchModeDescriptor: | ||||||
|  |     def __get__(self, instance, cls=None): | ||||||
|  |         if instance is None: | ||||||
|  |             return self | ||||||
|  |         res = instance.fetch_mode = FETCH_ONE | ||||||
|  |         return res | ||||||
|  |  | ||||||
|  |  | ||||||
| class ModelState: | class ModelState: | ||||||
|     """Store model instance state.""" |     """Store model instance state.""" | ||||||
|  |  | ||||||
| @@ -476,6 +485,14 @@ class ModelState: | |||||||
|     # on the actual save. |     # on the actual save. | ||||||
|     adding = True |     adding = True | ||||||
|     fields_cache = ModelStateFieldsCacheDescriptor() |     fields_cache = ModelStateFieldsCacheDescriptor() | ||||||
|  |     fetch_mode = ModelStateFetchModeDescriptor() | ||||||
|  |     peers = () | ||||||
|  |  | ||||||
|  |     def __getstate__(self): | ||||||
|  |         state = self.__dict__.copy() | ||||||
|  |         # Weak references can't be pickled. | ||||||
|  |         state.pop("peers", None) | ||||||
|  |         return state | ||||||
|  |  | ||||||
|  |  | ||||||
| class Model(AltersData, metaclass=ModelBase): | class Model(AltersData, metaclass=ModelBase): | ||||||
| @@ -595,7 +612,7 @@ class Model(AltersData, metaclass=ModelBase): | |||||||
|         post_init.send(sender=cls, instance=self) |         post_init.send(sender=cls, instance=self) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def from_db(cls, db, field_names, values): |     def from_db(cls, db, field_names, values, *, fetch_mode=None): | ||||||
|         if len(values) != len(cls._meta.concrete_fields): |         if len(values) != len(cls._meta.concrete_fields): | ||||||
|             values_iter = iter(values) |             values_iter = iter(values) | ||||||
|             values = [ |             values = [ | ||||||
| @@ -605,6 +622,8 @@ class Model(AltersData, metaclass=ModelBase): | |||||||
|         new = cls(*values) |         new = cls(*values) | ||||||
|         new._state.adding = False |         new._state.adding = False | ||||||
|         new._state.db = db |         new._state.db = db | ||||||
|  |         if fetch_mode is not None: | ||||||
|  |             new._state.fetch_mode = fetch_mode | ||||||
|         return new |         return new | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
| @@ -714,8 +733,8 @@ class Model(AltersData, metaclass=ModelBase): | |||||||
|         should be an iterable of field attnames. If fields is None, then |         should be an iterable of field attnames. If fields is None, then | ||||||
|         all non-deferred fields are reloaded. |         all non-deferred fields are reloaded. | ||||||
|  |  | ||||||
|         When accessing deferred fields of an instance, the deferred loading |         When fetching deferred fields for a single instance (the FETCH_ONE | ||||||
|         of the field will call this method. |         fetch mode), the deferred loading uses this method. | ||||||
|         """ |         """ | ||||||
|         if fields is None: |         if fields is None: | ||||||
|             self._prefetched_objects_cache = {} |             self._prefetched_objects_cache = {} | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								django/db/models/fetch_modes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								django/db/models/fetch_modes.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | from django.core.exceptions import FieldFetchBlocked | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FetchMode: | ||||||
|  |     __slots__ = () | ||||||
|  |  | ||||||
|  |     track_peers = False | ||||||
|  |  | ||||||
|  |     def fetch(self, fetcher, instance): | ||||||
|  |         raise NotImplementedError("Subclasses must implement this method.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FetchOne(FetchMode): | ||||||
|  |     __slots__ = () | ||||||
|  |  | ||||||
|  |     def fetch(self, fetcher, instance): | ||||||
|  |         fetcher.fetch_one(instance) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FETCH_ONE = FetchOne() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FetchPeers(FetchMode): | ||||||
|  |     __slots__ = () | ||||||
|  |  | ||||||
|  |     track_peers = True | ||||||
|  |  | ||||||
|  |     def fetch(self, fetcher, instance): | ||||||
|  |         instances = [ | ||||||
|  |             peer | ||||||
|  |             for peer_weakref in instance._state.peers | ||||||
|  |             if (peer := peer_weakref()) is not None | ||||||
|  |         ] | ||||||
|  |         if len(instances) > 1: | ||||||
|  |             fetcher.fetch_many(instances) | ||||||
|  |         else: | ||||||
|  |             fetcher.fetch_one(instance) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FETCH_PEERS = FetchPeers() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Raise(FetchMode): | ||||||
|  |     __slots__ = () | ||||||
|  |  | ||||||
|  |     def fetch(self, fetcher, instance): | ||||||
|  |         klass = instance.__class__.__qualname__ | ||||||
|  |         field_name = fetcher.field.name | ||||||
|  |         raise FieldFetchBlocked(f"Fetching of {klass}.{field_name} blocked.") from None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | RAISE = Raise() | ||||||
| @@ -78,7 +78,7 @@ from django.db.models.expressions import ColPairs | |||||||
| from django.db.models.fields.tuple_lookups import TupleIn | from django.db.models.fields.tuple_lookups import TupleIn | ||||||
| from django.db.models.functions import RowNumber | from django.db.models.functions import RowNumber | ||||||
| from django.db.models.lookups import GreaterThan, LessThanOrEqual | from django.db.models.lookups import GreaterThan, LessThanOrEqual | ||||||
| from django.db.models.query import QuerySet | from django.db.models.query import QuerySet, prefetch_related_objects | ||||||
| from django.db.models.query_utils import DeferredAttribute | from django.db.models.query_utils import DeferredAttribute | ||||||
| from django.db.models.utils import AltersData, resolve_callables | from django.db.models.utils import AltersData, resolve_callables | ||||||
| from django.utils.functional import cached_property | from django.utils.functional import cached_property | ||||||
| @@ -254,13 +254,9 @@ class ForwardManyToOneDescriptor: | |||||||
|                             break |                             break | ||||||
|  |  | ||||||
|             if rel_obj is None and has_value: |             if rel_obj is None and has_value: | ||||||
|                 rel_obj = self.get_object(instance) |                 instance._state.fetch_mode.fetch(self, instance) | ||||||
|                 remote_field = self.field.remote_field |                 return self.field.get_cached_value(instance) | ||||||
|                 # If this is a one-to-one relation, set the reverse accessor |  | ||||||
|                 # cache on the related object to the current instance to avoid |  | ||||||
|                 # an extra SQL query if it's accessed later on. |  | ||||||
|                 if not remote_field.multiple: |  | ||||||
|                     remote_field.set_cached_value(rel_obj, instance) |  | ||||||
|             self.field.set_cached_value(instance, rel_obj) |             self.field.set_cached_value(instance, rel_obj) | ||||||
|  |  | ||||||
|         if rel_obj is None and not self.field.null: |         if rel_obj is None and not self.field.null: | ||||||
| @@ -270,6 +266,21 @@ class ForwardManyToOneDescriptor: | |||||||
|         else: |         else: | ||||||
|             return rel_obj |             return rel_obj | ||||||
|  |  | ||||||
|  |     def fetch_one(self, instance): | ||||||
|  |         rel_obj = self.get_object(instance) | ||||||
|  |         self.field.set_cached_value(instance, rel_obj) | ||||||
|  |         # If this is a one-to-one relation, set the reverse accessor cache on | ||||||
|  |         # the related object to the current instance to avoid an extra SQL | ||||||
|  |         # query if it's accessed later on. | ||||||
|  |         remote_field = self.field.remote_field | ||||||
|  |         if not remote_field.multiple: | ||||||
|  |             remote_field.set_cached_value(rel_obj, instance) | ||||||
|  |  | ||||||
|  |     def fetch_many(self, instances): | ||||||
|  |         is_cached = self.is_cached | ||||||
|  |         missing_instances = [i for i in instances if not is_cached(i)] | ||||||
|  |         prefetch_related_objects(missing_instances, self.field.name) | ||||||
|  |  | ||||||
|     def __set__(self, instance, value): |     def __set__(self, instance, value): | ||||||
|         """ |         """ | ||||||
|         Set the related instance through the forward relation. |         Set the related instance through the forward relation. | ||||||
| @@ -504,16 +515,8 @@ class ReverseOneToOneDescriptor: | |||||||
|             if not instance._is_pk_set(): |             if not instance._is_pk_set(): | ||||||
|                 rel_obj = None |                 rel_obj = None | ||||||
|             else: |             else: | ||||||
|                 filter_args = self.related.field.get_forward_related_filter(instance) |                 instance._state.fetch_mode.fetch(self, instance) | ||||||
|                 try: |                 rel_obj = self.related.get_cached_value(instance) | ||||||
|                     rel_obj = self.get_queryset(instance=instance).get(**filter_args) |  | ||||||
|                 except self.related.related_model.DoesNotExist: |  | ||||||
|                     rel_obj = None |  | ||||||
|                 else: |  | ||||||
|                     # Set the forward accessor cache on the related object to |  | ||||||
|                     # the current instance to avoid an extra SQL query if it's |  | ||||||
|                     # accessed later on. |  | ||||||
|                     self.related.field.set_cached_value(rel_obj, instance) |  | ||||||
|             self.related.set_cached_value(instance, rel_obj) |             self.related.set_cached_value(instance, rel_obj) | ||||||
|  |  | ||||||
|         if rel_obj is None: |         if rel_obj is None: | ||||||
| @@ -524,6 +527,34 @@ class ReverseOneToOneDescriptor: | |||||||
|         else: |         else: | ||||||
|             return rel_obj |             return rel_obj | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def field(self): | ||||||
|  |         """ | ||||||
|  |         Add compatibility with the fetcher protocol. While self.related is not | ||||||
|  |         a field but a OneToOneRel, it quacks enough like a field to work. | ||||||
|  |         """ | ||||||
|  |         return self.related | ||||||
|  |  | ||||||
|  |     def fetch_one(self, instance): | ||||||
|  |         # Kept for backwards compatibility with overridden | ||||||
|  |         # get_forward_related_filter() | ||||||
|  |         filter_args = self.related.field.get_forward_related_filter(instance) | ||||||
|  |         try: | ||||||
|  |             rel_obj = self.get_queryset(instance=instance).get(**filter_args) | ||||||
|  |         except self.related.related_model.DoesNotExist: | ||||||
|  |             rel_obj = None | ||||||
|  |         else: | ||||||
|  |             self.related.field.set_cached_value(rel_obj, instance) | ||||||
|  |         self.related.set_cached_value(instance, rel_obj) | ||||||
|  |  | ||||||
|  |     def fetch_many(self, instances): | ||||||
|  |         is_cached = self.is_cached | ||||||
|  |         missing_instances = [i for i in instances if not is_cached(i)] | ||||||
|  |         prefetch_related_objects( | ||||||
|  |             missing_instances, | ||||||
|  |             self.related.get_accessor_name(), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def __set__(self, instance, value): |     def __set__(self, instance, value): | ||||||
|         """ |         """ | ||||||
|         Set the related instance through the reverse relation. |         Set the related instance through the reverse relation. | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import warnings | |||||||
| from contextlib import nullcontext | from contextlib import nullcontext | ||||||
| from functools import reduce | from functools import reduce | ||||||
| from itertools import chain, islice | from itertools import chain, islice | ||||||
|  | from weakref import ref as weak_ref | ||||||
|  |  | ||||||
| from asgiref.sync import sync_to_async | from asgiref.sync import sync_to_async | ||||||
|  |  | ||||||
| @@ -26,6 +27,7 @@ from django.db.models import AutoField, DateField, DateTimeField, Field, Max, sq | |||||||
| from django.db.models.constants import LOOKUP_SEP, OnConflict | from django.db.models.constants import LOOKUP_SEP, OnConflict | ||||||
| from django.db.models.deletion import Collector | from django.db.models.deletion import Collector | ||||||
| from django.db.models.expressions import Case, DatabaseDefault, F, Value, When | from django.db.models.expressions import Case, DatabaseDefault, F, Value, When | ||||||
|  | from django.db.models.fetch_modes import FETCH_ONE | ||||||
| from django.db.models.functions import Cast, Trunc | from django.db.models.functions import Cast, Trunc | ||||||
| from django.db.models.query_utils import FilteredRelation, Q | from django.db.models.query_utils import FilteredRelation, Q | ||||||
| from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, ROW_COUNT | from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, ROW_COUNT | ||||||
| @@ -122,10 +124,18 @@ class ModelIterable(BaseIterable): | |||||||
|             ) |             ) | ||||||
|             for field, related_objs in queryset._known_related_objects.items() |             for field, related_objs in queryset._known_related_objects.items() | ||||||
|         ] |         ] | ||||||
|  |         fetch_mode = queryset._fetch_mode | ||||||
|  |         peers = [] | ||||||
|         for row in compiler.results_iter(results): |         for row in compiler.results_iter(results): | ||||||
|             obj = model_cls.from_db( |             obj = model_cls.from_db( | ||||||
|                 db, init_list, row[model_fields_start:model_fields_end] |                 db, | ||||||
|  |                 init_list, | ||||||
|  |                 row[model_fields_start:model_fields_end], | ||||||
|  |                 fetch_mode=fetch_mode, | ||||||
|             ) |             ) | ||||||
|  |             if fetch_mode.track_peers: | ||||||
|  |                 peers.append(weak_ref(obj)) | ||||||
|  |                 obj._state.peers = peers | ||||||
|             for rel_populator in related_populators: |             for rel_populator in related_populators: | ||||||
|                 rel_populator.populate(row, obj) |                 rel_populator.populate(row, obj) | ||||||
|             if annotation_col_map: |             if annotation_col_map: | ||||||
| @@ -183,10 +193,17 @@ class RawModelIterable(BaseIterable): | |||||||
|                 query_iterator = compiler.composite_fields_to_tuples( |                 query_iterator = compiler.composite_fields_to_tuples( | ||||||
|                     query_iterator, cols |                     query_iterator, cols | ||||||
|                 ) |                 ) | ||||||
|  |             fetch_mode = self.queryset._fetch_mode | ||||||
|  |             peers = [] | ||||||
|             for values in query_iterator: |             for values in query_iterator: | ||||||
|                 # Associate fields to values |                 # Associate fields to values | ||||||
|                 model_init_values = [values[pos] for pos in model_init_pos] |                 model_init_values = [values[pos] for pos in model_init_pos] | ||||||
|                 instance = model_cls.from_db(db, model_init_names, model_init_values) |                 instance = model_cls.from_db( | ||||||
|  |                     db, model_init_names, model_init_values, fetch_mode=fetch_mode | ||||||
|  |                 ) | ||||||
|  |                 if fetch_mode.track_peers: | ||||||
|  |                     peers.append(weak_ref(instance)) | ||||||
|  |                     instance._state.peers = peers | ||||||
|                 if annotation_fields: |                 if annotation_fields: | ||||||
|                     for column, pos in annotation_fields: |                     for column, pos in annotation_fields: | ||||||
|                         setattr(instance, column, values[pos]) |                         setattr(instance, column, values[pos]) | ||||||
| @@ -293,6 +310,7 @@ class QuerySet(AltersData): | |||||||
|         self._prefetch_done = False |         self._prefetch_done = False | ||||||
|         self._known_related_objects = {}  # {rel_field: {pk: rel_obj}} |         self._known_related_objects = {}  # {rel_field: {pk: rel_obj}} | ||||||
|         self._iterable_class = ModelIterable |         self._iterable_class = ModelIterable | ||||||
|  |         self._fetch_mode = FETCH_ONE | ||||||
|         self._fields = None |         self._fields = None | ||||||
|         self._defer_next_filter = False |         self._defer_next_filter = False | ||||||
|         self._deferred_filter = None |         self._deferred_filter = None | ||||||
| @@ -1442,6 +1460,7 @@ class QuerySet(AltersData): | |||||||
|             params=params, |             params=params, | ||||||
|             translations=translations, |             translations=translations, | ||||||
|             using=using, |             using=using, | ||||||
|  |             fetch_mode=self._fetch_mode, | ||||||
|         ) |         ) | ||||||
|         qs._prefetch_related_lookups = self._prefetch_related_lookups[:] |         qs._prefetch_related_lookups = self._prefetch_related_lookups[:] | ||||||
|         return qs |         return qs | ||||||
| @@ -1913,6 +1932,12 @@ class QuerySet(AltersData): | |||||||
|         clone._db = alias |         clone._db = alias | ||||||
|         return clone |         return clone | ||||||
|  |  | ||||||
|  |     def fetch_mode(self, fetch_mode): | ||||||
|  |         """Set the fetch mode for the QuerySet.""" | ||||||
|  |         clone = self._chain() | ||||||
|  |         clone._fetch_mode = fetch_mode | ||||||
|  |         return clone | ||||||
|  |  | ||||||
|     ################################### |     ################################### | ||||||
|     # PUBLIC INTROSPECTION ATTRIBUTES # |     # PUBLIC INTROSPECTION ATTRIBUTES # | ||||||
|     ################################### |     ################################### | ||||||
| @@ -2051,6 +2076,7 @@ class QuerySet(AltersData): | |||||||
|         c._prefetch_related_lookups = self._prefetch_related_lookups[:] |         c._prefetch_related_lookups = self._prefetch_related_lookups[:] | ||||||
|         c._known_related_objects = self._known_related_objects |         c._known_related_objects = self._known_related_objects | ||||||
|         c._iterable_class = self._iterable_class |         c._iterable_class = self._iterable_class | ||||||
|  |         c._fetch_mode = self._fetch_mode | ||||||
|         c._fields = self._fields |         c._fields = self._fields | ||||||
|         return c |         return c | ||||||
|  |  | ||||||
| @@ -2186,6 +2212,7 @@ class RawQuerySet: | |||||||
|         translations=None, |         translations=None, | ||||||
|         using=None, |         using=None, | ||||||
|         hints=None, |         hints=None, | ||||||
|  |         fetch_mode=FETCH_ONE, | ||||||
|     ): |     ): | ||||||
|         self.raw_query = raw_query |         self.raw_query = raw_query | ||||||
|         self.model = model |         self.model = model | ||||||
| @@ -2197,6 +2224,7 @@ class RawQuerySet: | |||||||
|         self._result_cache = None |         self._result_cache = None | ||||||
|         self._prefetch_related_lookups = () |         self._prefetch_related_lookups = () | ||||||
|         self._prefetch_done = False |         self._prefetch_done = False | ||||||
|  |         self._fetch_mode = fetch_mode | ||||||
|  |  | ||||||
|     def resolve_model_init_order(self): |     def resolve_model_init_order(self): | ||||||
|         """Resolve the init field names and value positions.""" |         """Resolve the init field names and value positions.""" | ||||||
| @@ -2295,6 +2323,7 @@ class RawQuerySet: | |||||||
|             params=self.params, |             params=self.params, | ||||||
|             translations=self.translations, |             translations=self.translations, | ||||||
|             using=alias, |             using=alias, | ||||||
|  |             fetch_mode=self._fetch_mode, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|   | |||||||
| @@ -264,7 +264,8 @@ class DeferredAttribute: | |||||||
|                         f"Cannot retrieve deferred field {field_name!r} " |                         f"Cannot retrieve deferred field {field_name!r} " | ||||||
|                         "from an unsaved model." |                         "from an unsaved model." | ||||||
|                     ) |                     ) | ||||||
|                 instance.refresh_from_db(fields=[field_name]) |  | ||||||
|  |                 instance._state.fetch_mode.fetch(self, instance) | ||||||
|             else: |             else: | ||||||
|                 data[field_name] = val |                 data[field_name] = val | ||||||
|         return data[field_name] |         return data[field_name] | ||||||
| @@ -281,6 +282,20 @@ class DeferredAttribute: | |||||||
|             return getattr(instance, link_field.attname) |             return getattr(instance, link_field.attname) | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |     def fetch_one(self, instance): | ||||||
|  |         instance.refresh_from_db(fields=[self.field.attname]) | ||||||
|  |  | ||||||
|  |     def fetch_many(self, instances): | ||||||
|  |         attname = self.field.attname | ||||||
|  |         db = instances[0]._state.db | ||||||
|  |         value_by_pk = ( | ||||||
|  |             self.field.model._base_manager.using(db) | ||||||
|  |             .values_list(attname) | ||||||
|  |             .in_bulk({i.pk for i in instances}) | ||||||
|  |         ) | ||||||
|  |         for instance in instances: | ||||||
|  |             setattr(instance, attname, value_by_pk[instance.pk]) | ||||||
|  |  | ||||||
|  |  | ||||||
| class class_or_instance_method: | class class_or_instance_method: | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -165,6 +165,16 @@ Django core exception classes are defined in ``django.core.exceptions``. | |||||||
|     - A field name is invalid |     - A field name is invalid | ||||||
|     - A query contains invalid order_by arguments |     - A query contains invalid order_by arguments | ||||||
|  |  | ||||||
|  | ``FieldFetchBlocked`` | ||||||
|  | --------------------- | ||||||
|  |  | ||||||
|  | .. versionadded:: 6.1 | ||||||
|  |  | ||||||
|  | .. exception:: FieldFetchBlocked | ||||||
|  |  | ||||||
|  |     Raised when a field would be fetched on-demand and the | ||||||
|  |     :attr:`~django.db.models.RAISE` fetch mode is active. | ||||||
|  |  | ||||||
| ``ValidationError`` | ``ValidationError`` | ||||||
| ------------------- | ------------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -180,10 +180,10 @@ update, you could write a test similar to this:: | |||||||
|         obj.refresh_from_db() |         obj.refresh_from_db() | ||||||
|         self.assertEqual(obj.val, 2) |         self.assertEqual(obj.val, 2) | ||||||
|  |  | ||||||
| Note that when deferred fields are accessed, the loading of the deferred | When a deferred field is loaded on-demand for a single model instance, the | ||||||
| field's value happens through this method. Thus it is possible to customize | loading happens through this method. Thus it is possible to customize the way | ||||||
| the way deferred loading happens. The example below shows how one can reload | this loading happens. The example below shows how one can reload all of the | ||||||
| all of the instance's fields when a deferred field is reloaded:: | instance's fields when a deferred field is loaded on-demand:: | ||||||
|  |  | ||||||
|     class ExampleModel(models.Model): |     class ExampleModel(models.Model): | ||||||
|         def refresh_from_db(self, using=None, fields=None, **kwargs): |         def refresh_from_db(self, using=None, fields=None, **kwargs): | ||||||
|   | |||||||
| @@ -1022,15 +1022,38 @@ Uses SQL's ``EXCEPT`` operator to keep only elements present in the | |||||||
|  |  | ||||||
| See :meth:`union` for some restrictions. | See :meth:`union` for some restrictions. | ||||||
|  |  | ||||||
|  | ``fetch_mode()`` | ||||||
|  | ~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
|  | .. versionadded:: 6.1 | ||||||
|  |  | ||||||
|  | .. method:: fetch_mode(mode) | ||||||
|  |  | ||||||
|  | Returns a ``QuerySet`` that sets the given fetch mode for all model instances | ||||||
|  | created by this ``QuerySet``. The fetch mode controls on-demand loading of | ||||||
|  | fields when they are accessed, such as for foreign keys and deferred fields. | ||||||
|  | For example, to use the :attr:`~django.db.models.FETCH_PEERS` mode to | ||||||
|  | batch-load all related objects on first access: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.db import models | ||||||
|  |  | ||||||
|  |     books = Book.objects.fetch_mode(models.FETCH_PEERS) | ||||||
|  |  | ||||||
|  | See more in the :doc:`fetch mode topic guide </topics/db/fetch-modes>`. | ||||||
|  |  | ||||||
| ``select_related()`` | ``select_related()`` | ||||||
| ~~~~~~~~~~~~~~~~~~~~ | ~~~~~~~~~~~~~~~~~~~~ | ||||||
|  |  | ||||||
| .. method:: select_related(*fields) | .. method:: select_related(*fields) | ||||||
|  |  | ||||||
| Returns a ``QuerySet`` that will "follow" foreign-key relationships, selecting | Returns a ``QuerySet`` that will join in the named foreign-key relationships, | ||||||
| additional related-object data when it executes its query. This is a | selecting additional related objects when it executes its query. This method | ||||||
| performance booster which results in a single more complex query but means | can be a performance booster, fetching data ahead of time rather than | ||||||
| later use of foreign-key relationships won't require database queries. | triggering on-demand loading through the model instances' | ||||||
|  | :doc:`fetch mode </topics/db/fetch-modes>`, at the cost of a more complex | ||||||
|  | initial query. | ||||||
|  |  | ||||||
| The following examples illustrate the difference between plain lookups and | The following examples illustrate the difference between plain lookups and | ||||||
| ``select_related()`` lookups. Here's standard lookup:: | ``select_related()`` lookups. Here's standard lookup:: | ||||||
| @@ -1050,20 +1073,8 @@ And here's ``select_related`` lookup:: | |||||||
|     # in the previous query. |     # in the previous query. | ||||||
|     b = e.blog |     b = e.blog | ||||||
|  |  | ||||||
| You can use ``select_related()`` with any queryset of objects:: | You can use ``select_related()`` with any queryset. The order of chaining with | ||||||
|  | other methods isn't important. For example, these querysets are equivalent:: | ||||||
|     from django.utils import timezone |  | ||||||
|  |  | ||||||
|     # Find all the blogs with entries scheduled to be published in the future. |  | ||||||
|     blogs = set() |  | ||||||
|  |  | ||||||
|     for e in Entry.objects.filter(pub_date__gt=timezone.now()).select_related("blog"): |  | ||||||
|         # Without select_related(), this would make a database query for each |  | ||||||
|         # loop iteration in order to fetch the related blog for each entry. |  | ||||||
|         blogs.add(e.blog) |  | ||||||
|  |  | ||||||
| The order of ``filter()`` and ``select_related()`` chaining isn't important. |  | ||||||
| These querysets are equivalent:: |  | ||||||
|  |  | ||||||
|     Entry.objects.filter(pub_date__gt=timezone.now()).select_related("blog") |     Entry.objects.filter(pub_date__gt=timezone.now()).select_related("blog") | ||||||
|     Entry.objects.select_related("blog").filter(pub_date__gt=timezone.now()) |     Entry.objects.select_related("blog").filter(pub_date__gt=timezone.now()) | ||||||
| @@ -1141,12 +1152,15 @@ that is that ``select_related('foo', 'bar')`` is equivalent to | |||||||
|  |  | ||||||
| .. method:: prefetch_related(*lookups) | .. method:: prefetch_related(*lookups) | ||||||
|  |  | ||||||
| Returns a ``QuerySet`` that will automatically retrieve, in a single batch, | Returns a ``QuerySet`` that will automatically retrieve the given lookups, each | ||||||
| related objects for each of the specified lookups. | in one extra batch query. Prefetching is a way to optimize database access | ||||||
|  | when you know you'll be accessing related objects later, so you can avoid | ||||||
|  | triggering the on-demand loading behavior of the model instances' | ||||||
|  | :doc:`fetch mode </topics/db/fetch-modes>`. | ||||||
|  |  | ||||||
| This has a similar purpose to ``select_related``, in that both are designed to | This method has a similar purpose to :meth:`select_related`, in that both are | ||||||
| stop the deluge of database queries that is caused by accessing related | designed to eagerly fetch related objects. However, they work in different | ||||||
| objects, but the strategy is quite different. | ways. | ||||||
|  |  | ||||||
| ``select_related`` works by creating an SQL join and including the fields of | ``select_related`` works by creating an SQL join and including the fields of | ||||||
| the related object in the ``SELECT`` statement. For this reason, | the related object in the ``SELECT`` statement. For this reason, | ||||||
|   | |||||||
| @@ -26,6 +26,51 @@ only officially support, the latest release of each series. | |||||||
| What's new in Django 6.1 | What's new in Django 6.1 | ||||||
| ======================== | ======================== | ||||||
|  |  | ||||||
|  | Model field fetch modes | ||||||
|  | ----------------------- | ||||||
|  |  | ||||||
|  | The on-demand fetching behavior of model fields is now configurable with | ||||||
|  | :doc:`fetch modes </topics/db/fetch-modes>`. These modes allow you to control | ||||||
|  | how Django fetches data from the database when an unfetched field is accessed. | ||||||
|  |  | ||||||
|  | Django provides three fetch modes: | ||||||
|  |  | ||||||
|  | 1. ``FETCH_ONE``, the default, fetches the missing field for the current | ||||||
|  |    instance only. This mode represents Django's existing behavior. | ||||||
|  |  | ||||||
|  | 2. ``FETCH_PEERS`` fetches a missing field for all instances that came from | ||||||
|  |    the same :class:`~django.db.models.query.QuerySet`. | ||||||
|  |  | ||||||
|  |    This mode works like an on-demand ``prefetch_related()``. It can reduce most | ||||||
|  |    cases of the "N+1 queries problem" to two queries without any work to | ||||||
|  |    maintain a list of fields to prefetch. | ||||||
|  |  | ||||||
|  | 3. ``RAISE`` raises a :exc:`~django.core.exceptions.FieldFetchBlocked` | ||||||
|  |    exception. | ||||||
|  |  | ||||||
|  |    This mode can prevent unintentional queries in performance-critical | ||||||
|  |    sections of code. | ||||||
|  |  | ||||||
|  | Use the new method :meth:`.QuerySet.fetch_mode` to set the fetch mode for model | ||||||
|  | instances fetched by the ``QuerySet``: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.db import models | ||||||
|  |  | ||||||
|  |     books = Book.objects.fetch_mode(models.FETCH_PEERS) | ||||||
|  |     for book in books: | ||||||
|  |         print(book.author.name) | ||||||
|  |  | ||||||
|  | Despite the loop accessing the ``author`` foreign key on each instance, the | ||||||
|  | ``FETCH_PEERS`` fetch mode will make the above example perform only two | ||||||
|  | queries: | ||||||
|  |  | ||||||
|  | 1. Fetch all books. | ||||||
|  | 2. Fetch associated authors. | ||||||
|  |  | ||||||
|  | See :doc:`fetch modes </topics/db/fetch-modes>` for more details. | ||||||
|  |  | ||||||
| Minor features | Minor features | ||||||
| -------------- | -------------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -535,6 +535,7 @@ unencrypted | |||||||
| unescape | unescape | ||||||
| unescaped | unescaped | ||||||
| unevaluated | unevaluated | ||||||
|  | unfetched | ||||||
| unglamorous | unglamorous | ||||||
| ungrouped | ungrouped | ||||||
| unhandled | unhandled | ||||||
|   | |||||||
							
								
								
									
										138
									
								
								docs/topics/db/fetch-modes.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								docs/topics/db/fetch-modes.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | =========== | ||||||
|  | Fetch modes | ||||||
|  | =========== | ||||||
|  |  | ||||||
|  | .. versionadded:: 6.1 | ||||||
|  |  | ||||||
|  | .. module:: django.db.models.fetch_modes | ||||||
|  |  | ||||||
|  | .. currentmodule:: django.db.models | ||||||
|  |  | ||||||
|  | When accessing model fields that were not loaded as part of the original query, | ||||||
|  | Django will fetch that field's data from the database. You can customize the | ||||||
|  | behavior of this fetching with a **fetch mode**, making it more efficient or | ||||||
|  | even blocking it. | ||||||
|  |  | ||||||
|  | Use :meth:`.QuerySet.fetch_mode` to set the fetch mode for model | ||||||
|  | instances fetched by a ``QuerySet``: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.db import models | ||||||
|  |  | ||||||
|  |     books = Book.objects.fetch_mode(models.FETCH_PEERS) | ||||||
|  |  | ||||||
|  | Fetch modes apply to: | ||||||
|  |  | ||||||
|  | * :class:`~django.db.models.ForeignKey` fields | ||||||
|  | * :class:`~django.db.models.OneToOneField` fields and their reverse accessors | ||||||
|  | * Fields deferred with :meth:`.QuerySet.defer` or :meth:`.QuerySet.only` | ||||||
|  | * :ref:`generic-relations` | ||||||
|  |  | ||||||
|  | Available modes | ||||||
|  | =============== | ||||||
|  |  | ||||||
|  | .. admonition:: Referencing fetch modes | ||||||
|  |  | ||||||
|  |     Fetch modes are defined in ``django.db.models.fetch_modes``, but for | ||||||
|  |     convenience they're imported into :mod:`django.db.models`. The standard | ||||||
|  |     convention is to use ``from django.db import models`` and refer to the | ||||||
|  |     fetch modes as ``models.<mode>``. | ||||||
|  |  | ||||||
|  | Django provides three fetch modes. We'll explain them below using these models: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.db import models | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     class Author(models.Model): ... | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     class Book(models.Model): | ||||||
|  |         author = models.ForeignKey(Author, on_delete=models.CASCADE) | ||||||
|  |         ... | ||||||
|  |  | ||||||
|  | …and this loop: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     for book in books: | ||||||
|  |         print(book.author.name) | ||||||
|  |  | ||||||
|  | …where ``books`` is a ``QuerySet`` of ``Book`` instances using some fetch mode. | ||||||
|  |  | ||||||
|  | .. attribute:: FETCH_ONE | ||||||
|  |  | ||||||
|  | Fetches the missing field for the current instance only. This is the default | ||||||
|  | mode. | ||||||
|  |  | ||||||
|  | Using ``FETCH_ONE`` for the above example would use: | ||||||
|  |  | ||||||
|  | * 1 query to fetch ``books`` | ||||||
|  | * N queries, where N is the number of books, to fetch the missing ``author`` | ||||||
|  |   field | ||||||
|  |  | ||||||
|  | …for a total of 1+N queries. This query pattern is known as the "N+1 queries | ||||||
|  | problem" because it often leads to performance issues when N is large. | ||||||
|  |  | ||||||
|  | .. attribute:: FETCH_PEERS | ||||||
|  |  | ||||||
|  | Fetches the missing field for the current instance and its "peers"—instances | ||||||
|  | that came from the same initial ``QuerySet``. The behavior of this mode is | ||||||
|  | based on the assumption that if you need a field for one instance, you probably | ||||||
|  | need it for all instances in the same batch, since you'll likely process them | ||||||
|  | all identically. | ||||||
|  |  | ||||||
|  | Using ``FETCH_PEERS`` for the above example would use: | ||||||
|  |  | ||||||
|  | * 1 query to fetch ``books`` | ||||||
|  | * 1 query to fetch all missing ``author`` fields for the batch of books | ||||||
|  |  | ||||||
|  | …for a total of 2 queries. The batch query makes this mode a lot more efficient | ||||||
|  | than ``FETCH_ONE`` and is similar to an on-demand call to | ||||||
|  | :meth:`.QuerySet.prefetch_related` or | ||||||
|  | :func:`~django.db.models.prefetch_related_objects`. Using ``FETCH_PEERS`` can | ||||||
|  | reduce most cases of the "N+1 queries problem" to two queries without | ||||||
|  | much effort. | ||||||
|  |  | ||||||
|  | The "peer" instances are tracked in a list of weak references, to avoid | ||||||
|  | memory leaks where some peer instances are discarded. | ||||||
|  |  | ||||||
|  | .. attribute:: RAISE | ||||||
|  |  | ||||||
|  | Raises a :exc:`~django.core.exceptions.FieldFetchBlocked` exception. | ||||||
|  |  | ||||||
|  | Using ``RAISE`` for the above example would raise an exception at the access of | ||||||
|  | ``book.author`` access, like: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     FieldFetchBlocked("Fetching of Primary.value blocked.") | ||||||
|  |  | ||||||
|  | This mode can prevent unintentional queries in performance-critical | ||||||
|  | sections of code. | ||||||
|  |  | ||||||
|  | .. _fetch-modes-custom-manager: | ||||||
|  |  | ||||||
|  | Make a fetch mode the default for a model class | ||||||
|  | =============================================== | ||||||
|  |  | ||||||
|  | Set the default fetch mode for a model class with a | ||||||
|  | :ref:`custom manager <custom-managers>` that overrides ``get_queryset()``: | ||||||
|  |  | ||||||
|  | .. code-block:: python | ||||||
|  |  | ||||||
|  |     from django.db import models | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     class BookManager(models.Manager): | ||||||
|  |         def get_queryset(self): | ||||||
|  |             return super().get_queryset().fetch_mode(models.FETCH_PEERS) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     class Book(models.Model): | ||||||
|  |         title = models.TextField() | ||||||
|  |         author = models.ForeignKey("Author", on_delete=models.CASCADE) | ||||||
|  |  | ||||||
|  |         objects = BookManager() | ||||||
| @@ -13,6 +13,7 @@ Generally, each model maps to a single database table. | |||||||
|  |  | ||||||
|    models |    models | ||||||
|    queries |    queries | ||||||
|  |    fetch-modes | ||||||
|    aggregation |    aggregation | ||||||
|    search |    search | ||||||
|    managers |    managers | ||||||
|   | |||||||
| @@ -196,28 +196,46 @@ thousands of records are returned. The penalty will be compounded if the | |||||||
| database lives on a separate server, where network overhead and latency also | database lives on a separate server, where network overhead and latency also | ||||||
| play a factor. | play a factor. | ||||||
|  |  | ||||||
| Retrieve everything at once if you know you will need it | Retrieve related objects efficiently | ||||||
| ======================================================== | ==================================== | ||||||
|  |  | ||||||
| Hitting the database multiple times for different parts of a single 'set' of | Generally, accessing the database multiple times to retrieve different parts | ||||||
| data that you will need all parts of is, in general, less efficient than | of a single "set" of data is less efficient than retrieving it all in one | ||||||
| retrieving it all in one query. This is particularly important if you have a | query. This is particularly important if you have a query that is executed in a | ||||||
| query that is executed in a loop, and could therefore end up doing many | loop, and could therefore end up doing many database queries, when only one | ||||||
| database queries, when only one was needed. So: | is needed. Below are some techniques to combine queries for efficiency. | ||||||
|  |  | ||||||
|  | Use the ``FETCH_PEERS`` fetch mode | ||||||
|  | ---------------------------------- | ||||||
|  |  | ||||||
|  | Use the :attr:`~django.db.models.FETCH_PEERS` fetch mode to make on-demand | ||||||
|  | field access more efficient with bulk-fetching. Enable all it for all usage of | ||||||
|  | your models :ref:`with a custom manager <fetch-modes-custom-manager>`. | ||||||
|  |  | ||||||
|  | Using this fetch mode is easier than declaring fields to fetch with | ||||||
|  | :meth:`~django.db.models.query.QuerySet.select_related` or | ||||||
|  | :meth:`~django.db.models.query.QuerySet.prefetch_related`, especially when it's | ||||||
|  | hard to predict which fields will be accessed. | ||||||
|  |  | ||||||
| Use ``QuerySet.select_related()`` and ``prefetch_related()`` | Use ``QuerySet.select_related()`` and ``prefetch_related()`` | ||||||
| ------------------------------------------------------------ | ------------------------------------------------------------ | ||||||
|  |  | ||||||
| Understand :meth:`~django.db.models.query.QuerySet.select_related` and | When the :attr:`~django.db.models.FETCH_PEERS` fetch mode is not appropriate or | ||||||
| :meth:`~django.db.models.query.QuerySet.prefetch_related` thoroughly, and use | efficient enough, use :meth:`~django.db.models.query.QuerySet.select_related` | ||||||
| them: | and :meth:`~django.db.models.query.QuerySet.prefetch_related`. Understand their | ||||||
|  | documentation thoroughly and apply them where needed. | ||||||
|  |  | ||||||
| * in :doc:`managers and default managers </topics/db/managers>` where | It may be useful to apply these methods in :doc:`managers and default managers | ||||||
|   appropriate. Be aware when your manager is and is not used; sometimes this is | </topics/db/managers>`. Be aware when your manager is and is not used; | ||||||
|   tricky so don't make assumptions. | sometimes this is tricky so don't make assumptions. | ||||||
|  |  | ||||||
| * in view code or other layers, possibly making use of | Use ``prefetch_related_objects()`` | ||||||
|   :func:`~django.db.models.prefetch_related_objects` where needed. | ---------------------------------- | ||||||
|  |  | ||||||
|  | Where :attr:`~django.db.models.query.QuerySet.prefetch_related` would be useful | ||||||
|  | after the queryset has been evaluated, use | ||||||
|  | :func:`~django.db.models.prefetch_related_objects` to execute an extra | ||||||
|  | prefetch. | ||||||
|  |  | ||||||
| Don't retrieve things you don't need | Don't retrieve things you don't need | ||||||
| ==================================== | ==================================== | ||||||
|   | |||||||
| @@ -1702,6 +1702,12 @@ the link from the related model to the model that defines the relationship. | |||||||
| For example, a ``Blog`` object ``b`` has a manager that returns all related | For example, a ``Blog`` object ``b`` has a manager that returns all related | ||||||
| ``Entry`` objects in the ``entry_set`` attribute: ``b.entry_set.all()``. | ``Entry`` objects in the ``entry_set`` attribute: ``b.entry_set.all()``. | ||||||
|  |  | ||||||
|  | These accessors may be prefetched by the ``QuerySet`` methods | ||||||
|  | :meth:`~django.db.models.query.QuerySet.select_related` or | ||||||
|  | :meth:`~django.db.models.query.QuerySet.prefetch_related`. If not prefetched, | ||||||
|  | access will trigger an on-demand fetch through the model's | ||||||
|  | :doc:`fetch mode </topics/db/fetch-modes>`. | ||||||
|  |  | ||||||
| All examples in this section use the sample ``Blog``, ``Author`` and ``Entry`` | All examples in this section use the sample ``Blog``, ``Author`` and ``Entry`` | ||||||
| models defined at the top of this page. | models defined at the top of this page. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -807,6 +807,7 @@ class ManagerTest(SimpleTestCase): | |||||||
|         "alatest", |         "alatest", | ||||||
|         "aupdate", |         "aupdate", | ||||||
|         "aupdate_or_create", |         "aupdate_or_create", | ||||||
|  |         "fetch_mode", | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     def test_manager_methods(self): |     def test_manager_methods(self): | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| from django.core.exceptions import FieldDoesNotExist, FieldError | from django.core.exceptions import FieldDoesNotExist, FieldError, FieldFetchBlocked | ||||||
|  | from django.db.models import FETCH_PEERS, RAISE | ||||||
| from django.test import SimpleTestCase, TestCase | from django.test import SimpleTestCase, TestCase | ||||||
|  |  | ||||||
| from .models import ( | from .models import ( | ||||||
| @@ -29,6 +30,7 @@ class DeferTests(AssertionMixin, TestCase): | |||||||
|     def setUpTestData(cls): |     def setUpTestData(cls): | ||||||
|         cls.s1 = Secondary.objects.create(first="x1", second="y1") |         cls.s1 = Secondary.objects.create(first="x1", second="y1") | ||||||
|         cls.p1 = Primary.objects.create(name="p1", value="xx", related=cls.s1) |         cls.p1 = Primary.objects.create(name="p1", value="xx", related=cls.s1) | ||||||
|  |         cls.p2 = Primary.objects.create(name="p2", value="yy", related=cls.s1) | ||||||
|  |  | ||||||
|     def test_defer(self): |     def test_defer(self): | ||||||
|         qs = Primary.objects.all() |         qs = Primary.objects.all() | ||||||
| @@ -141,7 +143,6 @@ class DeferTests(AssertionMixin, TestCase): | |||||||
|     def test_saving_object_with_deferred_field(self): |     def test_saving_object_with_deferred_field(self): | ||||||
|         # Saving models with deferred fields is possible (but inefficient, |         # Saving models with deferred fields is possible (but inefficient, | ||||||
|         # since every field has to be retrieved first). |         # since every field has to be retrieved first). | ||||||
|         Primary.objects.create(name="p2", value="xy", related=self.s1) |  | ||||||
|         obj = Primary.objects.defer("value").get(name="p2") |         obj = Primary.objects.defer("value").get(name="p2") | ||||||
|         obj.name = "a new name" |         obj.name = "a new name" | ||||||
|         obj.save() |         obj.save() | ||||||
| @@ -181,10 +182,71 @@ class DeferTests(AssertionMixin, TestCase): | |||||||
|         self.assertEqual(obj.name, "adonis") |         self.assertEqual(obj.name, "adonis") | ||||||
|  |  | ||||||
|     def test_defer_fk_attname(self): |     def test_defer_fk_attname(self): | ||||||
|         primary = Primary.objects.defer("related_id").get() |         primary = Primary.objects.defer("related_id").get(name="p1") | ||||||
|         with self.assertNumQueries(1): |         with self.assertNumQueries(1): | ||||||
|             self.assertEqual(primary.related_id, self.p1.related_id) |             self.assertEqual(primary.related_id, self.p1.related_id) | ||||||
|  |  | ||||||
|  |     def test_only_fetch_mode_fetch_peers(self): | ||||||
|  |         p1, p2 = Primary.objects.fetch_mode(FETCH_PEERS).only("name") | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             p1.value | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             p2.value | ||||||
|  |  | ||||||
|  |     def test_only_fetch_mode_fetch_peers_single(self): | ||||||
|  |         p1 = Primary.objects.fetch_mode(FETCH_PEERS).only("name").get(name="p1") | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             p1.value | ||||||
|  |  | ||||||
|  |     def test_defer_fetch_mode_fetch_peers(self): | ||||||
|  |         p1, p2 = Primary.objects.fetch_mode(FETCH_PEERS).defer("value") | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             p1.value | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             p2.value | ||||||
|  |  | ||||||
|  |     def test_defer_fetch_mode_fetch_peers_single(self): | ||||||
|  |         p1 = Primary.objects.fetch_mode(FETCH_PEERS).defer("value").get(name="p1") | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             p1.value | ||||||
|  |  | ||||||
|  |     def test_only_fetch_mode_raise(self): | ||||||
|  |         p1 = Primary.objects.fetch_mode(RAISE).only("name").get(name="p1") | ||||||
|  |         msg = "Fetching of Primary.value blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             p1.value | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |  | ||||||
|  |     def test_defer_fetch_mode_raise(self): | ||||||
|  |         p1 = Primary.objects.fetch_mode(RAISE).defer("value").get(name="p1") | ||||||
|  |         msg = "Fetching of Primary.value blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             p1.value | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class DeferOtherDatabaseTests(TestCase): | ||||||
|  |     databases = {"other"} | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def setUpTestData(cls): | ||||||
|  |         cls.s1 = Secondary.objects.using("other").create(first="x1", second="y1") | ||||||
|  |         cls.p1 = Primary.objects.using("other").create( | ||||||
|  |             name="p1", value="xx", related=cls.s1 | ||||||
|  |         ) | ||||||
|  |         cls.p2 = Primary.objects.using("other").create( | ||||||
|  |             name="p2", value="yy", related=cls.s1 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_defer_fetch_mode_fetch_peers(self): | ||||||
|  |         p1, p2 = Primary.objects.using("other").fetch_mode(FETCH_PEERS).defer("value") | ||||||
|  |         with self.assertNumQueries(1, using="other"): | ||||||
|  |             p1.value | ||||||
|  |         with self.assertNumQueries(0, using="other"): | ||||||
|  |             p2.value | ||||||
|  |  | ||||||
|  |  | ||||||
| class BigChildDeferTests(AssertionMixin, TestCase): | class BigChildDeferTests(AssertionMixin, TestCase): | ||||||
|     @classmethod |     @classmethod | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.contrib.contenttypes.prefetch import GenericPrefetch | from django.contrib.contenttypes.prefetch import GenericPrefetch | ||||||
| from django.core.exceptions import FieldError | from django.core.exceptions import FieldError, FieldFetchBlocked | ||||||
| from django.db.models import Q, prefetch_related_objects | from django.db.models import Q, prefetch_related_objects | ||||||
|  | from django.db.models.fetch_modes import FETCH_PEERS, RAISE | ||||||
| from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature | from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature | ||||||
|  |  | ||||||
| from .models import ( | from .models import ( | ||||||
| @@ -780,6 +781,46 @@ class GenericRelationsTests(TestCase): | |||||||
|                 self.platypus.latin_name, |                 self.platypus.latin_name, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_fetch_peers(self): | ||||||
|  |         TaggedItem.objects.bulk_create( | ||||||
|  |             [ | ||||||
|  |                 TaggedItem(tag="lion", content_object=self.lion), | ||||||
|  |                 TaggedItem(tag="platypus", content_object=self.platypus), | ||||||
|  |                 TaggedItem(tag="quartz", content_object=self.quartz), | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |         # Peers fetching should fetch all related peers GFKs at once which is | ||||||
|  |         # one query per content type. | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             quartz_tag, platypus_tag, lion_tag = TaggedItem.objects.fetch_mode( | ||||||
|  |                 FETCH_PEERS | ||||||
|  |             ).order_by("-pk")[:3] | ||||||
|  |         with self.assertNumQueries(2): | ||||||
|  |             self.assertEqual(lion_tag.content_object, self.lion) | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             self.assertEqual(platypus_tag.content_object, self.platypus) | ||||||
|  |             self.assertEqual(quartz_tag.content_object, self.quartz) | ||||||
|  |         # It should ignore already cached instances though. | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             quartz_tag, platypus_tag, lion_tag = TaggedItem.objects.fetch_mode( | ||||||
|  |                 FETCH_PEERS | ||||||
|  |             ).order_by("-pk")[:3] | ||||||
|  |         with self.assertNumQueries(2): | ||||||
|  |             self.assertEqual(quartz_tag.content_object, self.quartz) | ||||||
|  |             self.assertEqual(lion_tag.content_object, self.lion) | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             self.assertEqual(platypus_tag.content_object, self.platypus) | ||||||
|  |             self.assertEqual(quartz_tag.content_object, self.quartz) | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_raise(self): | ||||||
|  |         TaggedItem.objects.create(tag="lion", content_object=self.lion) | ||||||
|  |         tag = TaggedItem.objects.fetch_mode(RAISE).get(tag="yellow") | ||||||
|  |         msg = "Fetching of TaggedItem.content_object blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             tag.content_object | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProxyRelatedModelTest(TestCase): | class ProxyRelatedModelTest(TestCase): | ||||||
|     def test_default_behavior(self): |     def test_default_behavior(self): | ||||||
|   | |||||||
| @@ -1,8 +1,13 @@ | |||||||
| import datetime | import datetime | ||||||
| from copy import deepcopy | from copy import deepcopy | ||||||
|  |  | ||||||
| from django.core.exceptions import FieldError, MultipleObjectsReturned | from django.core.exceptions import ( | ||||||
|  |     FieldError, | ||||||
|  |     FieldFetchBlocked, | ||||||
|  |     MultipleObjectsReturned, | ||||||
|  | ) | ||||||
| from django.db import IntegrityError, models, transaction | from django.db import IntegrityError, models, transaction | ||||||
|  | from django.db.models import FETCH_PEERS, RAISE | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils.translation import gettext_lazy | from django.utils.translation import gettext_lazy | ||||||
|  |  | ||||||
| @@ -916,3 +921,23 @@ class ManyToOneTests(TestCase): | |||||||
|                 instances=countries, |                 instances=countries, | ||||||
|                 querysets=[City.objects.all(), City.objects.all()], |                 querysets=[City.objects.all(), City.objects.all()], | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_fetch_peers_forward(self): | ||||||
|  |         Article.objects.create( | ||||||
|  |             headline="This is another test", | ||||||
|  |             pub_date=datetime.date(2005, 7, 27), | ||||||
|  |             reporter=self.r2, | ||||||
|  |         ) | ||||||
|  |         a1, a2 = Article.objects.fetch_mode(FETCH_PEERS) | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             a1.reporter | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             a2.reporter | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_raise_forward(self): | ||||||
|  |         a = Article.objects.fetch_mode(RAISE).get(pk=self.a.pk) | ||||||
|  |         msg = "Fetching of Article.reporter blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             a.reporter | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
|  | from django.core.exceptions import FieldFetchBlocked | ||||||
| from django.db import IntegrityError, connection, transaction | from django.db import IntegrityError, connection, transaction | ||||||
|  | from django.db.models import FETCH_PEERS, RAISE | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from .models import ( | from .models import ( | ||||||
| @@ -619,3 +621,39 @@ class OneToOneTests(TestCase): | |||||||
|                 instances=places, |                 instances=places, | ||||||
|                 querysets=[Bar.objects.all(), Bar.objects.all()], |                 querysets=[Bar.objects.all(), Bar.objects.all()], | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_fetch_peers_forward(self): | ||||||
|  |         Restaurant.objects.create( | ||||||
|  |             place=self.p2, serves_hot_dogs=True, serves_pizza=False | ||||||
|  |         ) | ||||||
|  |         r1, r2 = Restaurant.objects.fetch_mode(FETCH_PEERS) | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             r1.place | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             r2.place | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_fetch_peers_reverse(self): | ||||||
|  |         Restaurant.objects.create( | ||||||
|  |             place=self.p2, serves_hot_dogs=True, serves_pizza=False | ||||||
|  |         ) | ||||||
|  |         p1, p2 = Place.objects.fetch_mode(FETCH_PEERS) | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             p1.restaurant | ||||||
|  |         with self.assertNumQueries(0): | ||||||
|  |             p2.restaurant | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_raise_forward(self): | ||||||
|  |         r = Restaurant.objects.fetch_mode(RAISE).get(pk=self.r1.pk) | ||||||
|  |         msg = "Fetching of Restaurant.place blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             r.place | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_raise_reverse(self): | ||||||
|  |         p = Place.objects.fetch_mode(RAISE).get(pk=self.p1.pk) | ||||||
|  |         msg = "Fetching of Place.restaurant blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             p.restaurant | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType | |||||||
| from django.core.exceptions import ObjectDoesNotExist | from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.db import NotSupportedError, connection | from django.db import NotSupportedError, connection | ||||||
| from django.db.models import F, Prefetch, QuerySet, prefetch_related_objects | from django.db.models import F, Prefetch, QuerySet, prefetch_related_objects | ||||||
|  | from django.db.models.fetch_modes import RAISE | ||||||
| from django.db.models.query import get_prefetcher | from django.db.models.query import get_prefetcher | ||||||
| from django.db.models.sql import Query | from django.db.models.sql import Query | ||||||
| from django.test import ( | from django.test import ( | ||||||
| @@ -107,6 +108,10 @@ class PrefetchRelatedTests(TestDataMixin, TestCase): | |||||||
|         normal_books = [a.first_book for a in Author.objects.all()] |         normal_books = [a.first_book for a in Author.objects.all()] | ||||||
|         self.assertEqual(books, normal_books) |         self.assertEqual(books, normal_books) | ||||||
|  |  | ||||||
|  |     def test_fetch_mode_raise(self): | ||||||
|  |         authors = list(Author.objects.fetch_mode(RAISE).prefetch_related("first_book")) | ||||||
|  |         authors[0].first_book  # No exception, already loaded | ||||||
|  |  | ||||||
|     def test_foreignkey_reverse(self): |     def test_foreignkey_reverse(self): | ||||||
|         with self.assertNumQueries(2): |         with self.assertNumQueries(2): | ||||||
|             [ |             [ | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| from datetime import date | from datetime import date | ||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
|  |  | ||||||
| from django.core.exceptions import FieldDoesNotExist | from django.core.exceptions import FieldDoesNotExist, FieldFetchBlocked | ||||||
|  | from django.db.models import FETCH_PEERS, RAISE | ||||||
| from django.db.models.query import RawQuerySet | from django.db.models.query import RawQuerySet | ||||||
| from django.test import TestCase, skipUnlessDBFeature | from django.test import TestCase, skipUnlessDBFeature | ||||||
|  |  | ||||||
| @@ -158,6 +159,22 @@ class RawQueryTests(TestCase): | |||||||
|         books = Book.objects.all() |         books = Book.objects.all() | ||||||
|         self.assertSuccessfulRawQuery(Book, query, books) |         self.assertSuccessfulRawQuery(Book, query, books) | ||||||
|  |  | ||||||
|  |     def test_fk_fetch_mode_peers(self): | ||||||
|  |         query = "SELECT * FROM raw_query_book" | ||||||
|  |         books = list(Book.objects.fetch_mode(FETCH_PEERS).raw(query)) | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             books[0].author | ||||||
|  |             books[1].author | ||||||
|  |  | ||||||
|  |     def test_fk_fetch_mode_raise(self): | ||||||
|  |         query = "SELECT * FROM raw_query_book" | ||||||
|  |         books = list(Book.objects.fetch_mode(RAISE).raw(query)) | ||||||
|  |         msg = "Fetching of Book.author blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             books[0].author | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |  | ||||||
|     def test_db_column_handler(self): |     def test_db_column_handler(self): | ||||||
|         """ |         """ | ||||||
|         Test of a simple raw query against a model containing a field with |         Test of a simple raw query against a model containing a field with | ||||||
| @@ -294,6 +311,23 @@ class RawQueryTests(TestCase): | |||||||
|         with self.assertRaisesMessage(FieldDoesNotExist, msg): |         with self.assertRaisesMessage(FieldDoesNotExist, msg): | ||||||
|             list(Author.objects.raw(query)) |             list(Author.objects.raw(query)) | ||||||
|  |  | ||||||
|  |     def test_missing_fields_fetch_mode_peers(self): | ||||||
|  |         query = "SELECT id, first_name, dob FROM raw_query_author" | ||||||
|  |         authors = list(Author.objects.fetch_mode(FETCH_PEERS).raw(query)) | ||||||
|  |         with self.assertNumQueries(1): | ||||||
|  |             authors[0].last_name | ||||||
|  |             authors[1].last_name | ||||||
|  |  | ||||||
|  |     def test_missing_fields_fetch_mode_raise(self): | ||||||
|  |         query = "SELECT id, first_name, dob FROM raw_query_author" | ||||||
|  |         authors = list(Author.objects.fetch_mode(RAISE).raw(query)) | ||||||
|  |         msg = "Fetching of Author.last_name blocked." | ||||||
|  |         with self.assertRaisesMessage(FieldFetchBlocked, msg) as cm: | ||||||
|  |             authors[0].last_name | ||||||
|  |         self.assertIsNone(cm.exception.__cause__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |         self.assertTrue(cm.exception.__suppress_context__) | ||||||
|  |  | ||||||
|     def test_annotations(self): |     def test_annotations(self): | ||||||
|         query = ( |         query = ( | ||||||
|             "SELECT a.*, count(b.id) as book_count " |             "SELECT a.*, count(b.id) as book_count " | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user