Loading django/db/models/fields/related.py +6 −3 Original line number Diff line number Diff line Loading @@ -21,8 +21,9 @@ from django.utils.version import get_docs_version from . import Field from .related_descriptors import ( ForwardManyToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ForwardManyToOneDescriptor, ForwardOneToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ) from .related_lookups import ( RelatedExact, RelatedGreaterThan, RelatedGreaterThanOrEqual, RelatedIn, Loading Loading @@ -437,6 +438,7 @@ class ForeignObject(RelatedField): requires_unique_target = True related_accessor_class = ReverseManyToOneDescriptor forward_related_accessor_class = ForwardManyToOneDescriptor rel_class = ForeignObjectRel def __init__(self, to, on_delete, from_fields, to_fields, rel=None, related_name=None, Loading Loading @@ -698,7 +700,7 @@ class ForeignObject(RelatedField): def contribute_to_class(self, cls, name, private_only=False, **kwargs): super(ForeignObject, self).contribute_to_class(cls, name, private_only=private_only, **kwargs) setattr(cls, self.name, ForwardManyToOneDescriptor(self)) setattr(cls, self.name, self.forward_related_accessor_class(self)) def contribute_to_related_class(self, cls, related): # Internal FK's - i.e., those with a related name ending with '+' - Loading Loading @@ -969,6 +971,7 @@ class OneToOneField(ForeignKey): one_to_one = True related_accessor_class = ReverseOneToOneDescriptor forward_related_accessor_class = ForwardOneToOneDescriptor rel_class = OneToOneRel description = _("One-to-one relationship") Loading django/db/models/fields/related_descriptors.py +35 −11 Original line number Diff line number Diff line Loading @@ -23,18 +23,21 @@ reverse many-to-one relation. There are three types of relations (many-to-one, one-to-one, and many-to-many) and two directions (forward and reverse) for a total of six combinations. 1. Related instance on the forward side of a many-to-one or one-to-one relation: ``ForwardManyToOneDescriptor``. 1. Related instance on the forward side of a many-to-one relation: ``ForwardManyToOneDescriptor``. Uniqueness of foreign key values is irrelevant to accessing the related instance, making the many-to-one and one-to-one cases identical as far as the descriptor is concerned. The constraint is checked upstream (unicity validation in forms) or downstream (unique indexes in the database). If you're looking for ``ForwardOneToOneDescriptor``, use ``ForwardManyToOneDescriptor`` instead. 2. Related instance on the forward side of a one-to-one relation: ``ForwardOneToOneDescriptor``. 2. Related instance on the reverse side of a one-to-one relation: It avoids querying the database when accessing the parent link field in a multi-table inheritance scenario. 3. Related instance on the reverse side of a one-to-one relation: ``ReverseOneToOneDescriptor``. One-to-one relations are asymmetrical, despite the apparent symmetry of the Loading @@ -42,13 +45,13 @@ and two directions (forward and reverse) for a total of six combinations. one table to another. As a consequence ``ReverseOneToOneDescriptor`` is slightly different from ``ForwardManyToOneDescriptor``. 3. Related objects manager for related instances on the reverse side of a 4. Related objects manager for related instances on the reverse side of a many-to-one relation: ``ReverseManyToOneDescriptor``. Unlike the previous two classes, this one provides access to a collection of objects. It returns a manager rather than an instance. 4. Related objects manager for related instances on the forward or reverse 5. Related objects manager for related instances on the forward or reverse sides of a many-to-many relation: ``ManyToManyDescriptor``. Many-to-many relations are symmetrical. The syntax of Django models Loading Loading @@ -151,6 +154,11 @@ class ForwardManyToOneDescriptor(object): setattr(rel_obj, rel_obj_cache_name, instance) return queryset, rel_obj_attr, instance_attr, True, self.cache_name def get_object(self, instance): qs = self.get_queryset(instance=instance) # Assuming the database enforces foreign keys, this won't fail. return qs.get(self.field.get_reverse_related_filter(instance)) def __get__(self, instance, cls=None): """ Get the related instance through the forward relation. Loading @@ -174,10 +182,7 @@ class ForwardManyToOneDescriptor(object): if None in val: rel_obj = None else: qs = self.get_queryset(instance=instance) qs = qs.filter(self.field.get_reverse_related_filter(instance)) # Assuming the database enforces foreign keys, this won't fail. rel_obj = qs.get() rel_obj = self.get_object(instance) # If this is a one-to-one relation, set the reverse accessor # cache on the related object to the current instance to avoid # an extra SQL query if it's accessed later on. Loading Loading @@ -259,6 +264,25 @@ class ForwardManyToOneDescriptor(object): setattr(value, self.field.remote_field.get_cache_name(), instance) class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor): def get_object(self, instance): if self.field.remote_field.parent_link: deferred = instance.get_deferred_fields() # Because it's a parent link, all the data is available in the # instance, so populate the parent model with this data. rel_model = self.field.remote_field.model fields = [field.attname for field in rel_model._meta.concrete_fields] # If any of the related model's fields are deferred, fallback to # fetching all fields from the related model. This avoids a query # on the related model for every deferred field. if not any(field in fields for field in deferred): kwargs = {field: getattr(instance, field) for field in fields} return rel_model(**kwargs) return super(ForwardOneToOneDescriptor, self).get_object(instance) class ReverseOneToOneDescriptor(object): """ Accessor to the related object on the reverse side of a one-to-one Loading tests/model_inheritance_regress/tests.py +32 −0 Original line number Diff line number Diff line Loading @@ -496,3 +496,35 @@ class ModelInheritanceTest(TestCase): r.supplier_set.all(), [s], lambda x: x, ) def test_queries_on_parent_access(self): italian_restaurant = ItalianRestaurant.objects.create( name="Guido's House of Pasta", address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False, serves_gnocchi=True, ) # No queries are made when accessing the parent objects. italian_restaurant = ItalianRestaurant.objects.get(pk=italian_restaurant.pk) with self.assertNumQueries(0): restaurant = italian_restaurant.restaurant_ptr self.assertEqual(restaurant.place_ptr.restaurant, restaurant) self.assertEqual(restaurant.italianrestaurant, italian_restaurant) # One query is made when accessing the parent objects when the instance # is deferred. italian_restaurant = ItalianRestaurant.objects.only('serves_gnocchi').get(pk=italian_restaurant.pk) with self.assertNumQueries(1): restaurant = italian_restaurant.restaurant_ptr self.assertEqual(restaurant.place_ptr.restaurant, restaurant) self.assertEqual(restaurant.italianrestaurant, italian_restaurant) # No queries are made when accessing the parent objects when the # instance has deferred a field not present in the parent table. italian_restaurant = ItalianRestaurant.objects.defer('serves_gnocchi').get(pk=italian_restaurant.pk) with self.assertNumQueries(0): restaurant = italian_restaurant.restaurant_ptr self.assertEqual(restaurant.place_ptr.restaurant, restaurant) self.assertEqual(restaurant.italianrestaurant, italian_restaurant) tests/proxy_models/tests.py +1 −1 Original line number Diff line number Diff line Loading @@ -384,7 +384,7 @@ class ProxyModelAdminTests(TestCase): tracker_user = TrackerUser.objects.all()[0] base_user = BaseUser.objects.all()[0] issue = Issue.objects.all()[0] with self.assertNumQueries(7): with self.assertNumQueries(6): collector = admin.utils.NestedObjects('default') collector.collect(ProxyTrackerUser.objects.all()) self.assertIn(tracker_user, collector.edges.get(None, ())) Loading Loading
django/db/models/fields/related.py +6 −3 Original line number Diff line number Diff line Loading @@ -21,8 +21,9 @@ from django.utils.version import get_docs_version from . import Field from .related_descriptors import ( ForwardManyToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ForwardManyToOneDescriptor, ForwardOneToOneDescriptor, ManyToManyDescriptor, ReverseManyToOneDescriptor, ReverseOneToOneDescriptor, ) from .related_lookups import ( RelatedExact, RelatedGreaterThan, RelatedGreaterThanOrEqual, RelatedIn, Loading Loading @@ -437,6 +438,7 @@ class ForeignObject(RelatedField): requires_unique_target = True related_accessor_class = ReverseManyToOneDescriptor forward_related_accessor_class = ForwardManyToOneDescriptor rel_class = ForeignObjectRel def __init__(self, to, on_delete, from_fields, to_fields, rel=None, related_name=None, Loading Loading @@ -698,7 +700,7 @@ class ForeignObject(RelatedField): def contribute_to_class(self, cls, name, private_only=False, **kwargs): super(ForeignObject, self).contribute_to_class(cls, name, private_only=private_only, **kwargs) setattr(cls, self.name, ForwardManyToOneDescriptor(self)) setattr(cls, self.name, self.forward_related_accessor_class(self)) def contribute_to_related_class(self, cls, related): # Internal FK's - i.e., those with a related name ending with '+' - Loading Loading @@ -969,6 +971,7 @@ class OneToOneField(ForeignKey): one_to_one = True related_accessor_class = ReverseOneToOneDescriptor forward_related_accessor_class = ForwardOneToOneDescriptor rel_class = OneToOneRel description = _("One-to-one relationship") Loading
django/db/models/fields/related_descriptors.py +35 −11 Original line number Diff line number Diff line Loading @@ -23,18 +23,21 @@ reverse many-to-one relation. There are three types of relations (many-to-one, one-to-one, and many-to-many) and two directions (forward and reverse) for a total of six combinations. 1. Related instance on the forward side of a many-to-one or one-to-one relation: ``ForwardManyToOneDescriptor``. 1. Related instance on the forward side of a many-to-one relation: ``ForwardManyToOneDescriptor``. Uniqueness of foreign key values is irrelevant to accessing the related instance, making the many-to-one and one-to-one cases identical as far as the descriptor is concerned. The constraint is checked upstream (unicity validation in forms) or downstream (unique indexes in the database). If you're looking for ``ForwardOneToOneDescriptor``, use ``ForwardManyToOneDescriptor`` instead. 2. Related instance on the forward side of a one-to-one relation: ``ForwardOneToOneDescriptor``. 2. Related instance on the reverse side of a one-to-one relation: It avoids querying the database when accessing the parent link field in a multi-table inheritance scenario. 3. Related instance on the reverse side of a one-to-one relation: ``ReverseOneToOneDescriptor``. One-to-one relations are asymmetrical, despite the apparent symmetry of the Loading @@ -42,13 +45,13 @@ and two directions (forward and reverse) for a total of six combinations. one table to another. As a consequence ``ReverseOneToOneDescriptor`` is slightly different from ``ForwardManyToOneDescriptor``. 3. Related objects manager for related instances on the reverse side of a 4. Related objects manager for related instances on the reverse side of a many-to-one relation: ``ReverseManyToOneDescriptor``. Unlike the previous two classes, this one provides access to a collection of objects. It returns a manager rather than an instance. 4. Related objects manager for related instances on the forward or reverse 5. Related objects manager for related instances on the forward or reverse sides of a many-to-many relation: ``ManyToManyDescriptor``. Many-to-many relations are symmetrical. The syntax of Django models Loading Loading @@ -151,6 +154,11 @@ class ForwardManyToOneDescriptor(object): setattr(rel_obj, rel_obj_cache_name, instance) return queryset, rel_obj_attr, instance_attr, True, self.cache_name def get_object(self, instance): qs = self.get_queryset(instance=instance) # Assuming the database enforces foreign keys, this won't fail. return qs.get(self.field.get_reverse_related_filter(instance)) def __get__(self, instance, cls=None): """ Get the related instance through the forward relation. Loading @@ -174,10 +182,7 @@ class ForwardManyToOneDescriptor(object): if None in val: rel_obj = None else: qs = self.get_queryset(instance=instance) qs = qs.filter(self.field.get_reverse_related_filter(instance)) # Assuming the database enforces foreign keys, this won't fail. rel_obj = qs.get() rel_obj = self.get_object(instance) # If this is a one-to-one relation, set the reverse accessor # cache on the related object to the current instance to avoid # an extra SQL query if it's accessed later on. Loading Loading @@ -259,6 +264,25 @@ class ForwardManyToOneDescriptor(object): setattr(value, self.field.remote_field.get_cache_name(), instance) class ForwardOneToOneDescriptor(ForwardManyToOneDescriptor): def get_object(self, instance): if self.field.remote_field.parent_link: deferred = instance.get_deferred_fields() # Because it's a parent link, all the data is available in the # instance, so populate the parent model with this data. rel_model = self.field.remote_field.model fields = [field.attname for field in rel_model._meta.concrete_fields] # If any of the related model's fields are deferred, fallback to # fetching all fields from the related model. This avoids a query # on the related model for every deferred field. if not any(field in fields for field in deferred): kwargs = {field: getattr(instance, field) for field in fields} return rel_model(**kwargs) return super(ForwardOneToOneDescriptor, self).get_object(instance) class ReverseOneToOneDescriptor(object): """ Accessor to the related object on the reverse side of a one-to-one Loading
tests/model_inheritance_regress/tests.py +32 −0 Original line number Diff line number Diff line Loading @@ -496,3 +496,35 @@ class ModelInheritanceTest(TestCase): r.supplier_set.all(), [s], lambda x: x, ) def test_queries_on_parent_access(self): italian_restaurant = ItalianRestaurant.objects.create( name="Guido's House of Pasta", address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False, serves_gnocchi=True, ) # No queries are made when accessing the parent objects. italian_restaurant = ItalianRestaurant.objects.get(pk=italian_restaurant.pk) with self.assertNumQueries(0): restaurant = italian_restaurant.restaurant_ptr self.assertEqual(restaurant.place_ptr.restaurant, restaurant) self.assertEqual(restaurant.italianrestaurant, italian_restaurant) # One query is made when accessing the parent objects when the instance # is deferred. italian_restaurant = ItalianRestaurant.objects.only('serves_gnocchi').get(pk=italian_restaurant.pk) with self.assertNumQueries(1): restaurant = italian_restaurant.restaurant_ptr self.assertEqual(restaurant.place_ptr.restaurant, restaurant) self.assertEqual(restaurant.italianrestaurant, italian_restaurant) # No queries are made when accessing the parent objects when the # instance has deferred a field not present in the parent table. italian_restaurant = ItalianRestaurant.objects.defer('serves_gnocchi').get(pk=italian_restaurant.pk) with self.assertNumQueries(0): restaurant = italian_restaurant.restaurant_ptr self.assertEqual(restaurant.place_ptr.restaurant, restaurant) self.assertEqual(restaurant.italianrestaurant, italian_restaurant)
tests/proxy_models/tests.py +1 −1 Original line number Diff line number Diff line Loading @@ -384,7 +384,7 @@ class ProxyModelAdminTests(TestCase): tracker_user = TrackerUser.objects.all()[0] base_user = BaseUser.objects.all()[0] issue = Issue.objects.all()[0] with self.assertNumQueries(7): with self.assertNumQueries(6): collector = admin.utils.NestedObjects('default') collector.collect(ProxyTrackerUser.objects.all()) self.assertIn(tracker_user, collector.edges.get(None, ())) Loading