From 330638b89f14e1fb06e9d313ccc9768ae167c53f Mon Sep 17 00:00:00 2001 From: Amir Hadi Date: Sat, 13 Apr 2019 14:33:05 +0200 Subject: [PATCH] Fixed #6785 -- Made QuerySet.get() fetch a limited number of rows. Co-authored-by: Tim Graham Co-authored-by: Patryk Zawadzki Co-Authored-By: Mariusz Felisiak --- django/db/models/query.py | 13 +++++++++++-- tests/basic/tests.py | 22 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 84236107f3..a21a091af0 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -28,6 +28,9 @@ from django.utils import timezone from django.utils.functional import cached_property, partition from django.utils.version import get_version +# The maximum number of results to fetch in a get() query. +MAX_GET_RESULTS = 21 + # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 @@ -398,6 +401,10 @@ class QuerySet: clone = self.filter(*args, **kwargs) if self.query.can_filter() and not self.query.distinct_fields: clone = clone.order_by() + limit = None + if not clone.query.select_for_update or connections[clone.db].features.supports_select_for_update_with_limit: + limit = MAX_GET_RESULTS + clone.query.set_limits(high=limit) num = len(clone) if num == 1: return clone._result_cache[0] @@ -407,8 +414,10 @@ class QuerySet: self.model._meta.object_name ) raise self.model.MultipleObjectsReturned( - "get() returned more than one %s -- it returned %s!" % - (self.model._meta.object_name, num) + 'get() returned more than one %s -- it returned %s!' % ( + self.model._meta.object_name, + num if not limit or num < limit else 'more than %s' % (limit - 1), + ) ) def create(self, **kwargs): diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 328ddc31f1..b29dda64f7 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections, models from django.db.models.manager import BaseManager -from django.db.models.query import EmptyQuerySet, QuerySet +from django.db.models.query import MAX_GET_RESULTS, EmptyQuerySet, QuerySet from django.test import ( SimpleTestCase, TestCase, TransactionTestCase, skipUnlessDBFeature, ) @@ -387,6 +387,26 @@ class ModelTest(TestCase): # Fields that weren't deleted aren't reloaded. self.assertEqual(article.pub_date, new_pub_date) + def test_multiple_objects_max_num_fetched(self): + max_results = MAX_GET_RESULTS - 1 + Article.objects.bulk_create( + Article(headline='Area %s' % i, pub_date=datetime(2005, 7, 28)) + for i in range(max_results) + ) + self.assertRaisesMessage( + MultipleObjectsReturned, + 'get() returned more than one Article -- it returned %d!' % max_results, + Article.objects.get, + headline__startswith='Area', + ) + Article.objects.create(headline='Area %s' % max_results, pub_date=datetime(2005, 7, 28)) + self.assertRaisesMessage( + MultipleObjectsReturned, + 'get() returned more than one Article -- it returned more than %d!' % max_results, + Article.objects.get, + headline__startswith='Area', + ) + class ModelLookupTest(TestCase): @classmethod