Строку в объект Q
Моя строка содержит скобки для определения старшинства операций. Операции: and, or, eq (равно), ne (не равно), gt (больше чем), lt (меньше чем)
пример строки: "(date eq '2016-05-01') AND ((number_of_calories gt 20) OR (number_of_calories lt 10))"
>Как я могу фильтровать модели django с заданными учетными данными? какой способ лучше?
В настоящее время у меня есть следующее решение, но это не очень хороший подход, так как он уязвим для SQL Injection:
q_string = "(date eq '2016-05-01') AND ((number_of_calories gt 20) OR (number_of_calories lt 10))"
query = convert_string(q_string)
# query = "(date = '2016-05-01') AND ((number_of_calories > 20) OR (number_of_calories < 10))"
Users.objects.raw('SELECT * FROM Users WHERE ' + query)
Итак, я думаю о преобразовании строки в объект Q, что-то вроде:
q_object = Q(q_string)
Users.models.filter(q_object)
как я могу преобразовать строку в объект Q?
В настоящее время я активно использую объект Q в проекте, который использует параметры get пользователей для фильтрации результатов поиска.
Вот фрагмент
some_initial_query_object = Model.objects.all()
qs_result_dates = []
qs_result_dates.append(
Q(
event_date__start_date_time__gte='2021-08-01',
event_date__start_date_time__lt='2021-09-01' + datetime.timedelta(days=1)
)
)
some_initial_query_object = some_initial_query_object.filter(qs_result_dates)
В вашем сценарии вы можете использовать | для OR и & для AND
Q(date='2016-05-01')
&
Q(number_of_calories__gt=20, number_of_calories__lt=10)
Для разбора строки запроса типа:
string = "((num_of_pages gt 20) OR (num_of_pages lt 10)) AND (date gt '2016-05-01')"
вы можете использовать пакет pyparsing (не эксперт, но очень мощная библиотека) с объектами django Q:
- разбор кода:
import pyparsing as pp
import operator as op
from django.db.models import Q
word = pp.Word(pp.alphas, pp.alphanums + "_-*'")
operator = pp.oneOf('lt gt eq').setResultsName('operator')
number = pp.pyparsing_common.number()
quoted = pp.quotedString().setParseAction(pp.removeQuotes)
term = (word | number | quoted)
key = term.setResultsName('key')
value = term.setResultsName('value')
group = pp.Group(key + operator + value)
def q_item(item):
"""Helper for create django Q() object"""
k = f'{item.key}__{item.operator}'
v = item.value
return Q(**{k: v})
class BaseBinary:
def __init__(self, tokens):
self.args = tokens[0][0::2]
def __repr__(self):
return f'{self.__class__.__name__}({self.symbol}):{self.args}'
def evaluate(self):
a = q_item(self.args[0]) if not isinstance(self.args[0], BaseBinary) else self.args[0].evaluate()
b = q_item(self.args[1]) if not isinstance(self.args[1], BaseBinary) else self.args[1].evaluate()
return self.op(a, b)
class BoolNotOp(BaseBinary):
symbol = 'NOT'
op = op.not_
def __init__(self, tokens):
super().__init__(tokens)
self.args = tokens[0][1]
def evaluate(self):
a = q_item(self.args) if not isinstance(self.args, BaseBinary) else self.args.evaluate()
return ~a
class BoolAndOp(BaseBinary):
symbol = 'AND'
op = op.and_
class BoolOrOp(BaseBinary):
symbol = 'OR'
op = op.or_
expr = pp.infixNotation(group,
[('NOT', 1, pp.opAssoc.RIGHT, BoolNotOp),
('AND', 2, pp.opAssoc.LEFT, BoolAndOp),
('OR', 2, pp.opAssoc.LEFT, BoolOrOp)])
Теперь дана строка вида:
string = "(date gt '2016-05-01') AND ((num_of_pages gt 20) OR (num_of_pages lt 10))"
к синтаксическому анализатору:
parser = expr.parseString(string)[0]
print(parser.evaluate())
дайте нам наши Q-объекты:
(AND: ('date__gt', '2016-05-01'), (OR: ('num_of_pages__gt', 20), ('num_of_pages__lt', 10)))
готов к фильтрации
class Book(models.Model):
title = models.CharField(max_length=200)
counter = models.PositiveIntegerField(default=0)
date = models.DateField(auto_now=True)
num_of_pages = models.PositiveIntegerField(default=0)
qs = Book.objects.filter(parser.evaluate())
print(qs.query)
SELECT "core_book"."id", "core_book"."title", "core_book"."counter", "core_book"."date", "core_book"."num_of_pages" FROM "core_book" WHERE ("core_book"."date" > 2016-05-01 AND ("core_book"."num_of_pages" > 20 OR "core_book"."num_of_pages" < 10))
P.S не полностью протестировано.
Вот пример динамической фильтрации с использованием DRF путем переопределения метода get_queryset
на ModelViewSet
, который я использую во всех своих проектах. Используя этот метод, я могу использовать всю мощь фреймворка Django-ORM из фронтенда.
views.py
def BaseAPIView(...):
''' base view for other views to inherit '''
def get_queryset(self):
queryset = self.queryset
# get filter request from client:
filter_string = self.request.query_params.get('filter')
# apply filters if they are passed in:
if filters:
filter_dictionary = json.loads(filter_string)
queryset = queryset.filter(**filter_dictionary)
return queryset
Теперь url запроса будет выглядеть, например, так: my_website.com/api/users?filter={"first_name":"John"}
Который может быть построен следующим образом:
script.js
// using ajax as an example:
var filter = JSON.stringify({
"first_name" : "John"
});
$.ajax({
"url" : "my_website.com/api/users?filter=" + filter,
"type" : "GET",
...
});
Некоторые преимущества:
- не нужно указывать, какие поля можно фильтровать в каждом классе представления
- напишите его один раз, используйте везде
- фильтрация на переднем крае выглядит точно так же, как фильтрация в django
- можно делать то же самое с
exclude
Некоторые недостатки:
- потенциальные риски безопасности, если вы хотите, чтобы некоторые поля не подлежали фильтрации
- менее интуитивный внешний код для запроса таблицы
В целом, этот подход оказался для меня гораздо более полезным, чем все существующие пакеты.