[编辑]

我创建了一个示例Django Repl.it操场,其中预装了这种确切的情况:

https://repl.it/@mormoran/Django-Building-dynamic-Q-queries-for-related-tables

[/编辑]

我正在尝试根据相关对象过滤表上的对象,但是这样做很麻烦。

我有一张桌子Run

class Run(models.Model):
    start_time = models.DateTimeField(db_index=True)
    end_time = models.DateTimeField()


每个Run对象都有相关的表RunValue

class RunValue(models.Model):
    run = models.ForeignKey(Run, on_delete=models.CASCADE)
    run_parameter = models.CharField(max_length=50)
    value = models.FloatField(default=0)


RunValue中,我们存储了运行的详细特征,称为run_parameter。诸如电压,温度,压力等因素。

为了简单起见,我们假设要过滤的字段是“最低温度”和“最高温度”。

因此,例如:

Run 1:
    Run Values:
        run_parameter: "Min. Temperature", value: 430
        run_parameter: "Max. Temperature", value: 436

Run 2:
    Run Values:
        run_parameter: "Min. Temperature", value: 627
        run_parameter: "Max. Temperature", value: 671

Run 3:
    Run Values:
        run_parameter: "Min. Temperature", value: 642
        run_parameter: "Max. Temperature", value: 694

Run 4:
    Run Values:
        run_parameter: "Min. Temperature", value: 412
        run_parameter: "Max. Temperature", value: 534


(RunValue.value是浮点数,但是为了简单起见,我们将其保留为int)。

我的页面上有两个HTML输入,用户在其中输入min和max(用于温度)。它们都可以留为空白,或者只留一个,或者两者都留,所以它是一个开放式过滤器,可以定义要过滤的范围或不过滤。例如,如果用户要输入:

Min. temperature = 400
Max. temperature = 500


该组过滤器仅应从上述Run实例示例返回运行1,其中下限阈值高于400,上限阈值低于500。所有其他Run不符合条件。

因此,我需要返回所有Run对象实例,其中RunValue与用户输入的过滤器匹配。

这是我尝试过的:

# Grabbing temp ranges from request and setting default filter mins and maxs:
temp_ranges = [0, 999999] # Defaults in case the user does not set anything

if min_temp_filter:
    temp_ranges = [min_temp_filter, 999999]

if max_temp_filter:
    temp_ranges = [0, max_temp_filter]

if min_temp_filter and max_temp_filter:
    temp_ranges = [min_temp_filter, max_temp_filter]

# Starting Q queries
temp_q_queries = [
    Q(runvalue__run_parameter__icontains='Min. Temperature'),
    Q(runvalue__run_parameter__icontains='Max. Temperature')
]

queryset = models.Q(reduce(operator.or_, temp_q_queries), runvalue__value__range=temp_ranges)
filtered_run_instances = Run.objects.filter(queryset)


运行会产生一些结果,但不会达到预期的结果。当它只应返回运行1时,它将返回运行1和运行4。

temp_ranges从400到500,运行1合格,但运行4的最高温度超过500,因此不合格。筛选器需要通过同时查看两个范围(最小值和最大值)来排除对象实例。

打印的查询如下:

(AND: (OR: ('runvalue__run_parameter__icontains', 'Min. Temperaure'), ('runvalue__run_parameter__icontains', 'Max. Temperature')), ('runvalue__value__range', ['400', '500']))


我认为我需要使用伪代码进行过滤:

All Runs that have RunValue instances where the RunValue.run_parameter is either "Min. Temperature" OR "Max. Temperature" AND the RunValue.value are between 400 and 500

然后,我认为我应该将Q查询中的值范围作为常规Django过滤器(以逗号分隔):

temp_q_queries = [
    Q(runvalue__run_parameter__icontains='Min. Temperature', runvalue__value__range=temp_ranges),
    Q(runvalue__run_parameter__icontains='Max. Temperature', runvalue__value__range=temp_ranges)
]

queryset = models.Q(reduce(operator.or_, temp_q_queries))
filtered_run_instances = Run.objects.filter(queryset)


结果相同,因此值范围不是问题所在,而是逻辑分组(我认为?)。

因此,我尝试做两个减少Q的查询(看起来有点粗糙),以便说:

All Runs that have RunValue instances where the name is "Min. Temperature" AND the values are higher than 400, AND all Runs that have RunValue instances where the name is "Max. Temperature" AND the values are lower than 500

temp_q_queries = [
    models.Q(reduce(operator.and_, [Q(runvalue__run_parameter__icontains='Min. Temperature'), Q(runvalue__value__gte=temp_ranges[0])]),
    models.Q(reduce(operator.and_, [Q(runvalue__run_parameter__icontains='Max. Temperature'), Q(runvalue__value__lte=temp_ranges[1])]))
]

queryset = models.Q(reduce(operator.and_, temp_q_queries))
filtered_run_instances = Run.objects.filter(queryset)


(请注意,所有3个reduce都更改为AND门)

这产生了0个命中。

temp_q_queries使用相同的复合归约方法,但将queryset的外部逻辑门更改为OR会产生相同的错误结果,即运行1和运行4:

queryset = models.Q(reduce(operator.or_, temp_q_queries))
filtered_run_instances = Run.objects.filter(queryset)


也许我在这里使自己过于复杂,并且有一个非常简单的我没有看到(我已经尝试解决这个逻辑难题两天了,获得了一些隧道洞察力。但是我还是希望它可以解决,并且简单。

任何帮助或问题将不胜感激。

最佳答案

您的问题是您需要同时满足这两个条件,并且它们在与RunValue相关的表的同一行上永远都无效。您想要选择在该范围内具有“最低温度”行并且同样具有“最高温度”有效行的根对象。您必须使用子查询。

最好是使用Django 3.0 Exists() subquery condition。可以很容易地为一个旧的Django定制它。

一个具体的例子:

from django.db.models import Exists, OuterRef

queryset = Run.objects.filter(
    Exists(RunValue.objects.filter(
        run=OuterRef('pk'),
        run_parameter='Min. temperature',
        value__gte=400)),
    Exists(RunValue.objects.filter(
        run=OuterRef('pk'),
        run_parameter='Max. temperature',
        value__lte=500)),
)


通用解决方案也是如此,因为您需要动态过滤器:

filter_data = {
    'Min. temperature': 400,
    'Max. temperature': 500,
}

param_operators = {
    'Min. Temperature': 'gte',
    'Max. Temperature': 'lte',
    # much more supported parameters... e.g. 'some boolean as 0 or 1': 'eq'.
}

conditions = []
for key, value in filter_data.items():
    if value is not None:
        conditions.append(Exists(RunValue.objects.filter(
            run=OuterRef('pk'),
            run_parameter=key,
            **{'value__{}'.format(param_operators[key]): value}
        )))
queryset = Run.objects.filter(*conditions)


您知道“最低温度”
阅读大约十行文档后,可以轻松为Django >=1.11 <= 2.2 Exists() condition自定义该答案。

在这种简单情况下,即使您想用简短的单行表达式重写它并添加助记符临时变量,也不需要Q()对象。



编辑这种具体的例子可以用Django
queryset = Run.objects.annotate(
    min_temperature_filter=Exists(RunValue.objects.filter(
        run=OuterRef('pk'),
        run_parameter='Min. temperature',
        value__gte=400)),
    max_temperature_filter=Exists(RunValue.objects.filter(
        run=OuterRef('pk'),
        run_parameter='Max. temperature',
        value__lte=500)),
).filter(
    min_temperature_filter=True,
    max_temperature_filter=True,
)

07-26 09:24