Mantenga la precedencia del operador SQL al construir objetos Q en Django
orm (2)
Estoy tratando de construir una consulta compleja en Django agregando objetos Q basados en una lista de entradas de usuario:
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 )
En base a esto, espero obtener una consulta con la siguiente condición:
f1 = 1 or f2 = 2 and f3 = 3 or f4 = 4
que, en función de la precedencia del operador por defecto se ejecutará como
f1 = 1 or (f2 = 2 and f3 = 3) or f4 = 4
Sin embargo, estoy recibiendo la siguiente consulta:
((f1 = 1 or f2 = 2) and f3 = 3) or f4 = 4
Parece que el objeto Q () obliga a evaluar las condiciones en el orden en que se agregaron.
¿Hay alguna manera de que pueda mantener la precedencia de SQL predeterminada? Básicamente quiero decirle al ORM que no agregue paréntesis en mis condiciones.
Dado que la precedencia de SQL es la misma que la precedencia de Python cuando se trata de AND
, OR
y NOT
, debe poder lograr lo que quiere al permitir que Python analice la expresión.
Una forma rápida y sucia de hacerlo sería construir la expresión como una cadena y dejar que Python la 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 analizará esta expresión de acuerdo con su propia precedencia de operador, y la cláusula SQL resultante coincidirá con sus expectativas:
f1 = 1 or (f2 = 2 and f3 = 3) or f4 = 4
Por supuesto, el uso de eval()
con cadenas proporcionadas por el usuario sería un riesgo importante para la seguridad, por lo que aquí estoy construyendo los objetos Q
separado (de la misma manera que lo hizo) y me refiero a ellos en la cadena eval. Entonces, no creo que haya implicaciones de seguridad adicionales al usar eval()
aquí.
Parece que no eres el único con un problema similar . (editado por el comentario de @hynekcer)
Una solución alternativa sería "analizar" los parámetros entrantes en una lista de objetos Q()
y crear su consulta a partir de esa lista:
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...
Ahora la query_list
consultas contiene las consultas individuales como Q()
o las relaciones Q() AND Q()
entre ellas.
La lista se puede reduce()
d con el operador or_
para crear las relaciones OR
restantes y se puede utilizar en una consulta de filter()
, get()
etc.
MyModel.objects.filter(reduce(or_, query_list))
PD: Aunque la respuesta de Kevin es inteligente, usar eval()
se considera una mala práctica y se debe evitar.