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

时间:2017-10-30 15:04:21

标签: sql django-queryset django-orm

示例模型:

class User(models.Model):
    pass


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

我想使用UserStatusChanges字段对end_date个查询集进行注释,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.

我不确定,也许有更好的解决方案。但是这个特殊的解决方案解决了我的问题。

1 个答案:

答案 0 :(得分:2)

您可以在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(),其默认值等于当前时间戳。