如何添加括号以使用 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
我想做一个复杂的过滤器:
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