我试图通过添加基于用户输入列表的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不要在我的条件中添加括号.
由于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)
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()这里使用没有任何额外的安全隐患。
似乎您不是唯一一个遇到类似问题的人。(由于@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()被认为是一种不好的做法,应该避免。