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.
This commit is contained in:
parent
22ff86ec52
commit
bfb746f983
|
@ -7,6 +7,7 @@ from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
|
||||||
from django.db import DEFAULT_DB_ALIAS, models, router, transaction
|
from django.db import DEFAULT_DB_ALIAS, models, router, transaction
|
||||||
from django.db.models import DO_NOTHING
|
from django.db.models import DO_NOTHING
|
||||||
from django.db.models.base import ModelBase, make_foreign_order_accessors
|
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 (
|
from django.db.models.fields.related import (
|
||||||
ForeignObject, ForeignObjectRel, ReverseManyToOneDescriptor,
|
ForeignObject, ForeignObjectRel, ReverseManyToOneDescriptor,
|
||||||
lazy_related_operation,
|
lazy_related_operation,
|
||||||
|
@ -15,7 +16,7 @@ from django.db.models.query_utils import PathInfo
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
class GenericForeignKey:
|
class GenericForeignKey(FieldCacheMixin):
|
||||||
"""
|
"""
|
||||||
Provide a generic many-to-one relation through the ``content_type`` and
|
Provide a generic many-to-one relation through the ``content_type`` and
|
||||||
``object_id`` fields.
|
``object_id`` fields.
|
||||||
|
@ -49,7 +50,6 @@ class GenericForeignKey:
|
||||||
def contribute_to_class(self, cls, name, **kwargs):
|
def contribute_to_class(self, cls, name, **kwargs):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.model = cls
|
self.model = cls
|
||||||
self.cache_attr = "_%s_cache" % name
|
|
||||||
cls._meta.add_field(self, private=True)
|
cls._meta.add_field(self, private=True)
|
||||||
setattr(cls, name, self)
|
setattr(cls, name, self)
|
||||||
|
|
||||||
|
@ -156,6 +156,9 @@ class GenericForeignKey:
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_cache_name(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
def get_content_type(self, obj=None, id=None, using=None):
|
def get_content_type(self, obj=None, id=None, using=None):
|
||||||
if obj is not None:
|
if obj is not None:
|
||||||
return ContentType.objects.db_manager(obj._state.db).get_for_model(
|
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)),
|
return (model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
|
||||||
model)
|
model)
|
||||||
|
|
||||||
return (ret_val,
|
return (
|
||||||
lambda obj: (obj.pk, obj.__class__),
|
ret_val,
|
||||||
gfk_key,
|
lambda obj: (obj.pk, obj.__class__),
|
||||||
True,
|
gfk_key,
|
||||||
self.name)
|
True,
|
||||||
|
self.name,
|
||||||
def is_cached(self, instance):
|
True,
|
||||||
return hasattr(instance, self.cache_attr)
|
)
|
||||||
|
|
||||||
def __get__(self, instance, cls=None):
|
def __get__(self, instance, cls=None):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
|
@ -224,23 +227,19 @@ class GenericForeignKey:
|
||||||
ct_id = getattr(instance, f.get_attname(), None)
|
ct_id = getattr(instance, f.get_attname(), None)
|
||||||
pk_val = getattr(instance, self.fk_field)
|
pk_val = getattr(instance, self.fk_field)
|
||||||
|
|
||||||
try:
|
rel_obj = self.get_cached_value(instance, default=None)
|
||||||
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
|
|
||||||
|
|
||||||
if rel_obj is not 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:
|
if ct_id is not None:
|
||||||
ct = self.get_content_type(id=ct_id, using=instance._state.db)
|
ct = self.get_content_type(id=ct_id, using=instance._state.db)
|
||||||
with suppress(ObjectDoesNotExist):
|
with suppress(ObjectDoesNotExist):
|
||||||
rel_obj = ct.get_object_for_this_type(pk=pk_val)
|
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
|
return rel_obj
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
|
@ -252,7 +251,7 @@ class GenericForeignKey:
|
||||||
|
|
||||||
setattr(instance, self.ct_field, ct)
|
setattr(instance, self.ct_field, ct)
|
||||||
setattr(instance, self.fk_field, fk)
|
setattr(instance, self.fk_field, fk)
|
||||||
setattr(instance, self.cache_attr, value)
|
self.set_cached_value(instance, value)
|
||||||
|
|
||||||
|
|
||||||
class GenericRel(ForeignObjectRel):
|
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
|
# We (possibly) need to convert object IDs to the type of the
|
||||||
# instances' PK in order to match up instances:
|
# instances' PK in order to match up instances:
|
||||||
object_id_converter = instances[0]._meta.pk.to_python
|
object_id_converter = instances[0]._meta.pk.to_python
|
||||||
return (queryset.filter(**query),
|
return (
|
||||||
lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)),
|
queryset.filter(**query),
|
||||||
lambda obj: obj.pk,
|
lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)),
|
||||||
False,
|
lambda obj: obj.pk,
|
||||||
self.prefetch_cache_name)
|
False,
|
||||||
|
self.prefetch_cache_name,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
|
||||||
def add(self, *objs, bulk=True):
|
def add(self, *objs, bulk=True):
|
||||||
db = router.db_for_write(self.model, instance=self.instance)
|
db = router.db_for_write(self.model, instance=self.instance)
|
||||||
|
|
|
@ -378,6 +378,7 @@ class ModelState:
|
||||||
# Necessary for correct validation of new instances of objects with explicit (non-auto) PKs.
|
# 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.
|
# This impacts validation only; it has no effect on the actual save.
|
||||||
self.adding = True
|
self.adding = True
|
||||||
|
self.fields_cache = {}
|
||||||
|
|
||||||
|
|
||||||
class Model(metaclass=ModelBase):
|
class Model(metaclass=ModelBase):
|
||||||
|
@ -607,12 +608,12 @@ class Model(metaclass=ModelBase):
|
||||||
continue
|
continue
|
||||||
setattr(self, field.attname, getattr(db_instance, field.attname))
|
setattr(self, field.attname, getattr(db_instance, field.attname))
|
||||||
# Throw away stale foreign key references.
|
# Throw away stale foreign key references.
|
||||||
if field.is_relation and field.get_cache_name() in self.__dict__:
|
if field.is_relation and field.is_cached(self):
|
||||||
rel_instance = getattr(self, field.get_cache_name())
|
rel_instance = field.get_cached_value(self)
|
||||||
local_val = getattr(db_instance, field.attname)
|
local_val = getattr(db_instance, field.attname)
|
||||||
related_val = None if rel_instance is None else getattr(rel_instance, field.target_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):
|
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
|
self._state.db = db_instance._state.db
|
||||||
|
|
||||||
def serializable_value(self, field_name):
|
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
|
# a ForeignKey or OneToOneField on this model. If the field is
|
||||||
# nullable, allowing the save() would result in silent data loss.
|
# nullable, allowing the save() would result in silent data loss.
|
||||||
for field in self._meta.concrete_fields:
|
for field in self._meta.concrete_fields:
|
||||||
if field.is_relation:
|
# If the related field isn't cached, then an instance hasn't
|
||||||
# If the related field isn't cached, then an instance hasn't
|
# been assigned and there's no need to worry about this check.
|
||||||
# been assigned and there's no need to worry about this check.
|
if field.is_relation and field.is_cached(self):
|
||||||
try:
|
|
||||||
getattr(self, field.get_cache_name())
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
obj = getattr(self, field.name, None)
|
obj = getattr(self, field.name, None)
|
||||||
# A pk may have been assigned manually to a model instance not
|
# A pk may have been assigned manually to a model instance not
|
||||||
# saved to the database (or auto-generated in a case like
|
# 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:
|
if obj and obj.pk is None:
|
||||||
# Remove the object from a related instance cache.
|
# Remove the object from a related instance cache.
|
||||||
if not field.remote_field.multiple:
|
if not field.remote_field.multiple:
|
||||||
delattr(obj, field.remote_field.get_cache_name())
|
field.remote_field.delete_cached_value(obj)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"save() prohibited to prevent data loss due to "
|
"save() prohibited to prevent data loss due to "
|
||||||
"unsaved related object '%s'." % field.name
|
"unsaved related object '%s'." % field.name
|
||||||
|
@ -773,9 +770,8 @@ class Model(metaclass=ModelBase):
|
||||||
# the related object cache, in case it's been accidentally
|
# the related object cache, in case it's been accidentally
|
||||||
# populated. A fresh instance will be re-built from the
|
# populated. A fresh instance will be re-built from the
|
||||||
# database if necessary.
|
# database if necessary.
|
||||||
cache_name = field.get_cache_name()
|
if field.is_cached(self):
|
||||||
if hasattr(self, cache_name):
|
field.delete_cached_value(self)
|
||||||
delattr(self, cache_name)
|
|
||||||
|
|
||||||
def _save_table(self, raw=False, cls=None, force_insert=False,
|
def _save_table(self, raw=False, cls=None, force_insert=False,
|
||||||
force_update=False, using=None, update_fields=None):
|
force_update=False, using=None, update_fields=None):
|
||||||
|
|
|
@ -734,9 +734,6 @@ class Field(RegisterLookupMixin):
|
||||||
column = self.db_column or attname
|
column = self.db_column or attname
|
||||||
return attname, column
|
return attname, column
|
||||||
|
|
||||||
def get_cache_name(self):
|
|
||||||
return '_%s_cache' % self.name
|
|
||||||
|
|
||||||
def get_internal_type(self):
|
def get_internal_type(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
|
|
@ -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()]
|
|
@ -16,6 +16,7 @@ from django.utils.functional import cached_property, curry
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from . import Field
|
from . import Field
|
||||||
|
from .mixins import FieldCacheMixin
|
||||||
from .related_descriptors import (
|
from .related_descriptors import (
|
||||||
ForwardManyToOneDescriptor, ForwardOneToOneDescriptor,
|
ForwardManyToOneDescriptor, ForwardOneToOneDescriptor,
|
||||||
ManyToManyDescriptor, ReverseManyToOneDescriptor,
|
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)
|
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."""
|
"""Base class that all relational fields inherit from."""
|
||||||
|
|
||||||
# Field flags
|
# Field flags
|
||||||
|
@ -438,6 +439,9 @@ class RelatedField(Field):
|
||||||
"The relation has multiple target fields, but only single target field was asked for")
|
"The relation has multiple target fields, but only single target field was asked for")
|
||||||
return target_fields[0]
|
return target_fields[0]
|
||||||
|
|
||||||
|
def get_cache_name(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class ForeignObject(RelatedField):
|
class ForeignObject(RelatedField):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -86,7 +86,6 @@ class ForwardManyToOneDescriptor:
|
||||||
|
|
||||||
def __init__(self, field_with_rel):
|
def __init__(self, field_with_rel):
|
||||||
self.field = field_with_rel
|
self.field = field_with_rel
|
||||||
self.cache_name = self.field.get_cache_name()
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def RelatedObjectDoesNotExist(self):
|
def RelatedObjectDoesNotExist(self):
|
||||||
|
@ -100,7 +99,7 @@ class ForwardManyToOneDescriptor:
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_name)
|
return self.field.is_cached(instance)
|
||||||
|
|
||||||
def get_queryset(self, **hints):
|
def get_queryset(self, **hints):
|
||||||
return self.field.remote_field.model._base_manager.db_manager(hints=hints).all()
|
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
|
instance_attr = self.field.get_local_related_value
|
||||||
instances_dict = {instance_attr(inst): inst for inst in instances}
|
instances_dict = {instance_attr(inst): inst for inst in instances}
|
||||||
related_field = self.field.foreign_related_fields[0]
|
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
|
# FIXME: This will need to be revisited when we introduce support for
|
||||||
# composite fields. In the meantime we take this practical approach to
|
# composite fields. In the meantime we take this practical approach to
|
||||||
|
@ -121,7 +121,7 @@ class ForwardManyToOneDescriptor:
|
||||||
# (related_name ends with a '+'). Refs #21410.
|
# (related_name ends with a '+'). Refs #21410.
|
||||||
# The check for len(...) == 1 is a special case that allows the query
|
# The check for len(...) == 1 is a special case that allows the query
|
||||||
# to be join-less and smaller. Refs #21760.
|
# 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}}
|
query = {'%s__in' % related_field.name: {instance_attr(inst)[0] for inst in instances}}
|
||||||
else:
|
else:
|
||||||
query = {'%s__in' % self.field.related_query_name(): instances}
|
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,
|
# Since we're going to assign directly in the cache,
|
||||||
# we must manage the reverse relation cache manually.
|
# we must manage the reverse relation cache manually.
|
||||||
if not self.field.remote_field.multiple:
|
if not remote_field.multiple:
|
||||||
rel_obj_cache_name = self.field.remote_field.get_cache_name()
|
|
||||||
for rel_obj in queryset:
|
for rel_obj in queryset:
|
||||||
instance = instances_dict[rel_obj_attr(rel_obj)]
|
instance = instances_dict[rel_obj_attr(rel_obj)]
|
||||||
setattr(rel_obj, rel_obj_cache_name, instance)
|
remote_field.set_cached_value(rel_obj, instance)
|
||||||
return queryset, rel_obj_attr, instance_attr, True, self.cache_name
|
return queryset, rel_obj_attr, instance_attr, True, self.field.get_cache_name(), False
|
||||||
|
|
||||||
def get_object(self, instance):
|
def get_object(self, instance):
|
||||||
qs = self.get_queryset(instance=instance)
|
qs = self.get_queryset(instance=instance)
|
||||||
|
@ -154,23 +153,24 @@ class ForwardManyToOneDescriptor:
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# The related instance is loaded from the database and then cached in
|
# The related instance is loaded from the database and then cached
|
||||||
# the attribute defined in self.cache_name. It can also be pre-cached
|
# by the field on the model instance state. It can also be pre-cached
|
||||||
# by the reverse accessor (ReverseOneToOneDescriptor).
|
# by the reverse accessor (ReverseOneToOneDescriptor).
|
||||||
try:
|
try:
|
||||||
rel_obj = getattr(instance, self.cache_name)
|
rel_obj = self.field.get_cached_value(instance)
|
||||||
except AttributeError:
|
except KeyError:
|
||||||
val = self.field.get_local_related_value(instance)
|
val = self.field.get_local_related_value(instance)
|
||||||
if None in val:
|
if None in val:
|
||||||
rel_obj = None
|
rel_obj = None
|
||||||
else:
|
else:
|
||||||
rel_obj = self.get_object(instance)
|
rel_obj = self.get_object(instance)
|
||||||
|
remote_field = self.field.remote_field
|
||||||
# If this is a one-to-one relation, set the reverse accessor
|
# If this is a one-to-one relation, set the reverse accessor
|
||||||
# cache on the related object to the current instance to avoid
|
# cache on the related object to the current instance to avoid
|
||||||
# an extra SQL query if it's accessed later on.
|
# an extra SQL query if it's accessed later on.
|
||||||
if not self.field.remote_field.multiple:
|
if not remote_field.multiple:
|
||||||
setattr(rel_obj, self.field.remote_field.get_cache_name(), instance)
|
remote_field.set_cached_value(rel_obj, instance)
|
||||||
setattr(instance, self.cache_name, 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:
|
||||||
raise self.RelatedObjectDoesNotExist(
|
raise self.RelatedObjectDoesNotExist(
|
||||||
|
@ -208,6 +208,7 @@ class ForwardManyToOneDescriptor:
|
||||||
if not router.allow_relation(value, instance):
|
if not router.allow_relation(value, instance):
|
||||||
raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value)
|
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
|
# 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
|
# out the cache on any old related object. Otherwise, deleting the
|
||||||
# previously-related object will also cause this object to be deleted,
|
# 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
|
# populated the cache, then we don't care - we're only accessing
|
||||||
# the object to invalidate the accessor cache, so there's no
|
# the object to invalidate the accessor cache, so there's no
|
||||||
# need to populate the cache just to expire it again.
|
# 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
|
# 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
|
# cache. This cache also might not exist if the related object
|
||||||
# hasn't been accessed yet.
|
# hasn't been accessed yet.
|
||||||
if related is not None:
|
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:
|
for lh_field, rh_field in self.field.related_fields:
|
||||||
setattr(instance, lh_field.attname, None)
|
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
|
# Set the related instance cache used by __get__ to avoid an SQL query
|
||||||
# when accessing the attribute we just set.
|
# 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
|
# 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
|
# the related object to the current instance to avoid an extra SQL
|
||||||
# query if it's accessed later on.
|
# query if it's accessed later on.
|
||||||
if value is not None and not self.field.remote_field.multiple:
|
if value is not None and not remote_field.multiple:
|
||||||
setattr(value, self.field.remote_field.get_cache_name(), instance)
|
remote_field.set_cached_value(value, instance)
|
||||||
|
|
||||||
|
|
||||||
class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor):
|
class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor):
|
||||||
|
@ -308,8 +309,9 @@ class ReverseOneToOneDescriptor:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, related):
|
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.related = related
|
||||||
self.cache_name = related.get_cache_name()
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def RelatedObjectDoesNotExist(self):
|
def RelatedObjectDoesNotExist(self):
|
||||||
|
@ -322,7 +324,7 @@ class ReverseOneToOneDescriptor:
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_cached(self, instance):
|
def is_cached(self, instance):
|
||||||
return hasattr(instance, self.cache_name)
|
return self.related.is_cached(instance)
|
||||||
|
|
||||||
def get_queryset(self, **hints):
|
def get_queryset(self, **hints):
|
||||||
return self.related.related_model._base_manager.db_manager(hints=hints).all()
|
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,
|
# Since we're going to assign directly in the cache,
|
||||||
# we must manage the reverse relation cache manually.
|
# we must manage the reverse relation cache manually.
|
||||||
rel_obj_cache_name = self.related.field.get_cache_name()
|
|
||||||
for rel_obj in queryset:
|
for rel_obj in queryset:
|
||||||
instance = instances_dict[rel_obj_attr(rel_obj)]
|
instance = instances_dict[rel_obj_attr(rel_obj)]
|
||||||
setattr(rel_obj, rel_obj_cache_name, instance)
|
self.related.field.set_cached_value(rel_obj, instance)
|
||||||
return queryset, rel_obj_attr, instance_attr, True, self.cache_name
|
return queryset, rel_obj_attr, instance_attr, True, self.related.get_cache_name(), False
|
||||||
|
|
||||||
def __get__(self, instance, cls=None):
|
def __get__(self, instance, cls=None):
|
||||||
"""
|
"""
|
||||||
|
@ -364,12 +365,12 @@ class ReverseOneToOneDescriptor:
|
||||||
if instance is None:
|
if instance is None:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# The related instance is loaded from the database and then cached in
|
# The related instance is loaded from the database and then cached
|
||||||
# the attribute defined in self.cache_name. It can also be pre-cached
|
# by the field on the model instance state. It can also be pre-cached
|
||||||
# by the forward accessor (ForwardManyToOneDescriptor).
|
# by the forward accessor (ForwardManyToOneDescriptor).
|
||||||
try:
|
try:
|
||||||
rel_obj = getattr(instance, self.cache_name)
|
rel_obj = self.related.get_cached_value(instance)
|
||||||
except AttributeError:
|
except KeyError:
|
||||||
related_pk = instance.pk
|
related_pk = instance.pk
|
||||||
if related_pk is None:
|
if related_pk is None:
|
||||||
rel_obj = None
|
rel_obj = None
|
||||||
|
@ -383,8 +384,8 @@ class ReverseOneToOneDescriptor:
|
||||||
# Set the forward accessor cache on the related object to
|
# Set the forward accessor cache on the related object to
|
||||||
# the current instance to avoid an extra SQL query if it's
|
# the current instance to avoid an extra SQL query if it's
|
||||||
# accessed later on.
|
# accessed later on.
|
||||||
setattr(rel_obj, self.related.field.get_cache_name(), instance)
|
self.related.field.set_cached_value(rel_obj, instance)
|
||||||
setattr(instance, self.cache_name, rel_obj)
|
self.related.set_cached_value(instance, rel_obj)
|
||||||
|
|
||||||
if rel_obj is None:
|
if rel_obj is None:
|
||||||
raise self.RelatedObjectDoesNotExist(
|
raise self.RelatedObjectDoesNotExist(
|
||||||
|
@ -415,11 +416,17 @@ class ReverseOneToOneDescriptor:
|
||||||
if value is None:
|
if value is None:
|
||||||
# Update the cached related instance (if any) & clear the cache.
|
# Update the cached related instance (if any) & clear the cache.
|
||||||
try:
|
try:
|
||||||
rel_obj = getattr(instance, self.cache_name)
|
# Following the example above, this would be the cached
|
||||||
except AttributeError:
|
# ``restaurant`` instance (if any).
|
||||||
|
rel_obj = self.related.get_cached_value(instance)
|
||||||
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
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)
|
setattr(rel_obj, self.related.field.name, None)
|
||||||
elif not isinstance(value, self.related.related_model):
|
elif not isinstance(value, self.related.related_model):
|
||||||
# An object must be an instance of the related class.
|
# 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
|
# Set the related instance cache used by __get__ to avoid an SQL query
|
||||||
# when accessing the attribute we just set.
|
# 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
|
# 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.
|
# 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:
|
class ReverseManyToOneDescriptor:
|
||||||
|
@ -584,7 +591,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
|
||||||
instance = instances_dict[rel_obj_attr(rel_obj)]
|
instance = instances_dict[rel_obj_attr(rel_obj)]
|
||||||
setattr(rel_obj, self.field.name, instance)
|
setattr(rel_obj, self.field.name, instance)
|
||||||
cache_name = self.field.related_query_name()
|
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):
|
def add(self, *objs, bulk=True):
|
||||||
self._remove_prefetched_objects()
|
self._remove_prefetched_objects()
|
||||||
|
@ -882,6 +889,7 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
|
||||||
),
|
),
|
||||||
False,
|
False,
|
||||||
self.prefetch_cache_name,
|
self.prefetch_cache_name,
|
||||||
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def add(self, *objs):
|
def add(self, *objs):
|
||||||
|
|
|
@ -13,9 +13,10 @@ from django.core import exceptions
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from . import BLANK_CHOICE_DASH
|
from . import BLANK_CHOICE_DASH
|
||||||
|
from .mixins import FieldCacheMixin
|
||||||
|
|
||||||
|
|
||||||
class ForeignObjectRel:
|
class ForeignObjectRel(FieldCacheMixin):
|
||||||
"""
|
"""
|
||||||
Used by ForeignObject to store information about the relation.
|
Used by ForeignObject to store information about the relation.
|
||||||
|
|
||||||
|
@ -162,12 +163,16 @@ class ForeignObjectRel:
|
||||||
return self.related_name
|
return self.related_name
|
||||||
return opts.model_name + ('_set' if self.multiple else '')
|
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):
|
def get_path_info(self):
|
||||||
return self.field.get_reverse_path_info()
|
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):
|
class ManyToOneRel(ForeignObjectRel):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -72,7 +72,7 @@ class ModelIterable(BaseIterable):
|
||||||
if queryset._known_related_objects:
|
if queryset._known_related_objects:
|
||||||
for field, rel_objs in queryset._known_related_objects.items():
|
for field, rel_objs in queryset._known_related_objects.items():
|
||||||
# Avoid overwriting objects loaded e.g. by select_related
|
# Avoid overwriting objects loaded e.g. by select_related
|
||||||
if hasattr(obj, field.get_cache_name()):
|
if field.is_cached(obj):
|
||||||
continue
|
continue
|
||||||
pk = getattr(obj, field.get_attname())
|
pk = getattr(obj, field.get_attname())
|
||||||
try:
|
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 returned instances,
|
||||||
# callable that gets value to be matched for passed in instances,
|
# callable that gets value to be matched for passed in instances,
|
||||||
# boolean that is True for singly related objects,
|
# 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
|
# The 'values to be matched' must be hashable as they will be used
|
||||||
# in a dictionary.
|
# 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)))
|
prefetcher.get_prefetch_queryset(instances, lookup.get_current_queryset(level)))
|
||||||
# We have to handle the possibility that the QuerySet we just got back
|
# 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
|
# 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:
|
if single:
|
||||||
val = vals[0] if vals else None
|
val = vals[0] if vals else None
|
||||||
to_attr = to_attr if as_attr else cache_name
|
if as_attr:
|
||||||
setattr(obj, to_attr, val)
|
# 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:
|
else:
|
||||||
if as_attr:
|
if as_attr:
|
||||||
setattr(obj, to_attr, vals)
|
setattr(obj, to_attr, vals)
|
||||||
|
@ -1653,9 +1664,9 @@ class RelatedPopulator:
|
||||||
# model's fields.
|
# model's fields.
|
||||||
# - related_populators: a list of RelatedPopulator instances if
|
# - related_populators: a list of RelatedPopulator instances if
|
||||||
# select_related() descends to related models from this model.
|
# select_related() descends to related models from this model.
|
||||||
# - cache_name, reverse_cache_name: the names to use for setattr
|
# - field, remote_field: the fields to use for populating the
|
||||||
# when assigning the fetched object to the from_obj. If the
|
# internal fields cache. If remote_field is set then we also
|
||||||
# reverse_cache_name is set, then we also set the reverse link.
|
# set the reverse link.
|
||||||
select_fields = klass_info['select_fields']
|
select_fields = klass_info['select_fields']
|
||||||
from_parent = klass_info['from_parent']
|
from_parent = klass_info['from_parent']
|
||||||
if not from_parent:
|
if not from_parent:
|
||||||
|
@ -1674,16 +1685,16 @@ class RelatedPopulator:
|
||||||
self.model_cls = klass_info['model']
|
self.model_cls = klass_info['model']
|
||||||
self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
|
self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
|
||||||
self.related_populators = get_related_populators(klass_info, select, self.db)
|
self.related_populators = get_related_populators(klass_info, select, self.db)
|
||||||
field = klass_info['field']
|
|
||||||
reverse = klass_info['reverse']
|
reverse = klass_info['reverse']
|
||||||
self.reverse_cache_name = None
|
field = klass_info['field']
|
||||||
|
self.remote_field = None
|
||||||
if reverse:
|
if reverse:
|
||||||
self.cache_name = field.remote_field.get_cache_name()
|
self.field = field.remote_field
|
||||||
self.reverse_cache_name = field.get_cache_name()
|
self.remote_field = field
|
||||||
else:
|
else:
|
||||||
self.cache_name = field.get_cache_name()
|
self.field = field
|
||||||
if field.unique:
|
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):
|
def populate(self, row, from_obj):
|
||||||
if self.reorder_for_init:
|
if self.reorder_for_init:
|
||||||
|
@ -1697,9 +1708,9 @@ class RelatedPopulator:
|
||||||
if obj and self.related_populators:
|
if obj and self.related_populators:
|
||||||
for rel_iter in self.related_populators:
|
for rel_iter in self.related_populators:
|
||||||
rel_iter.populate(row, obj)
|
rel_iter.populate(row, obj)
|
||||||
setattr(from_obj, self.cache_name, obj)
|
self.field.set_cached_value(from_obj, obj)
|
||||||
if obj and self.reverse_cache_name:
|
if obj and self.remote_field:
|
||||||
setattr(obj, self.reverse_cache_name, from_obj)
|
self.remote_field.set_cached_value(obj, from_obj)
|
||||||
|
|
||||||
|
|
||||||
def get_related_populators(klass_info, select, db):
|
def get_related_populators(klass_info, select, db):
|
||||||
|
|
|
@ -10,9 +10,9 @@ class ArticleTranslationDescriptor(ForwardManyToOneDescriptor):
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
raise AttributeError("%s must be accessed via instance" % self.field.name)
|
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:
|
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:
|
class ColConstraint:
|
||||||
|
|
|
@ -461,7 +461,7 @@ class ManyToOneTests(TestCase):
|
||||||
self.assertIs(c.parent, p)
|
self.assertIs(c.parent, p)
|
||||||
|
|
||||||
# But if we kill the cache, we get a new object.
|
# 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)
|
self.assertIsNot(c.parent, p)
|
||||||
|
|
||||||
# Assigning a new object results in that object getting cached immediately.
|
# Assigning a new object results in that object getting cached immediately.
|
||||||
|
|
|
@ -207,7 +207,7 @@ class OneToOneTests(TestCase):
|
||||||
self.assertIs(p.restaurant, r)
|
self.assertIs(p.restaurant, r)
|
||||||
|
|
||||||
# But if we kill the cache, we get a new object
|
# 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)
|
self.assertIsNot(p.restaurant, r)
|
||||||
|
|
||||||
# Reassigning the Restaurant object results in an immediate cache update
|
# Reassigning the Restaurant object results in an immediate cache update
|
||||||
|
|
Loading…
Reference in New Issue