在 Django 中构造 Q 对象时保持 SQL 运算符优先级
Posted
技术标签:
【中文标题】在 Django 中构造 Q 对象时保持 SQL 运算符优先级【英文标题】:Maintain SQL operator precedence when constructing Q objects in Django 【发布时间】:2017-07-19 15:19:10 【问题描述】:我正在尝试通过基于用户输入列表添加 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 )
基于此,我希望得到具有以下 where 条件的查询:
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 不要在我的条件中添加括号。
【问题讨论】:
Q() 对象没问题。它的工作方式与使用 AND 和 OR 评估普通布尔表达式的方式相同。如果您编写一个完整的表达式,Python 会以正确的优先级对其进行解析,但是您逐步使用低级操作,它希望您必须以正确的顺序执行它们,并手动为子表达式创建适当的临时变量。您的带有q.add(..., Q.operator)
的代码与 if item['operator'] == 'and': q = q & Q(...)
elif item['operator'] == 'or': q = q | Q(...)
相同,其评估类似于立即 boolean_variable |= boolean_value
。
【参考方案1】:
似乎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()
d 和or_
运算符以创建剩余的OR
关系并用于filter()
、get()
等查询:
MyModel.objects.filter(reduce(or_, query_list))
PS:虽然Kevin's answer很聪明,但using eval()
is considered a bad practice应该避免使用。
【讨论】:
上述“jehiah.cz”的链接不相关,因为它是多年前已修复的 Django 错误的解决方法(可能是 Q 对象的早期实现)。问题中的问题不是Q对象的问题,而是评估结果的无效顺序。【参考方案2】:由于对于AND
、OR
和NOT
,SQL 优先级与 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()
有任何额外的安全隐患。
【讨论】:
哇,开箱即用的非常聪明的思考......喜欢这个解决方法。我会等着看是否有办法让 ORM 做到这一点,否则我会给它赏金。谢谢以上是关于在 Django 中构造 Q 对象时保持 SQL 运算符优先级的主要内容,如果未能解决你的问题,请参考以下文章
Django ORM:相当于 SQL `NOT IN`? `exclude` 和 `Q` 对象不起作用