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

标签 python django django-queryset django-q

[编辑]

我创建了一个示例 Django Repl.it Playground ,预加载了这个确切的案例:

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

[/编辑]

我正在尝试根据相关对象过滤表上的对象,但遇到问题。

我有一个表运行:

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

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

Min. temperature = 400
Max. temperature = 500

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

那么我需要返回所有 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,Run 1 合格,但 Run 4 的最高温度超过 500,它不合格。过滤器需要通过同时查看两个范围(最小值和最大值)来排除对象实例。

打印的查询如下:

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

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

所有具有 RunValue 实例的运行,其中 RunValue.run_parameter 为“最低温度”或“最高温度”且 RunValue.value 介于 400 和 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查询(看起来有点粗糙),也就是说:

具有名称为“最低温度”且值高于 400 的 RunValue 实例的所有运行,以及具有名称为“最高温度”且值低于 400 的 RunValue 实例的所有运行超过 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)

您知道“最低温度”<=“最高温度”,但数据库优化器不知道。我通过删除范围的多余条件来优化它。最好完全删除无用的条件“最高温度”<= 999999。

此答案可以轻松定制为 Django >=1.11 <= 2.2 Exists() condition在您阅读该文档的大约数十行之后。

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

<小时/>

编辑具体示例可以这样为 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,
)

关于python - Django:为相关表构建动态 Q 查询,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59581548/

相关文章:

python - 两个时间戳系列之间的工作时间,不包括周末和节假日

python - 整数超出范围 - Django

python - 我如何使用 Django 获取 IPV6 地址?

python - DRF一对多序列化——缺少字段的AttributeError

django - 具有外键的模型每次查询大约需要 90 秒(我认为是外键模型/序列化器问题)

python - 在tkinter.treeview中选择一个单元格并获取单元格数据

python - 改进最小/最大下采样

python - 正则表达式获取包含字母和(数字/某些特殊)的 "words",但不仅限于数字

django - 对象没有属性 'distance' - GeoDjango

django - 使用包含 RadioSelect 小部件的 ModelChoiceField 在 ModelForm 中的查询集中获取 request.user