在Django中构造Q对象时保持SQL运算符优先级

Mar*_*ski 17 sql django orm

我试图通过添加基于用户输入列表的Q对象在Django中构建一个复杂的查询:

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 )
Run Code Online (Sandbox Code Playgroud)

基于此,我期望得到以下条件的查询:

f1 = 1 or f2 = 2 and f3 = 3 or f4 = 4
Run Code Online (Sandbox Code Playgroud)

其中,基于默认运算符优先级将执行为

f1 = 1 or (f2 = 2 and f3 = 3) or f4 = 4
Run Code Online (Sandbox Code Playgroud)

但是,我得到以下查询:

((f1 = 1 or f2 = 2) and f3 = 3) or f4 = 4
Run Code Online (Sandbox Code Playgroud)

看起来Q()对象强制条件按照添加顺序进行评估.

有没有办法可以保持默认的SQL优先级?基本上我想告诉ORM不要在我的条件中添加括号.

Kev*_*nry 5

由于SQL优先级相同优先级的Python当涉及到ANDORNOT,你应该能够通过让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)
Run Code Online (Sandbox Code Playgroud)

Python 会根据自己的运算符优先级解析这个表达式,生成的 SQL 子句将符合您的期望:

f1 = 1 or (f2 = 2 and f3 = 3) or f4 = 4
Run Code Online (Sandbox Code Playgroud)

当然,eval()与用户提供的字符串一起使用将是一个主要的安全风险,所以在这里我Q分别构造对象(以与您相同的方式)并仅在 eval 字符串中引用它们。所以我认为在eval()这里使用没有任何额外的安全隐患。


Joh*_*fis 5

似乎您不是唯一一个遇到类似问题的人(由于@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...
Run Code Online (Sandbox Code Playgroud)

现在query_list包含单个查询Q()Q() AND Q()它们之间的关系。
该列表可以reduce()使用Dor_运算符来创建其余OR的关系,并在使用filter()get()等查询:

MyModel.objects.filter(reduce(or_, query_list))
Run Code Online (Sandbox Code Playgroud)

PS:虽然凯文的回答很聪明,但使用eval()被认为是一种不好的做法,应该避免。