Fixed #29725 -- Removed unnecessary join in QuerySet.count() and exists() on a many-to-many relation.

This commit is contained in:
oliver 2018-10-16 00:01:57 +09:00 committed by Tim Graham
parent f2e2a1bd4b
commit 1299421cad
3 changed files with 80 additions and 3 deletions

View File

@ -912,6 +912,33 @@ def create_forward_many_to_many_manager(superclass, rel, reverse):
False,
)
@property
def constrained_target(self):
# If the through relation's target field's foreign integrity is
# enforced, the query can be performed solely against the through
# table as the INNER JOIN'ing against target table is unnecessary.
if not self.target_field.db_constraint:
return None
db = router.db_for_read(self.through, instance=self.instance)
if not connections[db].features.supports_foreign_keys:
return None
hints = {'instance': self.instance}
manager = self.through._base_manager.db_manager(db, hints=hints)
filters = {self.source_field_name: self.instance.pk}
# Nullable target rows must be excluded as well as they would have
# been filtered out from an INNER JOIN.
if self.target_field.null:
filters['%s__isnull' % self.target_field_name] = False
return manager.filter(**filters)
def exists(self):
constrained_target = self.constrained_target
return constrained_target.exists() if constrained_target else super().exists()
def count(self):
constrained_target = self.constrained_target
return constrained_target.count() if constrained_target else super().count()
def add(self, *objs):
if not rel.through._meta.auto_created:
opts = self.through._meta

View File

@ -55,3 +55,13 @@ class InheritedArticleA(AbstractArticle):
class InheritedArticleB(AbstractArticle):
pass
class NullableTargetArticle(models.Model):
headline = models.CharField(max_length=100)
publications = models.ManyToManyField(Publication, through='NullablePublicationThrough')
class NullablePublicationThrough(models.Model):
article = models.ForeignKey(NullableTargetArticle, models.CASCADE)
publication = models.ForeignKey(Publication, models.CASCADE, null=True)

View File

@ -1,7 +1,13 @@
from django.db import transaction
from django.test import TestCase
from unittest import mock
from .models import Article, InheritedArticleA, InheritedArticleB, Publication
from django.db import connection, transaction
from django.test import TestCase, skipUnlessDBFeature
from django.test.utils import CaptureQueriesContext
from .models import (
Article, InheritedArticleA, InheritedArticleB, NullablePublicationThrough,
NullableTargetArticle, Publication,
)
class ManyToManyTests(TestCase):
@ -554,3 +560,37 @@ class ManyToManyTests(TestCase):
]
)
self.assertQuerysetEqual(b.publications.all(), ['<Publication: Science Weekly>'])
class ManyToManyQueryTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.article = Article.objects.create(headline='Django lets you build Web apps easily')
cls.nullable_target_article = NullableTargetArticle.objects.create(headline='The python is good')
NullablePublicationThrough.objects.create(article=cls.nullable_target_article, publication=None)
@skipUnlessDBFeature('supports_foreign_keys')
def test_count_join_optimization(self):
with CaptureQueriesContext(connection) as query:
self.article.publications.count()
self.assertNotIn('JOIN', query[0]['sql'])
self.assertEqual(self.nullable_target_article.publications.count(), 0)
def test_count_join_optimization_disabled(self):
with mock.patch.object(connection.features, 'supports_foreign_keys', False), \
CaptureQueriesContext(connection) as query:
self.article.publications.count()
self.assertIn('JOIN', query[0]['sql'])
@skipUnlessDBFeature('supports_foreign_keys')
def test_exists_join_optimization(self):
with CaptureQueriesContext(connection) as query:
self.article.publications.exists()
self.assertNotIn('JOIN', query[0]['sql'])
self.assertIs(self.nullable_target_article.publications.exists(), False)
def test_exists_join_optimization_disabled(self):
with mock.patch.object(connection.features, 'supports_foreign_keys', False), \
CaptureQueriesContext(connection) as query:
self.article.publications.exists()
self.assertIn('JOIN', query[0]['sql'])