我试图在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不要在我的条件中添加括号。
答案 0 :(得分:4)
由于SQL优先级与AND
,OR
和NOT
的Python优先级相同,因此您应该能够通过让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()
不会产生任何额外的安全隐患。
答案 1 :(得分:4)
似乎是you are not the only one with a similar problem。(由于@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()
与or_
运算符一起创建剩余的OR
关系,并用于filter()
,get()
等查询:
MyModel.objects.filter(reduce(or_, query_list))
PS:虽然Kevin's answer很聪明,using eval()
is considered a bad practice也应该避免。