Django中的F表达式在datetime对象上的应用

12

我的模型是:

class Test():
   date1 = models.DateTimeField()
   date2 = models.DateTimeField()

使用以下查询语句,我可以找到 date2 大于 date1 的对象:

```sql SELECT * FROM objects WHERE date2 > date1; ```
Test.objects.filter(date2__gt=F('date1'))

我想找到所有date2date1晚一年的物品。
基于date1date2之间的差异,我该如何查找物品?


2
没有安装方便尝试,但是 Test.objects.annotate(next_year=F('date1') + timedelta(days=365)).filter(next_year__gt=F('date2')) 怎么样?或者在注释表达式中对两个日期进行减法运算。 - Anonymous
2个回答

12

通用解决方案:

您可以使用annotate注释日期差异,然后将其与timedelta(days=365)进行比较(非常接近@Anonymous在评论中建议的方法):

Test.objects.annotate(
    duration=F('date2') - F('date1')
).filter(duration__gt=timedelta(days=365))


PostgreSQL特定解决方案:

如果您正在使用PostgreSQL,则有另一种选项,源自这个答案

from django.db.models import F, Func

Test.objects.annotate(
    duration = Func(F('date2'), F('date1'), function='age')
).filter(duration__gt=timedelta(days=365))

如果我使用Test.objects.annotate(duration=F('date2') - F('date1')),我会在查询集中得到一些小数值。这些值是什么? - Mahabubur Rahaman Melon
@MahabuburRahamanMelon 你有没有使用MySQL呢?https://dev59.com/SGs05IYBdhLWcg3wDtpn?answertab=votes#tab-top - John Moutafis
是的,我正在使用MySQL。这对于这个解决方案有问题吗? - Mahabubur Rahaman Melon
尝试使用annotate(duration=F('end_time') - F('start_time')).filter(duration__gt=timedelta(0,299)),但是遇到了一个错误:TypeError: 期望为字符串或类似字节的对象 - sytech
1
@sytech 看起来你的代码其他地方出现了问题,与这个线程无关。也许是迁移方面的问题:https://dev59.com/jVkS5IYBdhLWcg3wHTJy - John Moutafis
显示剩余2条评论

5
你可以同时使用 __date查询条件TruncDate函数

from django.db.models import DateField, ExpressionWrapper, F
from django.db.models.functions import TruncDate
Test.obejcts.filter(
    date2__date__gt=ExpressionWrapper(
        TruncDate(F('date1')) + datetime.timedelta(days=365),
        output_field=DateField(),
    ),
)


如果您真正需要的是像date1 = 2019-05-14date2 > 2020-05-14这样的内容。那么这种方法并不总是正确的,因为闰年有366天。可以使用TruncExtract函数一起解决此问题。还有其他不同的方法可供选择...例如:
from django.db.models import DateField, ExpressionWrapper, F
from django.db.models.functions import TruncDate, ExtractDay

date_field = DateField()

YEAR = timedelta(days=365)
LEAP_YEAR = timedelta(days=366)

shifted_date1 = ExpressionWrapper(
    TruncDate(F('date1')) + YEAR,
    output_field=date_field,
)

leap_shifted_date1 = ExpressionWrapper(
    TruncDate(F('date1')) + LEAP_YEAR,
    output_field=date_field,
)


qs = Test.objects.filter(
    (
        # It's ok to add 365 days if...
        Q(date2__date__gt=shifted_date1)
        &
        (
            # If day of month after 365 days is the same...
            Q(date1__day=ExtractDay(shifted_date1))
            |
            # Or it's 29-th of February
            Q(
                date1__month=2,
                date1__day=29,
            )
        )
    )
    |
    Q(
        # Use 366 days for other cases
        date2__date__gt=leap_shifted_date1,
    )
)

顺便说一下,如果你的USE_TZ = True并且在指定时区时执行查询(例如在执行查询集之前使用timezone.activate(...)),那么在添加timedelta之前执行TruncDate非常重要,因为在一些国家里,“夏令时”的切换日期每年都不同,因此执行TruncDate(F('date1')+timedelta(...))可能会导致错误结果。例如:

  • 某些国家在2019年3月31日切换到DST时间,在2020年切换到2020年3月29日。
  • 2019年3月30日23:30当地时间尚未使用夏令时。
  • 将366天(因为明年是闰年)加到它上面,将得到2020-03-30 23:30 "non-DST",因此在"normalization"后,这个日期时间将变成 2020-03-31 00:30 "DST"
  • 在添加时间增量之前使用TruncDate解决了这个问题,因为TruncDate将值强制转换为日期

额外信息:一些国家在固定日期(例��每年的2月1日)切换到夏令时,而其他国家可能会“在三月的最后一个星期日”切换夏令时,这可能是每年不同的日期。

import pytz
import datetime

kyiv.localize(datetime.datetime(2011, 3, 28, 0, 1)) - kyiv.localize(datetime.datetime(2010, 3, 28, 0, 1))
# `datetime.timedelta(364, 82800)` is less than 365 days

附言:在“闰秒年”(2016-12-31 23:59:60.999)的最后几秒钟,TruncDate/timedelta-shift 的排序可能也受到影响,但是“幸运”的是,大多数数据库不支持闰秒,Python的datetime.datetime也缺少此功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接