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
parent
f6bd90c840
commit
e097e8a12f
@@ -16,6 +16,7 @@ from django.db.models.fields.related import (
|
||||
ReverseManyToOneDescriptor,
|
||||
lazy_related_operation,
|
||||
)
|
||||
from django.db.models.query import prefetch_related_objects
|
||||
from django.db.models.query_utils import PathInfo
|
||||
from django.db.models.sql import AND
|
||||
from django.db.models.sql.where import WhereNode
|
||||
@@ -253,6 +254,15 @@ class GenericForeignKeyDescriptor:
|
||||
return rel_obj
|
||||
else:
|
||||
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:
|
||||
ct = self.field.get_content_type(id=ct_id, using=instance._state.db)
|
||||
try:
|
||||
@@ -262,7 +272,11 @@ class GenericForeignKeyDescriptor:
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
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):
|
||||
ct = None
|
||||
|
||||
@@ -132,6 +132,12 @@ class FieldError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FieldFetchBlocked(FieldError):
|
||||
"""On-demand fetching of a model field blocked."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
NON_FIELD_ERRORS = "__all__"
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from django.db.models.expressions import (
|
||||
WindowFrame,
|
||||
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 __all__ as fields_all
|
||||
from django.db.models.fields.composite import CompositePrimaryKey
|
||||
@@ -105,6 +106,9 @@ __all__ += [
|
||||
"GeneratedField",
|
||||
"JSONField",
|
||||
"OrderWrt",
|
||||
"FETCH_ONE",
|
||||
"FETCH_PEERS",
|
||||
"RAISE",
|
||||
"Lookup",
|
||||
"Transform",
|
||||
"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.deletion import CASCADE, Collector
|
||||
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.related import (
|
||||
ForeignObjectRel,
|
||||
@@ -466,6 +467,14 @@ class ModelStateFieldsCacheDescriptor:
|
||||
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:
|
||||
"""Store model instance state."""
|
||||
|
||||
@@ -476,6 +485,14 @@ class ModelState:
|
||||
# on the actual save.
|
||||
adding = True
|
||||
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):
|
||||
@@ -595,7 +612,7 @@ class Model(AltersData, metaclass=ModelBase):
|
||||
post_init.send(sender=cls, instance=self)
|
||||
|
||||
@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):
|
||||
values_iter = iter(values)
|
||||
values = [
|
||||
@@ -605,6 +622,8 @@ class Model(AltersData, metaclass=ModelBase):
|
||||
new = cls(*values)
|
||||
new._state.adding = False
|
||||
new._state.db = db
|
||||
if fetch_mode is not None:
|
||||
new._state.fetch_mode = fetch_mode
|
||||
return new
|
||||
|
||||
def __repr__(self):
|
||||
@@ -714,8 +733,8 @@ class Model(AltersData, metaclass=ModelBase):
|
||||
should be an iterable of field attnames. If fields is None, then
|
||||
all non-deferred fields are reloaded.
|
||||
|
||||
When accessing deferred fields of an instance, the deferred loading
|
||||
of the field will call this method.
|
||||
When fetching deferred fields for a single instance (the FETCH_ONE
|
||||
fetch mode), the deferred loading uses this method.
|
||||
"""
|
||||
if fields is None:
|
||||
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.functions import RowNumber
|
||||
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.utils import AltersData, resolve_callables
|
||||
from django.utils.functional import cached_property
|
||||
@@ -254,13 +254,9 @@ class ForwardManyToOneDescriptor:
|
||||
break
|
||||
|
||||
if rel_obj is None and has_value:
|
||||
rel_obj = self.get_object(instance)
|
||||
remote_field = self.field.remote_field
|
||||
# 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)
|
||||
instance._state.fetch_mode.fetch(self, instance)
|
||||
return self.field.get_cached_value(instance)
|
||||
|
||||
self.field.set_cached_value(instance, rel_obj)
|
||||
|
||||
if rel_obj is None and not self.field.null:
|
||||
@@ -270,6 +266,21 @@ class ForwardManyToOneDescriptor:
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Set the related instance through the forward relation.
|
||||
@@ -504,16 +515,8 @@ class ReverseOneToOneDescriptor:
|
||||
if not instance._is_pk_set():
|
||||
rel_obj = None
|
||||
else:
|
||||
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:
|
||||
# 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)
|
||||
instance._state.fetch_mode.fetch(self, instance)
|
||||
rel_obj = self.related.get_cached_value(instance)
|
||||
self.related.set_cached_value(instance, rel_obj)
|
||||
|
||||
if rel_obj is None:
|
||||
@@ -524,6 +527,34 @@ class ReverseOneToOneDescriptor:
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Set the related instance through the reverse relation.
|
||||
|
||||
@@ -8,6 +8,7 @@ import warnings
|
||||
from contextlib import nullcontext
|
||||
from functools import reduce
|
||||
from itertools import chain, islice
|
||||
from weakref import ref as weak_ref
|
||||
|
||||
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.deletion import Collector
|
||||
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.query_utils import FilteredRelation, Q
|
||||
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()
|
||||
]
|
||||
fetch_mode = queryset._fetch_mode
|
||||
peers = []
|
||||
for row in compiler.results_iter(results):
|
||||
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:
|
||||
rel_populator.populate(row, obj)
|
||||
if annotation_col_map:
|
||||
@@ -183,10 +193,17 @@ class RawModelIterable(BaseIterable):
|
||||
query_iterator = compiler.composite_fields_to_tuples(
|
||||
query_iterator, cols
|
||||
)
|
||||
fetch_mode = self.queryset._fetch_mode
|
||||
peers = []
|
||||
for values in query_iterator:
|
||||
# Associate fields to values
|
||||
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:
|
||||
for column, pos in annotation_fields:
|
||||
setattr(instance, column, values[pos])
|
||||
@@ -293,6 +310,7 @@ class QuerySet(AltersData):
|
||||
self._prefetch_done = False
|
||||
self._known_related_objects = {} # {rel_field: {pk: rel_obj}}
|
||||
self._iterable_class = ModelIterable
|
||||
self._fetch_mode = FETCH_ONE
|
||||
self._fields = None
|
||||
self._defer_next_filter = False
|
||||
self._deferred_filter = None
|
||||
@@ -1442,6 +1460,7 @@ class QuerySet(AltersData):
|
||||
params=params,
|
||||
translations=translations,
|
||||
using=using,
|
||||
fetch_mode=self._fetch_mode,
|
||||
)
|
||||
qs._prefetch_related_lookups = self._prefetch_related_lookups[:]
|
||||
return qs
|
||||
@@ -1913,6 +1932,12 @@ class QuerySet(AltersData):
|
||||
clone._db = alias
|
||||
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 #
|
||||
###################################
|
||||
@@ -2051,6 +2076,7 @@ class QuerySet(AltersData):
|
||||
c._prefetch_related_lookups = self._prefetch_related_lookups[:]
|
||||
c._known_related_objects = self._known_related_objects
|
||||
c._iterable_class = self._iterable_class
|
||||
c._fetch_mode = self._fetch_mode
|
||||
c._fields = self._fields
|
||||
return c
|
||||
|
||||
@@ -2186,6 +2212,7 @@ class RawQuerySet:
|
||||
translations=None,
|
||||
using=None,
|
||||
hints=None,
|
||||
fetch_mode=FETCH_ONE,
|
||||
):
|
||||
self.raw_query = raw_query
|
||||
self.model = model
|
||||
@@ -2197,6 +2224,7 @@ class RawQuerySet:
|
||||
self._result_cache = None
|
||||
self._prefetch_related_lookups = ()
|
||||
self._prefetch_done = False
|
||||
self._fetch_mode = fetch_mode
|
||||
|
||||
def resolve_model_init_order(self):
|
||||
"""Resolve the init field names and value positions."""
|
||||
@@ -2295,6 +2323,7 @@ class RawQuerySet:
|
||||
params=self.params,
|
||||
translations=self.translations,
|
||||
using=alias,
|
||||
fetch_mode=self._fetch_mode,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -264,7 +264,8 @@ class DeferredAttribute:
|
||||
f"Cannot retrieve deferred field {field_name!r} "
|
||||
"from an unsaved model."
|
||||
)
|
||||
instance.refresh_from_db(fields=[field_name])
|
||||
|
||||
instance._state.fetch_mode.fetch(self, instance)
|
||||
else:
|
||||
data[field_name] = val
|
||||
return data[field_name]
|
||||
@@ -281,6 +282,20 @@ class DeferredAttribute:
|
||||
return getattr(instance, link_field.attname)
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -165,6 +165,16 @@ Django core exception classes are defined in ``django.core.exceptions``.
|
||||
- A field name is invalid
|
||||
- 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``
|
||||
-------------------
|
||||
|
||||
|
||||
@@ -180,10 +180,10 @@ update, you could write a test similar to this::
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.val, 2)
|
||||
|
||||
Note that when deferred fields are accessed, the loading of the deferred
|
||||
field's value happens through this method. Thus it is possible to customize
|
||||
the way deferred loading happens. The example below shows how one can reload
|
||||
all of the instance's fields when a deferred field is reloaded::
|
||||
When a deferred field is loaded on-demand for a single model instance, the
|
||||
loading happens through this method. Thus it is possible to customize the way
|
||||
this loading happens. The example below shows how one can reload all of the
|
||||
instance's fields when a deferred field is loaded on-demand::
|
||||
|
||||
class ExampleModel(models.Model):
|
||||
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.
|
||||
|
||||
``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()``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. method:: select_related(*fields)
|
||||
|
||||
Returns a ``QuerySet`` that will "follow" foreign-key relationships, selecting
|
||||
additional related-object data when it executes its query. This is a
|
||||
performance booster which results in a single more complex query but means
|
||||
later use of foreign-key relationships won't require database queries.
|
||||
Returns a ``QuerySet`` that will join in the named foreign-key relationships,
|
||||
selecting additional related objects when it executes its query. This method
|
||||
can be a performance booster, fetching data ahead of time rather than
|
||||
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
|
||||
``select_related()`` lookups. Here's standard lookup::
|
||||
@@ -1050,20 +1073,8 @@ And here's ``select_related`` lookup::
|
||||
# in the previous query.
|
||||
b = e.blog
|
||||
|
||||
You can use ``select_related()`` with any queryset of objects::
|
||||
|
||||
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::
|
||||
You can use ``select_related()`` with any queryset. The order of chaining with
|
||||
other methods isn't important. For example, these querysets are equivalent::
|
||||
|
||||
Entry.objects.filter(pub_date__gt=timezone.now()).select_related("blog")
|
||||
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)
|
||||
|
||||
Returns a ``QuerySet`` that will automatically retrieve, in a single batch,
|
||||
related objects for each of the specified lookups.
|
||||
Returns a ``QuerySet`` that will automatically retrieve the given lookups, each
|
||||
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
|
||||
stop the deluge of database queries that is caused by accessing related
|
||||
objects, but the strategy is quite different.
|
||||
This method has a similar purpose to :meth:`select_related`, in that both are
|
||||
designed to eagerly fetch related objects. However, they work in different
|
||||
ways.
|
||||
|
||||
``select_related`` works by creating an SQL join and including the fields of
|
||||
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
|
||||
========================
|
||||
|
||||
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
|
||||
--------------
|
||||
|
||||
|
||||
@@ -535,6 +535,7 @@ unencrypted
|
||||
unescape
|
||||
unescaped
|
||||
unevaluated
|
||||
unfetched
|
||||
unglamorous
|
||||
ungrouped
|
||||
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
|
||||
queries
|
||||
fetch-modes
|
||||
aggregation
|
||||
search
|
||||
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
|
||||
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
|
||||
data that you will need all parts of is, in general, less efficient than
|
||||
retrieving it all in one query. This is particularly important if you have a
|
||||
query that is executed in a loop, and could therefore end up doing many
|
||||
database queries, when only one was needed. So:
|
||||
Generally, accessing the database multiple times to retrieve different parts
|
||||
of a single "set" of data is less efficient than retrieving it all in one
|
||||
query. This is particularly important if you have a query that is executed in a
|
||||
loop, and could therefore end up doing many database queries, when only one
|
||||
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()``
|
||||
------------------------------------------------------------
|
||||
|
||||
Understand :meth:`~django.db.models.query.QuerySet.select_related` and
|
||||
:meth:`~django.db.models.query.QuerySet.prefetch_related` thoroughly, and use
|
||||
them:
|
||||
When the :attr:`~django.db.models.FETCH_PEERS` fetch mode is not appropriate or
|
||||
efficient enough, use :meth:`~django.db.models.query.QuerySet.select_related`
|
||||
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
|
||||
appropriate. Be aware when your manager is and is not used; sometimes this is
|
||||
tricky so don't make assumptions.
|
||||
It may be useful to apply these methods in :doc:`managers and default managers
|
||||
</topics/db/managers>`. Be aware when your manager is and is not used;
|
||||
sometimes this is tricky so don't make assumptions.
|
||||
|
||||
* in view code or other layers, possibly making use of
|
||||
:func:`~django.db.models.prefetch_related_objects` where needed.
|
||||
Use ``prefetch_related_objects()``
|
||||
----------------------------------
|
||||
|
||||
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
|
||||
====================================
|
||||
|
||||
@@ -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
|
||||
``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``
|
||||
models defined at the top of this page.
|
||||
|
||||
|
||||
@@ -807,6 +807,7 @@ class ManagerTest(SimpleTestCase):
|
||||
"alatest",
|
||||
"aupdate",
|
||||
"aupdate_or_create",
|
||||
"fetch_mode",
|
||||
]
|
||||
|
||||
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 .models import (
|
||||
@@ -29,6 +30,7 @@ class DeferTests(AssertionMixin, TestCase):
|
||||
def setUpTestData(cls):
|
||||
cls.s1 = Secondary.objects.create(first="x1", second="y1")
|
||||
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):
|
||||
qs = Primary.objects.all()
|
||||
@@ -141,7 +143,6 @@ class DeferTests(AssertionMixin, TestCase):
|
||||
def test_saving_object_with_deferred_field(self):
|
||||
# Saving models with deferred fields is possible (but inefficient,
|
||||
# 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.name = "a new name"
|
||||
obj.save()
|
||||
@@ -181,10 +182,71 @@ class DeferTests(AssertionMixin, TestCase):
|
||||
self.assertEqual(obj.name, "adonis")
|
||||
|
||||
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):
|
||||
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):
|
||||
@classmethod
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
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.fetch_modes import FETCH_PEERS, RAISE
|
||||
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
|
||||
|
||||
from .models import (
|
||||
@@ -780,6 +781,46 @@ class GenericRelationsTests(TestCase):
|
||||
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):
|
||||
def test_default_behavior(self):
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import datetime
|
||||
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.models import FETCH_PEERS, RAISE
|
||||
from django.test import TestCase
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
@@ -916,3 +921,23 @@ class ManyToOneTests(TestCase):
|
||||
instances=countries,
|
||||
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.models import FETCH_PEERS, RAISE
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import (
|
||||
@@ -619,3 +621,39 @@ class OneToOneTests(TestCase):
|
||||
instances=places,
|
||||
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.db import NotSupportedError, connection
|
||||
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.sql import Query
|
||||
from django.test import (
|
||||
@@ -107,6 +108,10 @@ class PrefetchRelatedTests(TestDataMixin, TestCase):
|
||||
normal_books = [a.first_book for a in Author.objects.all()]
|
||||
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):
|
||||
with self.assertNumQueries(2):
|
||||
[
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from datetime import date
|
||||
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.test import TestCase, skipUnlessDBFeature
|
||||
|
||||
@@ -158,6 +159,22 @@ class RawQueryTests(TestCase):
|
||||
books = Book.objects.all()
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
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):
|
||||
query = (
|
||||
"SELECT a.*, count(b.id) as book_count "
|
||||
|
||||
Reference in New Issue
Block a user