Django: Paginator + необработанный SQL-запрос
Я использую Django Paginator повсюду на своем сайте и даже написал специальный тег шаблона, чтобы сделать его более удобным. Но теперь я попал в состояние, когда мне нужно сделать сложный пользовательский необработанный запрос SQL, что без LIMIT
вернет около 100К записей.
Как я могу использовать Django Pagintor с пользовательским запросом?
Упрощенный пример моей проблемы:
Моя модель:
class PersonManager(models.Manager):
def complicated_list(self):
from django.db import connection
#Real query is much more complex
cursor.execute("""SELECT * FROM `myapp_person`""");
result_list = []
for row in cursor.fetchall():
result_list.append(row[0]);
return result_list
class Person(models.Model):
name = models.CharField(max_length=255);
surname = models.CharField(max_length=255);
age = models.IntegerField();
objects = PersonManager();
Как я использую пагинацию с Django ORM:
all_objects = Person.objects.all();
paginator = Paginator(all_objects, 10);
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
try:
persons = paginator.page(page)
except (EmptyPage, InvalidPage):
persons = paginator.page(paginator.num_pages)
Таким образом, Джанго становится очень умным и добавляет LIMIT
на запрос при его выполнении. Но когда я использую собственный менеджер:
all_objects = Person.objects.complicated_list();
все данные выбраны, и только тогда список питонов нарезается, что ОЧЕНЬ медленно. Как сделать так, чтобы мой пользовательский менеджер вел себя так же, как встроенный?
4 ответа
Рассматривая исходный код Paginator, в частности, функцию page(), я думаю, что вопрос только в том, чтобы реализовать нарезку на вашей стороне и преобразовать ее в соответствующее предложение LIMIT в SQL-запросе. Вам также может понадобиться добавить кеширование, но это будет выглядеть как QuerySet, так что, возможно, вы можете сделать что-то еще:
- Вы можете создать базу данных VIEW, используя CREATE VIEW myview AS [ваш запрос];
- добавить модель Django для этого ПРОСМОТРА с помощью Meta: managed=False
- использовать эту модель, как и любую другую модель, включая нарезку ее наборов запросов - это означает, что она идеально подходит для использования с Paginator
(К вашему сведению - я уже давно использую этот подход, даже со сложными отношениями "многие ко многим" с VIEW, подделывающими промежуточные таблицы m2m.)
Вот RawPaginator
класс я сделал, что переопределяет Paginator
работать с необработанными запросами. Требуется еще один аргумент, count
, который является общим количеством вашего запроса. Это не разрезает object_list
потому что вы должны разбивать на страницы в вашем необработанном запросе через OFFSET
а также LIMIT
,
from django.core.paginator import Paginator
class RawPaginator(Paginator):
def __init__(self, object_list, per_page, count, **kwargs):
super().__init__(object_list, per_page, **kwargs)
self.raw_count = count
def _get_count(self):
return self.raw_count
count = property(_get_count)
def page(self, number):
number = self.validate_number(number)
return self._get_page(self.object_list, number, self)
Я не знаю о Django 1.1, но если вы можете подождать 1.2 (что уже не должно быть так долго), вы можете использовать objects.raw()
как описано в этой статье и в документации по разработке.
В противном случае, если ваш запрос не слишком сложен, возможно, используя extra
пункт достаточно.
Я также хотел подключить PaginatedRawQuerySet
который я написал (пожалуйста, рассмотрите это как альфа-версию). Это добавляет способность нарезки к необработанному набору запросов. Пожалуйста, обратитесь к этому ответу - который я написал для другого вопроса с аналогичным требованием - чтобы понять, как он работает (особенно раздел "Слово предостережения" в конце).
from django.db import models
from django.db.models import sql
from django.db.models.query import RawQuerySet
class PaginatedRawQuerySet(RawQuerySet):
def __init__(self, raw_query, **kwargs):
super(PaginatedRawQuerySet, self).__init__(raw_query, **kwargs)
self.original_raw_query = raw_query
self._result_cache = None
def __getitem__(self, k):
"""
Retrieves an item or slice from the set of results.
"""
if not isinstance(k, (slice, int,)):
raise TypeError
assert ((not isinstance(k, slice) and (k >= 0)) or
(isinstance(k, slice) and (k.start is None or k.start >= 0) and
(k.stop is None or k.stop >= 0))), \
"Negative indexing is not supported."
if self._result_cache is not None:
return self._result_cache[k]
if isinstance(k, slice):
qs = self._clone()
if k.start is not None:
start = int(k.start)
else:
start = None
if k.stop is not None:
stop = int(k.stop)
else:
stop = None
qs.set_limits(start, stop)
return qs
qs = self._clone()
qs.set_limits(k, k + 1)
return list(qs)[0]
def __iter__(self):
self._fetch_all()
return iter(self._result_cache)
def count(self):
if self._result_cache is not None:
return len(self._result_cache)
return self.model.objects.count()
def set_limits(self, start, stop):
limit_offset = ''
new_params = tuple()
if start is None:
start = 0
elif start > 0:
new_params += (start,)
limit_offset = ' OFFSET %s'
if stop is not None:
new_params = (stop - start,) + new_params
limit_offset = 'LIMIT %s' + limit_offset
self.params = self.params + new_params
self.raw_query = self.original_raw_query + limit_offset
self.query = sql.RawQuery(sql=self.raw_query, using=self.db, params=self.params)
def _fetch_all(self):
if self._result_cache is None:
self._result_cache = list(super().__iter__())
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.model.__name__)
def __len__(self):
self._fetch_all()
return len(self._result_cache)
def _clone(self):
clone = self.__class__(raw_query=self.raw_query, model=self.model, using=self._db, hints=self._hints,
query=self.query, params=self.params, translations=self.translations)
return clone