如何添加括号以使用 Q() 构建复杂的动态 Django 过滤器?

How to add parentheses to build a complicated dynamic Django filter with Q()?

我想做一个复杂的过滤器:

queryset.filter(
  (Q(k__contains=“1”) & Q(k__contains=“2”) & (~Q(k__contains=“3”))) | 
  (Q(k1__contains=“2”) & (~Q(k4__contains=“3”)))
)

结构是固定的,但查询是动态的,取决于给定输入指定的情况。

输入可以是例如:

(k=1&k=2&~k=3) | (k1=1&~k4=3)

(k=1&~k=3) | (k1=1&~k4=3) | (k=4&~k=3)

如何添加括号来构建此查询以使其 运行 符合预期?

最后我没能用 django Q 完成这项工作。

错误使用 extra 来构建类似

的代码

queryset.extra(where = ["(k1 like '%1%' and k2 like '%2%' and (k3 not like '%3%')) or (k1 like '%4%' and (k3 not like '%3%'))"] ) 它有效。

给出两个答案:一个简单,一个完美

1) 简单答案 类似的简单表达式:

你的表达很简单,用一个简短的 Python 代码似乎更好地阅读它。最有用的简化是括号不嵌套。不太重要的简化是运算符 OR ("|") 从不在括号中。运算符 NOT 仅在括号内使用。运算符 NOT 永远不会重复两次(“~~”)。

语法: 我以 EBNF 语法规则的形式表达这些简化,稍后在讨论 Python 代码时可能会有用。

expression = term, [ "|", term ];
term       = "(", factor, { "&", factor }, ")";
factor     = [ "~" ], variable, "=", constant;

variable   = "a..z_0..9";           # anything except "(", ")", "|", "&", "~", "="
constant   = "0-9_a-z... ,'\"";     # anything except "(", ")", "|", "&", "~", "="

.strip() 方法可以轻松处理可选的空格,可以像数学中那样自由地接受表达式。支持常量内的空格。

解法:

def compile_q(input_expression):
    q_expression = ~Q()  # selected empty
    for term in input_expression.split('|'):
        q_term = Q()     # selected all
        for factor in term.strip().lstrip('(').rstrip(')').split('&'):
            left, right = factor.strip().split('=', 1)
            negated, left = left.startswith('~'), left.lstrip('~')
            q_factor = Q(**{left.strip() + '__contains': right.strip()})
            if negated:
                q_factor = ~q_factor
            q_term &= q_factor
        q_expression |= q_term
    return q_expression

多余的空满Q表达式~Q()Q()最终被Django优化淘汰

示例:

>>> expression = "(k=1&k=2&~k=3) | ( k1 = 1 & ~ k4 = 3 )"
>>> qs = queryset.filter(compile_q(expression))

检查:

>>> print(str(qs.values('pk').query))        # a simplified more readable sql print
SELECT id FROM ... WHERE
((k LIKE %1% AND k LIKE %2% AND NOT (k LIKE %3%)) OR (k1 LIKE %1% AND NOT (k4 LIKE %3%)))
>>> sql, params = qs.values('pk').query.get_compiler('default').as_sql()
>>> print(sql); print(params)                # a complete parametrized escaped print
SELECT... k LIKE %s ...
[... '%2%', ...]

第一个“print”是一个 Django 命令,用于简化更易读 SQL 没有撇号和转义,因为它实际上是委托给驱动程序。第二个打印是一个更复杂的参数化 SQL 命令,具有所有可能的安全转义。


2) 完美但更长的解决方案

这个答案可以编译布尔运算符的任意组合“|”、“&”、“~”、任何级别的嵌套括号 和比较运算符“=”到 Q() 表达式:

解法:(没多复杂)

import ast    # builtin Python parser
from django.contrib.auth.models import User
from django.db.models import Q


def q_combine(node: ast.AST) -> Q:
    if isinstance(node, ast.Module):
        assert len(node.body) == 1 and isinstance(node.body[0], ast.Expr)
        return q_combine(node.body[0].value)
    if isinstance(node, ast.BoolOp):
        if isinstance(node.op, ast.And):
            q = Q()
            for val in node.values:
                q &= q_combine(val)
            return q
        if isinstance(node.op, ast.Or):
            q = ~Q()
            for val in node.values:
                q |= q_combine(val)
            return q
    if isinstance(node, ast.UnaryOp):
        assert isinstance(node.op, ast.Not)
        return ~q_combine(node.operand)
    if isinstance(node, ast.Compare):
        assert isinstance(node.left, ast.Name)
        assert len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
        assert len(node.comparators) == 1 and isinstance(node.comparators[0], ast.Constant)
        return Q(**{node.left.id + '__contains': str(node.comparators[0].value)})
    raise ValueError('unexpected node {}'.format(type(node).__name__))

def compile_q(expression: str) -> Q:
    std_expr = (expression.replace('=', '==').replace('~', ' not ')
                .replace('&', ' and ').replace('|', ' or '))
    return q_combine(ast.parse(std_expr))

示例: 与我之前的回答相同,更复杂的是:

>>> expression = "~(~k=1&(k1=2|k1=3|(k=5 & k4=3))"
>>> qs = queryset.filter(compile_q(expression))

相同的示例给出相同的结果,嵌套更多的示例给出正确的更多嵌套的结果。

EBNF 语法规则在这种情况下并不重要,因为在此解决方案中没有实现任何解析器,并且使用了标准 Python 解析器 AST。与递归有点不同。

expression = term, [ "|", term ];
term       = factor, { "&", factor };
factor     = [ "~" ], variable, "=", constant | [ "~" ],  "(", expression, ")";

variable   = "a..z_0..9";  # any identifier acceptable by Python, e.g. not a Python keyword
constant   = "0-9_a-z... ,'\"";    # any numeric or string literal acceptable by Python

这个答案可以编译布尔运算符的任意组合“|”、“&”、“~”、任何级别的嵌套括号 和比较运算符“=”到 Q() 表达式:

解法:(没多复杂)

import ast    # builtin Python parser
from django.contrib.auth.models import User
from django.db.models import Q


def q_combine(node: ast.AST) -> Q:
    if isinstance(node, ast.Module):
        assert len(node.body) == 1 and isinstance(node.body[0], ast.Expr)
        return q_combine(node.body[0].value)
    if isinstance(node, ast.BoolOp):
        if isinstance(node.op, ast.And):
            q = Q()
            for val in node.values:
                q &= q_combine(val)
            return q
        if isinstance(node.op, ast.Or):
            q = ~Q()
            for val in node.values:
                q |= q_combine(val)
            return q
    if isinstance(node, ast.UnaryOp):
        assert isinstance(node.op, ast.Not)
        return ~q_combine(node.operand)
    if isinstance(node, ast.Compare):
        assert isinstance(node.left, ast.Name)
        assert len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
        assert len(node.comparators) == 1 and isinstance(node.comparators[0], ast.Constant)
        return Q(**{node.left.id + '__contains': str(node.comparators[0].value)})
    raise ValueError('unexpected node {}'.format(type(node).__name__))

def compile_q(expression: str) -> Q:
    std_expr = (expression.replace('=', '==').replace('~', ' not ')
                .replace('&', ' and ').replace('|', ' or '))
    return q_combine(ast.parse(std_expr))

示例: 与我之前的回答相同,更复杂的是:

>>> expression = "~(~k=1&(k1=2|k1=3|(k=5 & k4=3))"
>>> qs = queryset.filter(compile_q(expression))

相同的示例给出相同的结果,嵌套更多的示例给出正确的更多嵌套的结果。

EBNF 语法规则在这种情况下并不重要,因为在此解决方案中没有实现任何解析器,而是使用标准 Python 解析器 AST。与递归有点不同。

expression = term, [ "|", term ];
term       = factor, { "&", factor };
factor     = [ "~" ], variable, "=", constant | [ "~" ],  "(", expression, ")";

variable   = "a..z_0..9";  # any identifier acceptable by Python, e.g. not a Python keyword
constant   = "0-9_a-z... ,'\"";    # any numeric or string literal acceptable by Python