Django:为相关表构建动态 Q 查询

Django: Building dynamic Q queries for related tables

[编辑]

我已经创建了一个示例 Django Repl.it 游乐场预加载了这个确切的案例:

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

[/EDIT]

我正在尝试根据相关对象过滤 table 上的对象,但这样做时遇到问题。

我有一个tableRun:

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

每个Run对象都有相关的tableRunValue:

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。电压、温度、压力等

为简单起见,假设我要筛选的字段是 "Min. Temperature" 和 "Max. Temperature"。

例如:

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 是浮点数,但为简单起见,我们将其保留为整数)。

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

Min. temperature = 400
Max. temperature = 500

那套过滤器应该只有return 运行 上面Run个实例例子,其中阈值下限在400以上并且上限低于 500。所有其他 Run 不合格。

那么我需要 return 所有 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)

运行ning 会产生 一些 结果,但不是预期的结果。它 returns 运行 1 和 运行 4,当它应该只有 return 运行 1.

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)

相同的结果,所以值范围不是那里的问题,这是逻辑分组(我认为?)。

所以我尝试做了两个reduce 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 都更改为与门)

这产生了 0 次点击。

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

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

也许我在这里把自己搞得太复杂了,有一些我没有看到的非常简单的东西(我已经尝试解决这个逻辑难题 2 天了,有点狭隘的视野。但我宁愿希望它是可以解决的,而且很简单。

如有任何帮助或问题,我们将不胜感激。

您的问题是您需要同时满足这两个条件,并且它们永远不会在与 RunValue 相关的同一行上都有效 table。您想要 select 在该范围内具有行 "Min. Temperature" 并且类似地还有 "Max. Temperature" 有效行的对象。您必须使用子查询。

最好使用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)

您知道 "Min. Temperature" <= "Max. Temperature",但数据库优化器不知道。我通过删除范围的多余条件对其进行了优化。最好完全去掉一个无用的条件 "Max. Temperature" <= 999999.

在您阅读该文档的大约几十行后,可以很容易地为 Django >=1.11 <= 2.2 Exists() condition 定制这个答案。

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


编辑具体的例子可以重写for Django < 3.0这样

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,
)