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

Posted

技术标签:

【中文标题】Django:为相关表构建动态 Q 查询【英文标题】:Django: Building dynamic Q queries for related tables 【发布时间】:2020-04-22 04:38:23 【问题描述】:

[编辑]

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

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

[/EDIT]

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

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

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

Min. temperature = 400
Max. temperature = 500

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

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

运行产生一些结果,但不是预期的结果。它返回 Run 1 和 Run 4,而它应该只返回 Run 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 使用相同的复合 reduce 方法,但将 queryset 的外部逻辑门更改为 OR 会产生相同的错误结果,运行 1 和运行 4:

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

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

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

【问题讨论】:

【参考方案1】:

您的问题是您需要同时满足这两个条件,并且它们在 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() 对象,即使您想用简短的单行表达式重写它并添加助记临时变量。


EDIT具体的例子可以重写for 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,
)

【讨论】:

我无法按照 Exists() 文档让它工作,尽管我会奖励赏金,因为你确实提供了对我最终通过遵循你的 sn-p 使用的其他方法的宝贵见解。谢谢! @Mormoran 我添加了一个例子。

以上是关于Django:为相关表构建动态 Q 查询的主要内容,如果未能解决你的问题,请参考以下文章

Django ORM - 通过多个相关对象和 Q 查询选择

在 Django 中使用 Q() 动态构建复杂查询 [关闭]

Django添加Q过滤器以查询相关对象存在时,条件查询

Django-数据库查询性能相关

Django 通过查询相关表来访问 M2M 字段的属性

Django模型层相关