Лучший способ разбить необработанный SQL-запрос в представлении Django REST ListAPI?
У меня есть сырой SQL-запрос, который я использую для создания набора запросов для представления REST ListAPI Django. Это соответствует следующему (прошу прощения за бессмысленные имена):
class MyView(ListAPIView):
serializer_class = MySerializer
paginate_by = 10
def get_queryset(self):
params = {
"uid": str(self.request.user.id),
"param": str(self.kwargs['param'])
}
query = 'SELECT f.id ' \
'FROM myapp_foo f, myapp_bar b ' \
'WHERE b.foo_id = f.id AND ' \
'b.param >= %(param)s AND ' \
'f.dt_tm >= NOW() AND ' \
'(SELECT COUNT(*) FROM myapp_baz z ' \
'WHERE z.user_id = %(uid)s AND ' \
'z.qux_id = f.qux_id) = 0 ' \
'ORDER BY f.dt_tm;'
return Foo.objects.raw(query, params)
Это дает ошибку:
object of type 'RawQuerySet' has no len()
Я хотел бы рассчитать количество с аналогичным запросом SQL, а затем использовать параметры LIMIT и OFFSET, чтобы сделать нумерацию страниц. Я прочитал несколько предложений, в которых элементы списка учитываются для получения len, но это не кажется удовлетворительным, поскольку это было бы неэффективно, если бы в запросе не было небольшого LIMIT (что в любом случае не позволило бы использовать нумерацию страниц).
Обновление: я только заметил, что paginate_by ожидает устаревания.
Для начала как добавить метод count к возвращаемому объекту?
4 ответа
Более эффективным решением, чем другие альтернативы, было бы написать свой собственный RawQuerySet
замена. Я показываю код ниже, но вы также можете получить доступ к нему здесь. Абсолютно не гарантируется отсутствие ошибок; тем не менее я использую его в Django 1.11 на Python 3 (с PostgreSQL в качестве базы данных; должен работать и с MySQL). Проще говоря, этот класс добавляет соответствующий LIMIT
а также OFFSET
предложения к вашему сырому SQL-запросу. В этом нет ничего сумасшедшего, просто простая конкатенация строк, поэтому не включайте эти пункты в ваш необработанный SQL-запрос.
Класс
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
Как это использовать
Пользовательский менеджер
Я использую запрос выше через пользовательский менеджер:
class MyModelRawManager(models.Manager):
def raw(self, raw_query, params=None, translations=None, using=None):
if using is None:
using = self.db
return PaginatedRawQuerySet(raw_query, model=self.model, params=params, translations=translations, using=using)
def my_raw_sql_method(some_arg):
# set up your query and params
query = 'your query'
params = ('your', 'params', 'tuple')
return self.raw(raw_query=query, params=params)
Пользовательский класс нумерации страниц
Для завершения я также включаю класс нумерации страниц:
from rest_framework.pagination import PageNumberPagination
class MyModelResultsPagination(PageNumberPagination):
"""Fixed page-size pagination with 10 items."""
page_size = 10
max_page_size = 10
Ваш список APIView
class MyModelView(generics.ListAPIView):
serializer_class = MyModelSerializer
pagination_class = MyModelResultsPagination
def get_queryset(self):
return MyModel.raw_manager.my_raw_sql_method(some_arg)
Слово предостережения
PaginatedRawQuerySet
Класс, хотя он и был для меня функциональным, не был тщательно протестирован, но я полагаю, что он дает представление о том, что требуется для более эффективного решения, чем выбор всех элементов в вашем наборе запросов для каждого вызова.
Вы можете заметить, что есть обычай count
реализация метода (изначально отсутствует в RawQuerySet
), который рассчитывается по телефону self.model.objects.count()
, Без этого метода paginator будет оценивать len(your_raw_queryset)
, что будет иметь такое же влияние на производительность, как и другой ответ.
Этот класс не является универсальной заменой для RawQuerySet, что означает, что вы должны добавить свои собственные настройки, чтобы они соответствовали вашим потребностям.
Например, если вам нужно что-то более сложное, вы можете добавить еще один атрибут к PaginatedRawQuerySet
класс, называется raw_count_query
, который затем будет вызван внутри count()
вместо подсчета всех объектов, как это происходит сейчас (это будет использоваться в случаях, когда вам нужна фильтрация; raw_count_query предоставит SQL для подсчета подмножества на основе ваших условий).
Если вы приведете необработанный набор запросов к списку перед его возвратом, это должно предотвратить 'RawQuerySet' has no len()
ошибка.
return list(Foo.objects.raw(query))
Как вы говорите, это будет неэффективно, так как будет загружать весь набор запросов.
Можно было бы написать собственный класс разбиения на страницы, который эффективно разбивает на страницы с использованием limit и offset, и использовать его в своем представлении с атрибутом pagination_class.
У меня была та же проблема, и я обнаружил, что вместо использования raw вы можете использовать extra:
(...)
return Foo.objects.extra(where=query, params=params)
дополнительные переменные
where=['data->>"$.SOMETHING" = %s OR data->>"$.SOMETHING" = %s OR data->>"$.SOMETHING" = %s', 'data->>"$.GROUP" LIKE %s']
params=['EX1', 'EX2', 'EX3', '%EXEMPLE4%']
Примечание: основной вопрос заключается в использовании RawQuerySet с такими же свойствами QuerySet, лучший способ IMHO - использовать дополнительный API QuerySet, если это возможно.
Улучшенное решение от:
Класс
Я изменил содержаниеcount
метод, определенный вPaginatedRawQuerySet
class (фрагмент кода взят отсюда ) и небольшое исправление методаset_limits
, такие как:
def count(self):
from django.db import connection
if self._result_cache is not None:
return len(self._result_cache)
params = ["""'%s'""" % p for p in self.params]
sql = "SELECT COUNT(*) FROM (" + (self.raw_query % tuple(params)) + ") B;"
cursor = connection.cursor()
cursor.execute(sql)
row = cursor.fetchone()
return row[0]
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
# here is important to convert 'self.params' to a tuple.
self.params = tuple(self.params or ()) + 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)
Как это использовать
Пользовательский менеджер
class MyModelRawManager(models.Manager):
def raw(self, raw_query, params=None, translations=None, using=None):
if using is None:
using = self.db
return PaginatedRawQuerySet(raw_query, model=self.model, params=params, translations=translations, using=using)
Пользовательский класс нумерации страниц
from rest_framework.pagination import PageNumberPagination
class MyModelResultsPagination(PageNumberPagination):
"""Fixed page-size pagination with 10 items."""
page_size = 10
max_page_size = 10
Модель
class ModelTest(models.Model):
raw_manager = MyModelRawManager()
Вид
class MyModelView(generics.ListAPIView):
serializer_class = MyModelSerializer
pagination_class = MyModelResultsPagination
def get_queryset(self):
query_sql = "select * from offices"
return ModelTest.raw_manager.raw_query(query_sql)
def list(self, request, *args, **kwargs):
self.queryset = self.get_queryset()
paginated = self.paginate_queryset(self.queryset)
data = self.serializer_class(paginated, many=True).data
response = self.paginator.get_paginated_response(data)
return response