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

时间:2013-08-16 08:41:39

标签: django postgresql django-queryset

我正在尝试使用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是可选的,我有时必须使用date作为后备,并创建一个计算列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分支)。仍然无效)

5 个答案:

答案 0 :(得分:7)

简答: 如果使用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一次,然后再次使用它,并在每次需要时计算它。


您在问题中提到了注释/聚合。您可以对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”将无效。


我希望这有帮助。如果有什么我已经解释得很糟糕让我知道,我会看看我是否可以改进它。

答案 1 :(得分:3)

这里有一些解决方法

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)

答案 2 :(得分:1)

这会有用吗?:

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测试..

答案 3 :(得分:1)

您可以在模型定义中添加属性,然后执行以下操作:

@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并且该字段在模型中)

答案 4 :(得分:1)

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

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