Сохранять приоритет SQL-операторов при построении объектов Q в Django
Я пытаюсь построить сложный запрос в Django, добавив объекты Q на основе списка пользовательских входов:
from django.db.models import Q
q = Q()
expressions = [
{'operator': 'or', 'field': 'f1', 'value': 1},
{'operator': 'or', 'field': 'f2', 'value': 2},
{'operator': 'and', 'field': 'f3', 'value': 3},
{'operator': 'or', 'field': 'f4', 'value': 4},
]
for item in expressions:
if item['operator'] == 'and':
q.add(Q(**{item['field']:item['value']}), Q.AND )
elif item['operator'] == 'or':
q.add(Q(**{item['field']:item['value']}), Q.OR )
Исходя из этого, я ожидаю получить запрос со следующим условием:
f1 = 1 or f2 = 2 and f3 = 3 or f4 = 4
который на основе приоритета оператора по умолчанию будет выполняться как
f1 = 1 or (f2 = 2 and f3 = 3) or f4 = 4
однако, я получаю следующий запрос:
((f1 = 1 or f2 = 2) and f3 = 3) or f4 = 4
Похоже, что объект Q() заставляет условия оцениваться в том порядке, в котором они были добавлены.
Есть ли способ сохранить приоритет SQL по умолчанию? В основном я хочу сказать ORM не добавлять скобки в мои условия.
Ответы
Ответ 1
Кажется, что вы не единственная проблема с аналогичной. (отредактированный из-за комментария @hynekcer)
Обходным решением будет "разобрать" входящие параметры в список объектов Q()
и создать запрос из этого списка:
from operator import or_
from django.db.models import Q
query_list = []
for item in expressions:
if item['operator'] == 'and' and query_list:
# query_list must have at least one item for this to work
query_list[-1] = query_list[-1] & Q(**{item['field']:item['value']})
elif item['operator'] == 'or':
query_list.append(Q(**{item['field']:item['value']}))
else:
# If you find yourself here, something went wrong...
Теперь query_list
содержит отдельные запросы как Q()
или отношения Q() AND Q()
между ними.
Список может быть reduce()
d с оператором or_
для создания остальных отношений OR
и использоваться в filter()
, get()
и т.д. Query:
MyModel.objects.filter(reduce(or_, query_list))
PS: Хотя ответ Кевина умный, с использованием eval()
считается плохим практика, и ее следует избегать.
Ответ 2
Так как приоритет SQL такой же, как приоритет Python, когда дело доходит до AND
, OR
и NOT
, вы должны иметь возможность добиться того, чего хотите, позволяя Python анализировать выражение.
Один быстрый и грязный способ сделать это - построить выражение в виде строки и позволить Python eval()
ему.
from functools import reduce
ops = ["&" if item["operator"] == "and" else "|" for item in expressions]
qs = [Q(**{item["field"]: item["value"]}) for item in expressions]
q_string = reduce(
lambda acc, index: acc + " {op} qs[{index}]".format(op=ops[index], index=index),
range(len(expressions)),
"Q()"
) # equals "Q() | qs[0] | qs[1] & qs[2] | qs[3]"
q_expression = eval(q_string)
Python будет анализировать это выражение в соответствии со своим собственным приоритетом оператора, а результирующее предложение SQL будет соответствовать вашим ожиданиям:
f1 = 1 or (f2 = 2 and f3 = 3) or f4 = 4
Конечно, использование eval()
с поставленными пользователем строками было бы серьезным риском для безопасности, поэтому здесь я создаю объекты Q
отдельно (так же, как вы это сделали) и просто ссылаясь на них в eval строка. Поэтому я не думаю, что здесь есть дополнительные последствия для безопасности использования eval()
.