【问题标题】:Django Annotated Query to Count all entities used in a Reverse RelationshipDjango Annotated Query 以计算反向关系中使用的所有实体
【发布时间】:2020-03-11 01:07:35
【问题描述】:

这个问题是这个 SO 问题的后续问题:Django Annotated Query to Count Only Latest from Reverse Relationship

鉴于这些模型:

class Candidate(BaseModel):
    name = models.CharField(max_length=128)

class Status(BaseModel):
    name = models.CharField(max_length=128)

class StatusChange(BaseModel):
    candidate = models.ForeignKey("Candidate", related_name="status_changes")
    status = models.ForeignKey("Status", related_name="status_changes")
    created_at = models.DateTimeField(auto_now_add=True, blank=True)

由这些表格表示:

candidates
+----+--------------+
| id | name         |
+----+--------------+
|  1 | Beth         |
|  2 | Mark         |
|  3 | Mike         |
|  4 | Ryan         |
+----+--------------+

status
+----+--------------+
| id | name         |
+----+--------------+
|  1 | Review       |
|  2 | Accepted     |
|  3 | Rejected     |
+----+--------------+

status_change
+----+--------------+-----------+------------+
| id | candidate_id | status_id | created_at |
+----+--------------+-----------+------------+
|  1 | 1            | 1         | 03-01-2019 |
|  2 | 1            | 2         | 05-01-2019 |
|  4 | 2            | 1         | 01-01-2019 |
|  5 | 3            | 1         | 01-01-2019 |
|  6 | 4            | 3         | 01-01-2019 |
+----+--------------+-----------+------------+

我想获取每种状态类型的计数,但只包括每个候选人的最后状态:

last_status_count
+-----------+-------------+--------+
| status_id | status_name | count  |
+-----------+-------------+--------+
| 1         | Review      | 2      | 
| 2         | Accepted    | 1      | 
| 3         | Rejected    | 1      |
+-----------+-------------+--------+

我能够通过this answer 实现这一目标:

from django.db.models import Count, F, Max

Status.objects.filter(
    status_changes__in=StatusChange.objects.annotate(
        last=Max('candidate__status_changes__created_at')
    ).filter(
        created_at=F('last')
    )
).annotate(
    nlast=Count('status_changes')
)

>>> [(q.name, q.nlast) for q in qs]
[('Review', 2), ('Accepted', 1), ('Rejected', 1)]

但问题是,如果任何状态更改都没有引用状态,则结果中会忽略它。相反,我想把它算为零。 例如,如果状态是

+----+--------------+
| id | name         |
+----+--------------+
|  1 | Review       |
|  2 | Accepted     |
|  3 | Rejected     |
|  4 | Banned       |
+----+--------------+

我会得到:

+-----------+-------------+--------+
| status_id | status_name | count  |
+-----------+-------------+--------+
| 1         | Review      | 2      | 
| 2         | Accepted    | 1      | 
| 3         | Rejected    | 1      |
| 4         | Banned      | 0      |
+-----------+-------------+--------+

>>> [(q.name, q.nlast) for q in qs]
[('Review', 2), ('Accepted', 1), ('Rejected', 1), ('Accepted 0)]

我尝试了什么

我通过在 SQL 中进行外连接解决了这个问题,但我不确定如何在 Djano 中实现。 我尝试创建一个所有计数都注释为零的查询集并将其合并,但它不起作用:

last_status_changes = Status.objects.filter(
    status_changes__in=StatusChange.objects.annotate(
        last=Max('candidate__status_changes__created_at')
    ).filter(
        created_at=F('last')
    )
).annotate(
    nlast=Count('status_changes')
)
zero_query = (
    Status.objects.all()
    .annotate(nlast=Value(0, output_field=IntegerField()))
    .exclude(pk__in=last_status_changes.values("id"))
)

>>> qs = last_status_changes | zero_query
>>> [(q.name, q.nlast) for q in qs]
[('Review', 3), ('Accepted', 1), ('Rejected', 1)]
# this would double count "Review" and include not only last but others

任何帮助表示赞赏 谢谢

更新 1

我能够通过使用右连接的原始查询来解决这个问题,但如果使用 ORM 来解决这个问题会很棒

# Untested as I am using different model names in reality
SQL = """SELECT
        Min(status.id) as id
        , COUNT(latest_status_change.candidate_id) as status_count
    FROM
        (
        SELECT
            candidate_id,
            Max(created_at) AS latest_date
        FROM
            api_status_change
        GROUP BY candidate_id
        )
    AS latest_status_change
    INNER JOIN api_candidates ON (latest_status_change.candidate_id = api_candidates.id)
    INNER JOIN api_status_change ON 
        (
            latest_status_change.candidate_id = api_candidates.id 
            AND 
            latest_status_change.latest_date = api_status_change.created_at
        )
    RIGHT JOIN api_status AS status  ON (api_status_change.status_id = `status`.id)
    GROUP BY status.name
    ;
"""
qs = Status.objects.raw(SQL)
>>> [(q.name, q.nlast) for q in qs]
[('Review', 2), ('Accepted', 1), ('Rejected', 1), ('Accepted 0)]

【问题讨论】:

  • 你使用的是哪个 Django 版本?

标签: python django django-orm


【解决方案1】:

这里唯一的一个问题是您正在通过现有状态更改过滤您的State 查询集并期望完全相反的结果。在您的情况下,解决方案是摆脱过时的过滤

last_status_changes = Status.objects.annotate(
    nlast=Count('status_changes')
).order_by(
    '-nlast'
)

另一种情况是,如果您想真正过滤您的更改(例如按日期)

changed_status_ids = Status.objects.filter(
    status_changes__created_at__gte='2020-03-03'
).values_list(
    'id',
    flat=True
)

Status.objects.annotate(
    c=Count('status_changes')
).annotate(
    cnt=Case(
        When(
            id__in=changed_status_ids,
            then=F('c')
        ),
        output_field=models.IntegerField(),
        default=0
    )
).values(
    'cnt',
    'name'
).order_by(
    '-cnt'
)

【讨论】:

  • 感谢您的回复。我能够按原样使用它(给我计数,但不包括最新的),但它使我朝着正确的方向前进,我能够使用 Case When 解决它
【解决方案2】:

我用下面的查询集解决了这个问题:

qs_last_status_changes = StatusChanges.objects
    .annotate(
        _last_change=models.Max("candidate__status_changes__create_at")
    ).filter(created_at=models.F("_last_change")

qs_status = Status.objects\
    .annotate(count=models.Sum(
        models.Case(
            models.When(
                status_changes__in=qs_last_status_changes, 
                then=models.Value(1)
            ),
            output_field=models.IntegerField(),
            default=0,
        )
    )
)
>>> [(k.name, k.count) for k in qs_status]
[('Review', 2), ('Accepted', 1), ('Rejected', 1), ('Accepted 0)]

感谢 Andrey Nelubin 的建议

【讨论】:

    猜你喜欢
    • 2020-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-27
    • 1970-01-01
    • 1970-01-01
    • 2018-07-05
    相关资源
    最近更新 更多