Django:Paginator +原始SQL查询

时间:2010-03-28 09:54:59

标签: python sql django pagination

我在我的网站上到处都使用Django Paginator,甚至还写了一个特殊的模板标签,以使其更方便。但现在我进入了一个状态,我需要创建一个复杂的自定义原始SQL查询,没有LIMIT将返回大约100K记录。

如何在自定义查询中使用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中使用pagintation的方式:

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 个答案:

答案 0 :(得分:9)

查看Paginator的源代码,特别是page() function,我认为只需要实现slicing,并将其转换为SQL查询中的相关LIMIT子句。您可能还需要添加一些缓存,但这看起来像QuerySet,所以也许您可以做其他事情:

  • 您可以使用CREATE VIEW myview AS [您的查询];
  • 创建数据库视图
  • 使用Meta: managed=False
  • 为该视图添加Django模型
  • 像任何其他模型一样使用该模型,包括切割其查询集 - 这意味着它非常适合与Paginator一起使用

(为了您的信息 - 我已经使用这种方法很长一段时间了,即使是与VIEW伪造m2m中间表的复杂的多对多关系。)

答案 1 :(得分:2)

我不知道Django 1.1但是如果你可以等到1.2(不应该那么长)你可以按照this articledevelopment documentation中的描述使用objects.raw() {3}}

否则,如果查询不是太复杂,可能使用extra clause就足够了。

答案 2 :(得分:2)

这是我创建的RawPaginator类,它会覆盖Paginator以处理原始查询。它需要一个额外的参数count,这是您的查询的总计数。它不会对object_list进行分片,因为您必须通过OFFSETLIMIT在原始查询中进行分页。

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)

答案 3 :(得分:1)

我还想插一个我写的PaginatedRawQuerySet(请将其视为alpha版本)。这会将切片功能添加到原始查询集。请参考to this answer - 我为另一个有类似要求的问题写的 - 以了解它是如何工作的(特别是"谨慎和#34;最后一节)。

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