在 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】:

由于对于ANDORNOT,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 GIS SQL注入漏洞

Django中Q搜索的简单应用

Django ORM:相当于 SQL `NOT IN`? `exclude` 和 `Q` 对象不起作用

有没有办法构造一个 Q 对象,它代表一个 EmptyQueryset,即总是返回一个空结果?

Django Q 对象(复杂查询)是不是安全?

ORM