Сохранять приоритет 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().