如何在 django 查询集中获取表的计算元素?

Posted

技术标签:

【中文标题】如何在 django 查询集中获取表的计算元素?【英文标题】:How can I get computed elements of a table in a django queryset? 【发布时间】:2013-08-18 14:50:20 【问题描述】:

我正在尝试使用 django 的 queryset API 来模拟以下查询:

SELECT EXTRACT(year FROM chosen_date) AS year, 
EXTRACT(month FROM chosen_date) AS month,
 date_paid IS NOT NULL as is_paid FROM 
    (SELECT (CASE WHEN date_due IS NULL THEN date_due ELSE date END) AS chosen_date,* FROM invoice_invoice) as t1;

这个想法主要是在某些情况下,我宁愿在某些情况下使用date_due列而不是date列,但是由于date_due是可选的,我有时不得不使用@987654325 @ 无论如何作为后备,并创建一个计算列 chosen_date 以不必更改其余查询。

这是我在模拟这个时所做的第一次尝试,我无法真正看到如何正确地使用基本 api 进行空测试,所以我选择了extra

if(use_date_due):
    sum_qs = sum_qs.extra(select='chosen_date': 'CASE WHEN date_due IS NULL THEN date ELSE date_due END')
else: 
    sum_qs = sum_qs.extra(select='chosen_date':'date')
sum_qs = sum_qs.extra(select='year': 'EXTRACT(year FROM chosen_date)',
                              'month': 'EXTRACT(month FROM chosen_date)',
                              'is_paid':'date_paid IS NOT NULL')

但我遇到的问题是,当我运行第二个查询时,我收到一个关于 chosen_date 列不存在的错误。我后来在尝试使用计算列时遇到了类似的错误(比如在annotate() 调用中),但在文档中没有找到任何关于计算列与“基本”列的不同之处。有人对此有任何见解吗?

(编辑了python代码,因为以前的版本有一个明显的逻辑缺陷(忘记了else分支)。仍然不起作用)

【问题讨论】:

我会保持简单并使用原始查询。这就是他们的目的。 【参考方案1】:

简答: 如果您使用 extra(select=...) 创建别名(或计算)列 那么您就不能在随后对filter() 的调用中使用别名列。 此外,正如您所发现的,您不能在以后的调用中使用别名列 extra(select=...)extra(where=...)

尝试解释原因:

例如:

qs = MyModel.objects.extra(select='alias_col': 'title')

#FieldError: Cannot resolve keyword 'alias_col' into field...
filter_qs = qs.filter(alias_col='Camembert')

#DatabaseError: column "alias_col" does not exist
extra_qs = qs.extra(select='another_alias': 'alias_col')

filter_qs 将尝试生成如下查询:

SELECT (title) AS "alias_col", "myapp_mymodel"."title"
FROM "myapp_mymodel"
WHERE alias_col = "Camembert";

extra_qs 尝试类似:

SELECT (title) AS "alias_col", (alias_col) AS "another_alias",
        "myapp_mymodel"."title"
FROM "myapp_mymodel";

这些都不是有效的 SQL。通常,如果您想在查询的 SELECT 或 WHERE 子句中多次使用计算列的别名,则实际上每次都需要计算它。这就是为什么 Roman Pekar 的答案可以解决您的特定问题的原因 - 而不是尝试计算 chosen_date 一次然后再次使用它,而是在每次需要时计算它。


您在问题中提到了注释/聚合。您可以在annotate() 创建的别名上使用filter()(所以我有兴趣看到您正在谈论的类似错误,根据我的经验,它相当可靠)。这是因为当您尝试过滤由 annotate 创建的别名时,ORM 会识别您正在执行的操作并将别名替换为创建它的计算。

举个例子:

qs = MyModel.objects.annotate(alias_col=Max('id'))
qs = qs.filter(alias_col__gt=0)

产生类似的东西:

SELECT "myapp_mymodel"."id", "myapp_mymodel"."title",
        MAX("myapp_mymodel"."id") AS "alias_col"
FROM "myapp_mymodel"
GROUP BY "myapp_mymodel"."id", "myapp_mymodel"."title"
HAVING MAX("myapp_mymodel"."id") > 0;

使用“HAVING MAX alias_col > 0”是行不通的。


我希望这会有所帮助。如果有什么我解释得不好的地方,请告诉我,我会看看我是否可以改进它。

【讨论】:

【参考方案2】:

这里有一些解决方法

1. 在您的特定情况下,您可以多加一个:

if use_date_due:
    sum_qs = sum_qs.extra(select=
                          'year': 'EXTRACT(year FROM coalesce(date_due, date))',
                          'month': 'EXTRACT(month FROM coalesce(date_due, date))',
                          'is_paid':'date_paid IS NOT NULL'
                        )

2.也可以使用普通的python来获取你需要的数据:

for x in sum_qs:
    chosen_date = x.date_due if use_date_due and x.date_due else x.date
    print chosen_date.year, chosen_date.month

[(y.year, y.month) for y in (x.date_due if use_date_due and x.date_due else x.date for x in sum_qs)]

3. 在 SQL 世界中,这种类型的新字段计算通常通过使用子查询或common table expression 来完成。我更喜欢 cte 因为它的可读性。可能是这样的:

with cte1 as (
    select
        *, coalesce(date_due, date) as chosen_date
    from polls_invoice
)
select
    *,
    extract(year from chosen_date) as year,
    extract(month from chosen_date) as month,
    case when date_paid is not null then 1 else 0 end as is_paid
from cte1

您还可以链接任意数量的 cte:

with cte1 as (
    select
        *, coalesce(date_due, date) as chosen_date
    from polls_invoice
), cte2 as (
    select
        extract(year from chosen_date) as year,
        extract(month from chosen_date) as month,
        case when date_paid is not null then 1 else 0 end as is_paid
    from cte2
)
select
    year, month, sum(is_paid) as paid_count
from cte2
group by year, month

所以在 django 中你可以使用 raw query 喜欢:

Invoice.objects.raw('
     with cte1 as (
        select
            *, coalesce(date_due, date) as chosen_date
        from polls_invoice
    )
    select
        *,
        extract(year from chosen_date) as year,
        extract(month from chosen_date) as month,
        case when date_paid is not null then 1 else 0 end as is_paid
    from cte1')

您将拥有带有一些附加属性的 Invoice 对象。

4. 或者您可以简单地用普通 python 替换查询中的字段

if use_date_due:
    chosen_date = 'coalesce(date_due, date)'
else: 
    chosen_date = 'date'

year = 'extract(year from )'.format(chosen_date)
month = 'extract(month from )'.format(chosen_date)
fields = 'year': year, 'month': month, 'is_paid':'date_paid is not null', 'chosen_date':chosen_date)
sum_qs = sum_qs.extra(select = fields)

【讨论】:

【参考方案3】:

这行得通吗?:

from django.db import connection, transaction
cursor = connection.cursor()

sql = """
    SELECT 
        %s AS year, 
        %s AS month,
        date_paid IS NOT NULL as is_paid
    FROM (
        SELECT
            (CASE WHEN date_due IS NULL THEN date_due ELSE date END) AS chosen_date, *
        FROM
            invoice_invoice
    ) as t1;
    """ % (connection.ops.date_extract_sql('year', 'chosen_date'),
           connection.ops.date_extract_sql('month', 'chosen_date'))

# Data retrieval operation - no commit required
cursor.execute(sql)
rows = cursor.fetchall()

我认为它非常节省 CASE WHEN 和 IS NOT NULL 都与 db 无关,至少我认为它们是,因为它们以原始格式用于 django 测试..

【讨论】:

【参考方案4】:

您可以在模型定义中添加一个属性,然后执行:

@property
def chosen_date(self):
    return self.due_date if self.due_date else self.date

这假设您始终可以回退到日期。如果您愿意,可以在 due_date 捕获 DoesNotExist 异常,然后检查第二个异常。

您可以像访问其他任何内容一样访问该属性。

至于其他查询,我不会使用 SQL 从日期中提取 y/m/d,只需使用

model_instance.chosen_date.year

chosen_date 应该是一个 python 日期对象(如果您在 ORM 中使用 DateField 并且此字段在模型中)

【讨论】:

实际上我这样做的原因是因为稍后我会根据年份和月份进行总和聚合。为了做到这一点,我需要使用我想要分组的值调用values...而且我不能在values 中使用像__year 这样的字段查找。 另一件事是我用于chosen_date 的内容取决于上下文:有时我使用due_datedate 后备,有时只使用date【参考方案5】:

只需使用原始 sql。 raw() 管理器方法可用于执行返回模型实例的原始 SQL 查询。

https://docs.djangoproject.com/en/1.5/topics/db/sql/#performing-raw-sql-queries

【讨论】:

以上是关于如何在 django 查询集中获取表的计算元素?的主要内容,如果未能解决你的问题,请参考以下文章

在 django 的查询集中获取对象的计数

如何检查一个元素是不是存在于 Django 查询集中?

django模板变量从查询集中构建表的字段数

Django - 如何从模板中的查询集中获取最后一项

如何在查询集中动态设置 Django 过滤器

Django:如何从与用户相关的子查询集中获取不同的父列表?