summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaulo <commonzenpython@gmail.com>2016-06-04 16:10:37 -0700
committerTim Graham <timograham@gmail.com>2017-08-10 12:06:02 -0400
commitbfb746f983aa741afa3709794e70f1e0ab6040b5 (patch)
tree48b4a70b1d5119d75157dcae5283f881117fd0a4
parent22ff86ec52bb536ad9d1fa3ee0c9a3d5eb9925c1 (diff)
Refs #16043 -- Refactored internal fields value cache.
* Removed all hardcoded logic for _{fieldname}_cache. * Added an internal API for interacting with the field values cache. Thanks carljm and MarkusH for support.
-rw-r--r--django/contrib/contenttypes/fields.py58
-rw-r--r--django/db/models/base.py24
-rw-r--r--django/db/models/fields/__init__.py3
-rw-r--r--django/db/models/fields/mixins.py26
-rw-r--r--django/db/models/fields/related.py6
-rw-r--r--django/db/models/fields/related_descriptors.py80
-rw-r--r--django/db/models/fields/reverse_related.py13
-rw-r--r--django/db/models/query.py45
-rw-r--r--tests/foreign_object/models/article.py4
-rw-r--r--tests/many_to_one/tests.py2
-rw-r--r--tests/one_to_one/tests.py2
11 files changed, 156 insertions, 107 deletions
diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py
index 83944ecfe2..0884089d92 100644
--- a/django/contrib/contenttypes/fields.py
+++ b/django/contrib/contenttypes/fields.py
@@ -7,6 +7,7 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
from django.db import DEFAULT_DB_ALIAS, models, router, transaction
from django.db.models import DO_NOTHING
from django.db.models.base import ModelBase, make_foreign_order_accessors
+from django.db.models.fields.mixins import FieldCacheMixin
from django.db.models.fields.related import (
ForeignObject, ForeignObjectRel, ReverseManyToOneDescriptor,
lazy_related_operation,
@@ -15,7 +16,7 @@ from django.db.models.query_utils import PathInfo
from django.utils.functional import cached_property
-class GenericForeignKey:
+class GenericForeignKey(FieldCacheMixin):
"""
Provide a generic many-to-one relation through the ``content_type`` and
``object_id`` fields.
@@ -49,7 +50,6 @@ class GenericForeignKey:
def contribute_to_class(self, cls, name, **kwargs):
self.name = name
self.model = cls
- self.cache_attr = "_%s_cache" % name
cls._meta.add_field(self, private=True)
setattr(cls, name, self)
@@ -156,6 +156,9 @@ class GenericForeignKey:
else:
return []
+ def get_cache_name(self):
+ return self.name
+
def get_content_type(self, obj=None, id=None, using=None):
if obj is not None:
return ContentType.objects.db_manager(obj._state.db).get_for_model(
@@ -203,14 +206,14 @@ class GenericForeignKey:
return (model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
model)
- return (ret_val,
- lambda obj: (obj.pk, obj.__class__),
- gfk_key,
- True,
- self.name)
-
- def is_cached(self, instance):
- return hasattr(instance, self.cache_attr)
+ return (
+ ret_val,
+ lambda obj: (obj.pk, obj.__class__),
+ gfk_key,
+ True,
+ self.name,
+ True,
+ )
def __get__(self, instance, cls=None):
if instance is None:
@@ -224,23 +227,19 @@ class GenericForeignKey:
ct_id = getattr(instance, f.get_attname(), None)
pk_val = getattr(instance, self.fk_field)
- try:
- rel_obj = getattr(instance, self.cache_attr)
- except AttributeError:
- rel_obj = None
- else:
- if rel_obj and (ct_id != self.get_content_type(obj=rel_obj, using=instance._state.db).id or
- rel_obj._meta.pk.to_python(pk_val) != rel_obj.pk):
- rel_obj = None
-
+ rel_obj = self.get_cached_value(instance, default=None)
if rel_obj is not None:
- return rel_obj
-
+ ct_match = ct_id == self.get_content_type(obj=rel_obj, using=instance._state.db).id
+ pk_match = rel_obj._meta.pk.to_python(pk_val) == rel_obj.pk
+ if ct_match and pk_match:
+ return rel_obj
+ else:
+ rel_obj = None
if ct_id is not None:
ct = self.get_content_type(id=ct_id, using=instance._state.db)
with suppress(ObjectDoesNotExist):
rel_obj = ct.get_object_for_this_type(pk=pk_val)
- setattr(instance, self.cache_attr, rel_obj)
+ self.set_cached_value(instance, rel_obj)
return rel_obj
def __set__(self, instance, value):
@@ -252,7 +251,7 @@ class GenericForeignKey:
setattr(instance, self.ct_field, ct)
setattr(instance, self.fk_field, fk)
- setattr(instance, self.cache_attr, value)
+ self.set_cached_value(instance, value)
class GenericRel(ForeignObjectRel):
@@ -534,11 +533,14 @@ def create_generic_related_manager(superclass, rel):
# We (possibly) need to convert object IDs to the type of the
# instances' PK in order to match up instances:
object_id_converter = instances[0]._meta.pk.to_python
- return (queryset.filter(**query),
- lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)),
- lambda obj: obj.pk,
- False,
- self.prefetch_cache_name)
+ return (
+ queryset.filter(**query),
+ lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)),
+ lambda obj: obj.pk,
+ False,
+ self.prefetch_cache_name,
+ False,
+ )
def add(self, *objs, bulk=True):
db = router.db_for_write(self.model, instance=self.instance)
diff --git a/django/db/models/base.py b/django/db/models/base.py
index da82a6446f..dda2248308 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -378,6 +378,7 @@ class ModelState:
# Necessary for correct validation of new instances of objects with explicit (non-auto) PKs.
# This impacts validation only; it has no effect on the actual save.
self.adding = True
+ self.fields_cache = {}
class Model(metaclass=ModelBase):
@@ -607,12 +608,12 @@ class Model(metaclass=ModelBase):
continue
setattr(self, field.attname, getattr(db_instance, field.attname))
# Throw away stale foreign key references.
- if field.is_relation and field.get_cache_name() in self.__dict__:
- rel_instance = getattr(self, field.get_cache_name())
+ if field.is_relation and field.is_cached(self):
+ rel_instance = field.get_cached_value(self)
local_val = getattr(db_instance, field.attname)
related_val = None if rel_instance is None else getattr(rel_instance, field.target_field.attname)
if local_val != related_val or (local_val is None and related_val is None):
- del self.__dict__[field.get_cache_name()]
+ field.delete_cached_value(self)
self._state.db = db_instance._state.db
def serializable_value(self, field_name):
@@ -646,13 +647,9 @@ class Model(metaclass=ModelBase):
# a ForeignKey or OneToOneField on this model. If the field is
# nullable, allowing the save() would result in silent data loss.
for field in self._meta.concrete_fields:
- if field.is_relation:
- # If the related field isn't cached, then an instance hasn't
- # been assigned and there's no need to worry about this check.
- try:
- getattr(self, field.get_cache_name())
- except AttributeError:
- continue
+ # If the related field isn't cached, then an instance hasn't
+ # been assigned and there's no need to worry about this check.
+ if field.is_relation and field.is_cached(self):
obj = getattr(self, field.name, None)
# A pk may have been assigned manually to a model instance not
# saved to the database (or auto-generated in a case like
@@ -663,7 +660,7 @@ class Model(metaclass=ModelBase):
if obj and obj.pk is None:
# Remove the object from a related instance cache.
if not field.remote_field.multiple:
- delattr(obj, field.remote_field.get_cache_name())
+ field.remote_field.delete_cached_value(obj)
raise ValueError(
"save() prohibited to prevent data loss due to "
"unsaved related object '%s'." % field.name
@@ -773,9 +770,8 @@ class Model(metaclass=ModelBase):
# the related object cache, in case it's been accidentally
# populated. A fresh instance will be re-built from the
# database if necessary.
- cache_name = field.get_cache_name()
- if hasattr(self, cache_name):
- delattr(self, cache_name)
+ if field.is_cached(self):
+ field.delete_cached_value(self)
def _save_table(self, raw=False, cls=None, force_insert=False,
force_update=False, using=None, update_fields=None):
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 1cfd0294e1..d4ccddc726 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -734,9 +734,6 @@ class Field(RegisterLookupMixin):
column = self.db_column or attname
return attname, column
- def get_cache_name(self):
- return '_%s_cache' % self.name
-
def get_internal_type(self):
return self.__class__.__name__
diff --git a/django/db/models/fields/mixins.py b/django/db/models/fields/mixins.py
new file mode 100644
index 0000000000..c6dbc4ec5e
--- /dev/null
+++ b/django/db/models/fields/mixins.py
@@ -0,0 +1,26 @@
+NOT_PROVIDED = object()
+
+
+class FieldCacheMixin:
+ """Provide an API for working with the model's fields value cache."""
+
+ def get_cache_name(self):
+ raise NotImplementedError
+
+ def get_cached_value(self, instance, default=NOT_PROVIDED):
+ cache_name = self.get_cache_name()
+ try:
+ return instance._state.fields_cache[cache_name]
+ except KeyError:
+ if default is NOT_PROVIDED:
+ raise
+ return default
+
+ def is_cached(self, instance):
+ return self.get_cache_name() in instance._state.fields_cache
+
+ def set_cached_value(self, instance, value):
+ instance._state.fields_cache[self.get_cache_name()] = value
+
+ def delete_cached_value(self, instance):
+ del instance._state.fields_cache[self.get_cache_name()]
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 50efa2974c..f6b87b7de8 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -16,6 +16,7 @@ from django.utils.functional import cached_property, curry
from django.utils.translation import gettext_lazy as _
from . import Field
+from .mixins import FieldCacheMixin
from .related_descriptors import (
ForwardManyToOneDescriptor, ForwardOneToOneDescriptor,
ManyToManyDescriptor, ReverseManyToOneDescriptor,
@@ -78,7 +79,7 @@ def lazy_related_operation(function, model, *related_models, **kwargs):
return apps.lazy_model_operation(partial(function, **kwargs), *model_keys)
-class RelatedField(Field):
+class RelatedField(FieldCacheMixin, Field):
"""Base class that all relational fields inherit from."""
# Field flags
@@ -438,6 +439,9 @@ class RelatedField(Field):
"The relation has multiple target fields, but only single target field was asked for")
return target_fields[0]
+ def get_cache_name(self):
+ return self.name
+
class ForeignObject(RelatedField):
"""
diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py
index 4371e44c0c..d65af02f32 100644
--- a/django/db/models/fields/related_descriptors.py
+++ b/django/db/models/fields/related_descriptors.py
@@ -86,7 +86,6 @@ class ForwardManyToOneDescriptor:
def __init__(self, field_with_rel):
self.field = field_with_rel
- self.cache_name = self.field.get_cache_name()
@cached_property
def RelatedObjectDoesNotExist(self):
@@ -100,7 +99,7 @@ class ForwardManyToOneDescriptor:
)
def is_cached(self, instance):
- return hasattr(instance, self.cache_name)
+ return self.field.is_cached(instance)
def get_queryset(self, **hints):
return self.field.remote_field.model._base_manager.db_manager(hints=hints).all()
@@ -114,6 +113,7 @@ class ForwardManyToOneDescriptor:
instance_attr = self.field.get_local_related_value
instances_dict = {instance_attr(inst): inst for inst in instances}
related_field = self.field.foreign_related_fields[0]
+ remote_field = self.field.remote_field
# FIXME: This will need to be revisited when we introduce support for
# composite fields. In the meantime we take this practical approach to
@@ -121,7 +121,7 @@ class ForwardManyToOneDescriptor:
# (related_name ends with a '+'). Refs #21410.
# The check for len(...) == 1 is a special case that allows the query
# to be join-less and smaller. Refs #21760.
- if self.field.remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
+ if remote_field.is_hidden() or len(self.field.foreign_related_fields) == 1:
query = {'%s__in' % related_field.name: {instance_attr(inst)[0] for inst in instances}}
else:
query = {'%s__in' % self.field.related_query_name(): instances}
@@ -129,12 +129,11 @@ class ForwardManyToOneDescriptor:
# Since we're going to assign directly in the cache,
# we must manage the reverse relation cache manually.
- if not self.field.remote_field.multiple:
- rel_obj_cache_name = self.field.remote_field.get_cache_name()
+ if not remote_field.multiple:
for rel_obj in queryset:
instance = instances_dict[rel_obj_attr(rel_obj)]
- setattr(rel_obj, rel_obj_cache_name, instance)
- return queryset, rel_obj_attr, instance_attr, True, self.cache_name
+ remote_field.set_cached_value(rel_obj, instance)
+ return queryset, rel_obj_attr, instance_attr, True, self.field.get_cache_name(), False
def get_object(self, instance):
qs = self.get_queryset(instance=instance)
@@ -154,23 +153,24 @@ class ForwardManyToOneDescriptor:
if instance is None:
return self
- # The related instance is loaded from the database and then cached in
- # the attribute defined in self.cache_name. It can also be pre-cached
+ # The related instance is loaded from the database and then cached
+ # by the field on the model instance state. It can also be pre-cached
# by the reverse accessor (ReverseOneToOneDescriptor).
try:
- rel_obj = getattr(instance, self.cache_name)
- except AttributeError:
+ rel_obj = self.field.get_cached_value(instance)
+ except KeyError:
val = self.field.get_local_related_value(instance)
if None in val:
rel_obj = None
else:
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 self.field.remote_field.multiple:
- setattr(rel_obj, self.field.remote_field.get_cache_name(), instance)
- setattr(instance, self.cache_name, rel_obj)
+ if not remote_field.multiple:
+ remote_field.set_cached_value(rel_obj, instance)
+ self.field.set_cached_value(instance, rel_obj)
if rel_obj is None and not self.field.null:
raise self.RelatedObjectDoesNotExist(
@@ -208,6 +208,7 @@ class ForwardManyToOneDescriptor:
if not router.allow_relation(value, instance):
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
+ remote_field = self.field.remote_field
# If we're setting the value of a OneToOneField to None, we need to clear
# out the cache on any old related object. Otherwise, deleting the
# previously-related object will also cause this object to be deleted,
@@ -219,13 +220,13 @@ class ForwardManyToOneDescriptor:
# populated the cache, then we don't care - we're only accessing
# the object to invalidate the accessor cache, so there's no
# need to populate the cache just to expire it again.
- related = getattr(instance, self.cache_name, None)
+ related = self.field.get_cached_value(instance, default=None)
# If we've got an old related object, we need to clear out its
# cache. This cache also might not exist if the related object
# hasn't been accessed yet.
if related is not None:
- setattr(related, self.field.remote_field.get_cache_name(), None)
+ remote_field.set_cached_value(related, None)
for lh_field, rh_field in self.field.related_fields:
setattr(instance, lh_field.attname, None)
@@ -237,13 +238,13 @@ class ForwardManyToOneDescriptor:
# Set the related instance cache used by __get__ to avoid an SQL query
# when accessing the attribute we just set.
- setattr(instance, self.cache_name, value)
+ self.field.set_cached_value(instance, value)
# 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 value is not None and not self.field.remote_field.multiple:
- setattr(value, self.field.remote_field.get_cache_name(), instance)
+ if value is not None and not remote_field.multiple:
+ remote_field.set_cached_value(value, instance)
class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor):
@@ -308,8 +309,9 @@ class ReverseOneToOneDescriptor:
"""
def __init__(self, related):
+ # Following the example above, `related` is an instance of OneToOneRel
+ # which represents the reverse restaurant field (place.restaurant).
self.related = related
- self.cache_name = related.get_cache_name()
@cached_property
def RelatedObjectDoesNotExist(self):
@@ -322,7 +324,7 @@ class ReverseOneToOneDescriptor:
)
def is_cached(self, instance):
- return hasattr(instance, self.cache_name)
+ return self.related.is_cached(instance)
def get_queryset(self, **hints):
return self.related.related_model._base_manager.db_manager(hints=hints).all()
@@ -343,11 +345,10 @@ class ReverseOneToOneDescriptor:
# Since we're going to assign directly in the cache,
# we must manage the reverse relation cache manually.
- rel_obj_cache_name = self.related.field.get_cache_name()
for rel_obj in queryset:
instance = instances_dict[rel_obj_attr(rel_obj)]
- setattr(rel_obj, rel_obj_cache_name, instance)
- return queryset, rel_obj_attr, instance_attr, True, self.cache_name
+ self.related.field.set_cached_value(rel_obj, instance)
+ return queryset, rel_obj_attr, instance_attr, True, self.related.get_cache_name(), False
def __get__(self, instance, cls=None):
"""
@@ -364,12 +365,12 @@ class ReverseOneToOneDescriptor:
if instance is None:
return self
- # The related instance is loaded from the database and then cached in
- # the attribute defined in self.cache_name. It can also be pre-cached
+ # The related instance is loaded from the database and then cached
+ # by the field on the model instance state. It can also be pre-cached
# by the forward accessor (ForwardManyToOneDescriptor).
try:
- rel_obj = getattr(instance, self.cache_name)
- except AttributeError:
+ rel_obj = self.related.get_cached_value(instance)
+ except KeyError:
related_pk = instance.pk
if related_pk is None:
rel_obj = None
@@ -383,8 +384,8 @@ class ReverseOneToOneDescriptor:
# 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.
- setattr(rel_obj, self.related.field.get_cache_name(), instance)
- setattr(instance, self.cache_name, rel_obj)
+ self.related.field.set_cached_value(rel_obj, instance)
+ self.related.set_cached_value(instance, rel_obj)
if rel_obj is None:
raise self.RelatedObjectDoesNotExist(
@@ -415,11 +416,17 @@ class ReverseOneToOneDescriptor:
if value is None:
# Update the cached related instance (if any) & clear the cache.
try:
- rel_obj = getattr(instance, self.cache_name)
- except AttributeError:
+ # Following the example above, this would be the cached
+ # ``restaurant`` instance (if any).
+ rel_obj = self.related.get_cached_value(instance)
+ except KeyError:
pass
else:
- delattr(instance, self.cache_name)
+ # Remove the ``restaurant`` instance from the ``place``
+ # instance cache.
+ self.related.delete_cached_value(instance)
+ # Set the ``place`` field on the ``restaurant``
+ # instance to None.
setattr(rel_obj, self.related.field.name, None)
elif not isinstance(value, self.related.related_model):
# An object must be an instance of the related class.
@@ -447,11 +454,11 @@ class ReverseOneToOneDescriptor:
# Set the related instance cache used by __get__ to avoid an SQL query
# when accessing the attribute we just set.
- setattr(instance, self.cache_name, value)
+ self.related.set_cached_value(instance, value)
# 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.
- setattr(value, self.related.field.get_cache_name(), instance)
+ self.related.field.set_cached_value(value, instance)
class ReverseManyToOneDescriptor:
@@ -584,7 +591,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
instance = instances_dict[rel_obj_attr(rel_obj)]
setattr(rel_obj, self.field.name, instance)
cache_name = self.field.related_query_name()
- return queryset, rel_obj_attr, instance_attr, False, cache_name
+ return queryset, rel_obj_attr, instance_attr, False, cache_name, False
def add(self, *objs, bulk=True):
self._remove_prefetched_objects()
@@ -882,6 +889,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
),
False,
self.prefetch_cache_name,
+ False,
)
def add(self, *objs):
diff --git a/django/db/models/fields/reverse_related.py b/django/db/models/fields/reverse_related.py
index ab8b3ffe58..1f42375566 100644
--- a/django/db/models/fields/reverse_related.py
+++ b/django/db/models/fields/reverse_related.py
@@ -13,9 +13,10 @@ from django.core import exceptions
from django.utils.functional import cached_property
from . import BLANK_CHOICE_DASH
+from .mixins import FieldCacheMixin
-class ForeignObjectRel:
+class ForeignObjectRel(FieldCacheMixin):
"""
Used by ForeignObject to store information about the relation.
@@ -162,12 +163,16 @@ class ForeignObjectRel:
return self.related_name
return opts.model_name + ('_set' if self.multiple else '')
- def get_cache_name(self):
- return "_%s_cache" % self.get_accessor_name()
-
def get_path_info(self):
return self.field.get_reverse_path_info()
+ def get_cache_name(self):
+ """
+ Return the name of the cache key to use for storing an instance of the
+ forward model on the reverse model.
+ """
+ return self.get_accessor_name()
+
class ManyToOneRel(ForeignObjectRel):
"""
diff --git a/django/db/models/query.py b/django/db/models/query.py
index ecfb93039b..4e6e65b0f8 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -72,7 +72,7 @@ class ModelIterable(BaseIterable):
if queryset._known_related_objects:
for field, rel_objs in queryset._known_related_objects.items():
# Avoid overwriting objects loaded e.g. by select_related
- if hasattr(obj, field.get_cache_name()):
+ if field.is_cached(obj):
continue
pk = getattr(obj, field.get_attname())
try:
@@ -1544,12 +1544,13 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
# callable that gets value to be matched for returned instances,
# callable that gets value to be matched for passed in instances,
# boolean that is True for singly related objects,
- # cache name to assign to).
+ # cache or field name to assign to,
+ # boolean that is True when the previous argument is a cache name vs a field name).
# The 'values to be matched' must be hashable as they will be used
# in a dictionary.
- rel_qs, rel_obj_attr, instance_attr, single, cache_name = (
+ rel_qs, rel_obj_attr, instance_attr, single, cache_name, is_descriptor = (
prefetcher.get_prefetch_queryset(instances, lookup.get_current_queryset(level)))
# We have to handle the possibility that the QuerySet we just got back
# contains some prefetch_related lookups. We don't want to trigger the
@@ -1597,8 +1598,18 @@ def prefetch_one_level(instances, prefetcher, lookup, level):
if single:
val = vals[0] if vals else None
- to_attr = to_attr if as_attr else cache_name
- setattr(obj, to_attr, val)
+ if as_attr:
+ # A to_attr has been given for the prefetch.
+ setattr(obj, to_attr, val)
+ elif is_descriptor:
+ # cache_name points to a field name in obj.
+ # This field is a descriptor for a related object.
+ setattr(obj, cache_name, val)
+ else:
+ # No to_attr has been given for this prefetch operation and the
+ # cache_name does not point to a descriptor. Store the value of
+ # the field in the object's field cache.
+ obj._state.fields_cache[cache_name] = val
else:
if as_attr:
setattr(obj, to_attr, vals)
@@ -1653,9 +1664,9 @@ class RelatedPopulator:
# model's fields.
# - related_populators: a list of RelatedPopulator instances if
# select_related() descends to related models from this model.
- # - cache_name, reverse_cache_name: the names to use for setattr
- # when assigning the fetched object to the from_obj. If the
- # reverse_cache_name is set, then we also set the reverse link.
+ # - field, remote_field: the fields to use for populating the
+ # internal fields cache. If remote_field is set then we also
+ # set the reverse link.
select_fields = klass_info['select_fields']
from_parent = klass_info['from_parent']
if not from_parent:
@@ -1674,16 +1685,16 @@ class RelatedPopulator:
self.model_cls = klass_info['model']
self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
self.related_populators = get_related_populators(klass_info, select, self.db)
- field = klass_info['field']
reverse = klass_info['reverse']
- self.reverse_cache_name = None
+ field = klass_info['field']
+ self.remote_field = None
if reverse:
- self.cache_name = field.remote_field.get_cache_name()
- self.reverse_cache_name = field.get_cache_name()
+ self.field = field.remote_field
+ self.remote_field = field
else:
- self.cache_name = field.get_cache_name()
+ self.field = field
if field.unique:
- self.reverse_cache_name = field.remote_field.get_cache_name()
+ self.remote_field = field.remote_field
def populate(self, row, from_obj):
if self.reorder_for_init:
@@ -1697,9 +1708,9 @@ class RelatedPopulator:
if obj and self.related_populators:
for rel_iter in self.related_populators:
rel_iter.populate(row, obj)
- setattr(from_obj, self.cache_name, obj)
- if obj and self.reverse_cache_name:
- setattr(obj, self.reverse_cache_name, from_obj)
+ self.field.set_cached_value(from_obj, obj)
+ if obj and self.remote_field:
+ self.remote_field.set_cached_value(obj, from_obj)
def get_related_populators(klass_info, select, db):
diff --git a/tests/foreign_object/models/article.py b/tests/foreign_object/models/article.py
index 103661a4a3..10f92d9505 100644
--- a/tests/foreign_object/models/article.py
+++ b/tests/foreign_object/models/article.py
@@ -10,9 +10,9 @@ class ArticleTranslationDescriptor(ForwardManyToOneDescriptor):
def __set__(self, instance, value):
if instance is None:
raise AttributeError("%s must be accessed via instance" % self.field.name)
- setattr(instance, self.cache_name, value)
+ self.field.set_cached_value(instance, value)
if value is not None and not self.field.remote_field.multiple:
- setattr(value, self.field.related.get_cache_name(), instance)
+ self.field.remote_field.set_cached_value(value, instance)
class ColConstraint:
diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py
index aab050234c..5052c6d471 100644
--- a/tests/many_to_one/tests.py
+++ b/tests/many_to_one/tests.py
@@ -461,7 +461,7 @@ class ManyToOneTests(TestCase):
self.assertIs(c.parent, p)
# But if we kill the cache, we get a new object.
- del c._parent_cache
+ del c._state.fields_cache['parent']
self.assertIsNot(c.parent, p)
# Assigning a new object results in that object getting cached immediately.
diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py
index 82a061b736..65257cf7dc 100644
--- a/tests/one_to_one/tests.py
+++ b/tests/one_to_one/tests.py
@@ -207,7 +207,7 @@ class OneToOneTests(TestCase):
self.assertIs(p.restaurant, r)
# But if we kill the cache, we get a new object
- del p._restaurant_cache
+ del p._state.fields_cache['restaurant']
self.assertIsNot(p.restaurant, r)
# Reassigning the Restaurant object results in an immediate cache update