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
Другие вопросы по тегам