Django: Paginator + необработанный SQL-запрос - PullRequest
11 голосов
/ 28 марта 2010

Я использую 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)

Таким образом, Django становится очень умным и добавляет LIMIT к запросу при его выполнении. Но когда я использую собственный менеджер:

all_objects = Person.objects.complicated_list();

все данные выбраны, и только после этого список Python разрезается, что ОЧЕНЬ медленно. Как сделать так, чтобы мой пользовательский менеджер вел себя так же, как встроенный?

Ответы [ 4 ]

9 голосов
/ 28 марта 2010

Рассматривая исходный код Paginator, в частности функцию page () , я думаю, что вопрос только в реализации нарезки на вашей стороне и преобразовании этого в соответствующее предложение LIMIT в SQL запрос. Вам также может понадобиться добавить кеширование, но это будет выглядеть как QuerySet, так что, возможно, вы можете сделать что-то еще:

  • вы можете создать базу данных VIEW, используя CREATE VIEW myview AS [ваш запрос];
  • добавить модель Django для этого ПРОСМОТРА, с Meta: managed = False
  • использовать эту модель, как и любую другую модель, включая нарезку ее наборов запросов - это означает, что она идеально подходит для использования с Paginator

(К вашему сведению - я уже давно использую этот подход, даже со сложными отношениями «многие ко многим» с промежуточными таблицами подделок m2m VIEW.)

2 голосов
/ 16 марта 2016

Вот класс 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)
2 голосов
/ 28 марта 2010

Я не знаю о Django 1.1, но если вы можете подождать 1.2 (что уже не должно быть так долго), вы можете использовать objects.raw(), как описано в этой статье и в документация на разработку .

В противном случае, если ваш запрос не слишком сложен, возможно, достаточно использовать выражение extra .

1 голос
/ 12 мая 2017

Я также хотел подключить 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...