用 Django ORM 中的前一个对象注释查询集

Posted

技术标签:

【中文标题】用 Django ORM 中的前一个对象注释查询集【英文标题】:Annotate queryset with previous object in Django ORM 【发布时间】:2018-04-11 14:57:41 【问题描述】:

示例模型:

class User(models.Model):
    pass


class UserStatusChange(models.Model):
    user = models.ForeignKey(User, related_name='status_changes')
    status = models.CharField()
    start_date = models.DateField()

我想用end_date 字段注释UserStatusChanges 查询集,并且end_date 应该等于同一用户下一次状态更改的start_date

最终,我希望能够做到这一点:

qs = UserStatusChange.ojects.annotate(end_date=???)
qs = qs.filter(start_date__lte=some_date, end_date__gte=another_date)

从逻辑上讲,注释应该是这样的:

qs.annotate(
    end_date=qs.filter(
        user=OuterRef('user'),
        start_date__gt=OuterRef('start_date')
    ).order_by('start_date').first().start_date)

但如果可能的话,它应该是一个数据库查询。

解决方案:

subquery = UserStatusChange.objects.filter(user=OuterRef('user'),
                                           start_date__gt=OuterRef('start_date')).order_by('start_date')
UserStatusChange.objects.annotate(end_date=Subquery(subquery.values('start_date')[:1]))

感谢@hynekcer 的回答,这很有效。但是aggregate 我得到了错误:

ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.

UPD:在 Django 2.0+ 中可以使用Lead Window function 解决。 在 SQL 中会是这样的:

select 
     user_id, status_id, start_date,
     LEAD(start_date, 1) over (partition by user_id order by start_date)
from user_status_change;

【问题讨论】:

你想得到什么? user=F('user')? It will be compilad to app_userstatusshange.user=app_userstatusshange.user 的意思可能不是你所期望的。 @hynekcer 我想为同一用户选择状态更改。 @hynekcer 每个UserStatusChanges 都与某个特定用户相关。因此,当我想用​​end_date 注释UserStatusChanges 时,end_date 应该与某些用户之前的更改有关。所以,最后,我必须这样做:qs = UserStatusChanges.ojects.annotate(end_date=???)qs = qs.filter(start_date__lte=some_date, end_date__gte=another_date)User.objects.prefetch_related(Prefetch('status_changes', queryset=qs)) 这意味着我需要获取具有按日期范围过滤的他们自己的更改事件的用户列表。 end_date < start_date 可以吗?我按照你写的方式实现了它,但我对问题的感觉会重命名,例如到“previous_date”或将 end_date 定义为下一个状态的日期。 如果您想将end_date 注释为“与某些用户之前的更改相关”,那么它小于start_date。我希望您能明确问题的口头部分,我可以投票认为它对其他人有用并删除我的 cmets。 【参考方案1】:

您可以在 Django 1.11 中将 Subquery() 与 OuterRef() 一起使用。

from django.db.models import Min, OuterRef, Subquery
from django.db.models.functions import Coalesce

default_end = now()  # or the end of the recorded history
qs = (
    UserStatusChanges.objects
    .annotate(
        end_date=Coalesce(
            Subquery(
                UserStatusChanges.objects
                .filter(
                    user=OuterRef('user'),
                    start_date__gt=OuterRef('start_date')
                )
                .order_by()
                .aggregate(Min('start_date'))
            ),
            default_end
        )
    )
)
qs = qs.order_by('user', 'start_date')
# an optional filter
qs = qs.filter(start_date__lte=some_date, end_date__gte=another_date, user__in=[...])

在执行时编译为一个查询,例如当通过 prefetch_related 与用户过滤器结合使用时。如果您还想为最后一项设置一个有意义的end_date,那么您可以使用默认值等于当前时间戳Coalesce()

【讨论】:

以上是关于用 Django ORM 中的前一个对象注释查询集的主要内容,如果未能解决你的问题,请参考以下文章

用日期时间注释 Django 查询集

使用 Django 的 ORM 和 Django Rest Framework 序列化嵌套关系的查询集的正确方法?

查询集排序:为 django ORM 查询指定列排序规则

Django ORM数据库查询操作

Django ORM数据库查询操作

如何使用 StringAgg 或 ArrayAgg 连接多个子行中的一列来注释 django 查询集?