2013-08-16 53 views
9

我试图使用Django的查询集API效仿以下查询:我怎样才能在Django的queryset表的计算元素?

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'}) 

但是这个问题我有m是当我运行第二个查询,得到如何的chosen_date列不存在错误。我有后来当试图使用计算列(如从内部annotate()电话)类似的错误,但没有发现有关列计算如何从“基地”的人不同的文档中的任何东西。有没有人有这方面的见解?

(编辑Python代码,因为以前的版本有明显的逻辑漏洞(忘了else分支)仍不能正常工作)

+0

我会保持简单并使用原始查询。这是他们在那里。 –

回答

6

简短的回答: ,如果您使用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()(因此,我有兴趣查看您正在讨论的类似错误,这在我的经验中相当强劲)。这是因为,当您尝试过滤通过注释创建的别名时,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” 是行不通的。


我希望这是有帮助的。如果有任何我解释不好的让我知道,我会看看我是否可以改进它。

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

在SQL世界上这种类型计算的新领域通常是由uing子查询或common table expression完成。我更喜欢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像:

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

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

,你将有具有一些附加属性的发票对象。

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) 
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是相当分贝无关,至少我认为是这样,因为它们是在原始格式的Django测试使用..

1

你可以一个属性添加到您的模型定义,然后做:

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

这是假设你总是可以回退到date.If你喜欢,你可以赶上一个DUE_DATE异常DoesNotExist,然后检查第二个。

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

至于其他查询,我不会用SQL提取之日起,Y/M/d,只需使用

model_instance.chosen_date.year 

chosen_date应该是一个python日期对象(如果你使用ORM中的DateField和此字段位于模型中)

+0

实际上我这样做的原因是因为后来我根据年份和月份进行了总和聚合。为了做到这一点,我需要用我想要分组的值来调用'values' ...,并且我不能在'values'中使用像'__year'这样的字段查找。 – rtpg

+0

另一件事是,我用'selected_date'取决于上下文:有时我使用'due_date'与'date'后备,有时只是'日期'。 – rtpg