9

我正在尝试使用 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分支)。仍然不起作用)

4

5 回答 5

7

简短回答: 如果您使用创建别名(或计算)列,extra(select=...) 则不能在后续调用中使用别名列filter()。此外,正如您所发现的,您不能在以后调用 extra(select=...)or时使用别名列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()(所以我有兴趣看到您正在谈论的类似错误,根据我的经验,它相当强大)。这是因为当您尝试过滤由 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”是行不通的。


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

于 2013-08-21T18:20:04.797 回答
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 世界中,这种类型的新字段计算通常是通过使用子查询或公用表表达式来完成的。我更喜欢 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 中,您可以使用原始查询,例如:

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)
于 2013-08-16T08:48:29.343 回答
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

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

于 2013-08-20T16:02:23.653 回答
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 测试。

于 2013-08-19T08:37:41.993 回答
1

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

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

于 2013-08-20T22:27:46.507 回答