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

时间:2017-07-19 15:19:10

标签: sql django orm

我试图在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不要在我的条件中添加括号。

2 个答案:

答案 0 :(得分:4)

由于SQL优先级与ANDORNOT的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也应该避免。