summaryrefslogtreecommitdiff
path: root/django/db/models/fields
diff options
context:
space:
mode:
Diffstat (limited to 'django/db/models/fields')
-rw-r--r--django/db/models/fields/related.py40
-rw-r--r--django/db/models/fields/related_descriptors.py2
-rw-r--r--django/db/models/fields/related_lookups.py10
-rw-r--r--django/db/models/fields/reverse_related.py25
4 files changed, 62 insertions, 15 deletions
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 2e8c87c1b1..8533803ba6 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -447,7 +447,7 @@ class RelatedField(FieldCacheMixin, Field):
When filtering against this relation, return the field on the remote
model against which the filtering should happen.
"""
- target_fields = self.get_path_info()[-1].target_fields
+ target_fields = self.path_infos[-1].target_fields
if len(target_fields) > 1:
raise exceptions.FieldError(
"The relation has multiple target fields, but only single target field was asked for")
@@ -499,6 +499,13 @@ class ForeignObject(RelatedField):
self.to_fields = to_fields
self.swappable = swappable
+ def __copy__(self):
+ obj = super().__copy__()
+ # Remove any cached PathInfo values.
+ obj.__dict__.pop('path_infos', None)
+ obj.__dict__.pop('reverse_path_infos', None)
+ return obj
+
def check(self, **kwargs):
return [
*super().check(**kwargs),
@@ -743,6 +750,10 @@ class ForeignObject(RelatedField):
filtered_relation=filtered_relation,
)]
+ @cached_property
+ def path_infos(self):
+ return self.get_path_info()
+
def get_reverse_path_info(self, filtered_relation=None):
"""Get path from the related model to this field's model."""
opts = self.model._meta
@@ -757,6 +768,10 @@ class ForeignObject(RelatedField):
filtered_relation=filtered_relation,
)]
+ @cached_property
+ def reverse_path_infos(self):
+ return self.get_reverse_path_info()
+
@classmethod
@functools.lru_cache(maxsize=None)
def get_lookups(cls):
@@ -1541,12 +1556,17 @@ class ManyToManyField(RelatedField):
linkfield1 = int_model._meta.get_field(self.m2m_field_name())
linkfield2 = int_model._meta.get_field(self.m2m_reverse_field_name())
if direct:
- join1infos = linkfield1.get_reverse_path_info()
- join2infos = linkfield2.get_path_info(filtered_relation)
+ join1infos = linkfield1.reverse_path_infos
+ if filtered_relation:
+ join2infos = linkfield2.get_path_info(filtered_relation)
+ else:
+ join2infos = linkfield2.path_infos
else:
- join1infos = linkfield2.get_reverse_path_info()
- join2infos = linkfield1.get_path_info(filtered_relation)
-
+ join1infos = linkfield2.reverse_path_infos
+ if filtered_relation:
+ join2infos = linkfield1.get_path_info(filtered_relation)
+ else:
+ join2infos = linkfield1.path_infos
# Get join infos between the last model of join 1 and the first model
# of join 2. Assume the only reason these may differ is due to model
# inheritance.
@@ -1564,9 +1584,17 @@ class ManyToManyField(RelatedField):
def get_path_info(self, filtered_relation=None):
return self._get_path_info(direct=True, filtered_relation=filtered_relation)
+ @cached_property
+ def path_infos(self):
+ return self.get_path_info()
+
def get_reverse_path_info(self, filtered_relation=None):
return self._get_path_info(direct=False, filtered_relation=filtered_relation)
+ @cached_property
+ def reverse_path_infos(self):
+ return self.get_reverse_path_info()
+
def _get_m2m_db_table(self, opts):
"""
Function that can be curried to provide the m2m table name for this
diff --git a/django/db/models/fields/related_descriptors.py b/django/db/models/fields/related_descriptors.py
index cb77a0c476..ef546e844a 100644
--- a/django/db/models/fields/related_descriptors.py
+++ b/django/db/models/fields/related_descriptors.py
@@ -599,7 +599,7 @@ def create_reverse_many_to_one_manager(superclass, rel):
# for related object id.
rel_obj_id = tuple([
getattr(self.instance, target_field.attname)
- for target_field in self.field.get_path_info()[-1].target_fields
+ for target_field in self.field.path_infos[-1].target_fields
])
else:
rel_obj_id = getattr(self.instance, target_field.attname)
diff --git a/django/db/models/fields/related_lookups.py b/django/db/models/fields/related_lookups.py
index 34cca8ba5e..50f8b44158 100644
--- a/django/db/models/fields/related_lookups.py
+++ b/django/db/models/fields/related_lookups.py
@@ -30,7 +30,7 @@ def get_normalized_value(value, lhs):
from django.db.models import Model
if isinstance(value, Model):
value_list = []
- sources = lhs.output_field.get_path_info()[-1].target_fields
+ sources = lhs.output_field.path_infos[-1].target_fields
for source in sources:
while not isinstance(value, source.model) and source.remote_field:
source = source.remote_field.model._meta.get_field(source.remote_field.field_name)
@@ -55,10 +55,10 @@ class RelatedIn(In):
# ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
# doesn't have validation for non-integers, so we must run validation
# using the target field.
- if hasattr(self.lhs.output_field, 'get_path_info'):
+ if hasattr(self.lhs.output_field, 'path_infos'):
# Run the target field's get_prep_value. We can safely assume there is
# only one as we don't get to the direct value branch otherwise.
- target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
+ target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
self.rhs = [target_field.get_prep_value(v) for v in self.rhs]
return super().get_prep_lookup()
@@ -113,10 +113,10 @@ class RelatedLookupMixin:
# ForeignKey to IntegerField given value 'abc'. The ForeignKey itself
# doesn't have validation for non-integers, so we must run validation
# using the target field.
- if self.prepare_rhs and hasattr(self.lhs.output_field, 'get_path_info'):
+ if self.prepare_rhs and hasattr(self.lhs.output_field, 'path_infos'):
# Get the target field. We can safely assume there is only one
# as we don't get to the direct value branch otherwise.
- target_field = self.lhs.output_field.get_path_info()[-1].target_fields[-1]
+ target_field = self.lhs.output_field.path_infos[-1].target_fields[-1]
self.rhs = target_field.get_prep_value(self.rhs)
return super().get_prep_lookup()
diff --git a/django/db/models/fields/reverse_related.py b/django/db/models/fields/reverse_related.py
index 65950590e2..6f0c788bbd 100644
--- a/django/db/models/fields/reverse_related.py
+++ b/django/db/models/fields/reverse_related.py
@@ -71,7 +71,7 @@ class ForeignObjectRel(FieldCacheMixin):
When filtering against this relation, return the field on the remote
model against which the filtering should happen.
"""
- target_fields = self.get_path_info()[-1].target_fields
+ target_fields = self.path_infos[-1].target_fields
if len(target_fields) > 1:
raise exceptions.FieldError("Can't use target_field for multicolumn relations.")
return target_fields[0]
@@ -138,6 +138,18 @@ class ForeignObjectRel(FieldCacheMixin):
def __hash__(self):
return hash(self.identity)
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ # Delete the path_infos cached property because it can be recalculated
+ # at first invocation after deserialization. The attribute must be
+ # removed because subclasses like ManyToOneRel may have a PathInfo
+ # which contains an intermediate M2M table that's been dynamically
+ # created and doesn't exist in the .models module.
+ # This is a reverse relation, so there is no reverse_path_infos to
+ # delete.
+ state.pop('path_infos', None)
+ return state
+
def get_choices(
self, include_blank=True, blank_choice=BLANK_CHOICE_DASH,
limit_choices_to=None, ordering=(),
@@ -195,7 +207,14 @@ class ForeignObjectRel(FieldCacheMixin):
return opts.model_name + ('_set' if self.multiple else '')
def get_path_info(self, filtered_relation=None):
- return self.field.get_reverse_path_info(filtered_relation)
+ if filtered_relation:
+ return self.field.get_reverse_path_info(filtered_relation)
+ else:
+ return self.field.reverse_path_infos
+
+ @cached_property
+ def path_infos(self):
+ return self.get_path_info()
def get_cache_name(self):
"""
@@ -234,7 +253,7 @@ class ManyToOneRel(ForeignObjectRel):
self.field_name = field_name
def __getstate__(self):
- state = self.__dict__.copy()
+ state = super().__getstate__()
state.pop('related_model', None)
return state