Django中与时区相关的问题(USE_TZ / TIME_ZONE /auto_now_add /auto_now /timezone)

文章讲述了在Django项目中,关于datetime字段的时区处理问题,包括默认UTC时间与本地时间的区别,以及在model定义、ORM操作和时间比较中的最佳实践。特别强调了使用aware时间的重要性,避免因时区转换导致的时间偏差和潜在逻辑漏洞。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

结论:如果没有时区需要的话,建议【禁用时区支持】+【确定:服务器时区设置 = 你要的时区】

因为单 “数据库中datetime字段以UTC+0时间保存” 这一条就有点难受了,很多小应用我都是直接看数据库呢!

# linux查看时区的命令:
sudo timedatectl

# linux更改时区的命令:
sudo timedatectl set-timezone Asia/Shanghai

# https://www.bilibili.com/video/BV1MwUUY2Ex3/

经历:

项目中需要创建一条交易记录,交易记录中存在一个【创建时间】的字段,类型为datetime。

数据库中该字段数据类型为datetime 默认值为 CURRENT_TIMESTAMP:

model模型中定义 createdate = models.DateTimeField(auto_now_add=True)

插入语句使用record.save()

我发现插入的数据的createdate时间不是服务器时间(服务器时间为UTC+8),而是比晚8个小时(UTC+0),例如我插入一条数据,服务器时间为'2024-03-07 11:20:51' 但数据库得到时间为'2024-03-07 03:20:51'

-----------

另外当我把settings.py中 USE_TZ = True 屏蔽后,我的问题解决了,但我不知道会否产生副作用。先记录一下,后续再慢慢研究。 

貌似只对timezone类相关的有影响,datetime类不受影响,毕竟 datetime 是python自带,timezone是django带来的,USE_TZ = True 是django的settings内容,只对django产生影响

结论:Django ORM模型DateTimeField类型的字段在存储和显示时会受TIME_ZONE设置影响。

----------------

后续研究来了:

1、Django settings 的 默认值:USE_TZ = True 以及  TIME_ZONE = "UTC" (即UTC+0)

2、当在Django ORM模型中使用 auto_now_add=True 或  auto_now=True 时,一定要主要TIME_ZONE问题(如果没有时区需要的话,建议先禁用时区支持再使用auto_now_add=True)

3、【时区支持】要配合timezone使用才有效。

4、开启了时区支持的话,尽可能全部用 timezone,慎用 datetime.now() 或 time.time(),否则很容易搞混。

5、如果没有时区需要的话,建议禁用时区支持。(USE_TZ = False的情况下,TIME_ZONE 无论取任何值都不会产生效果,此时timezone.now() 和 datetime.datetime.now() 是等效的)

6、【USE_TZ = True 】的情况下,ORM模型会将时间转成[UTC+0]后写入,读取时也会将获得的值当成 UTC+0来处理。原生SQL则不进行任何转换,直接硬写入裸读出。

------------

另外:Django ORM 模型中 DateTimeField 字段的 auto_now=Trueauto_now_add=Truedefault=timezone.now 三者的区别

特性auto_now=Trueauto_now_add=True

default=timezone.now

用途记录对象最后一次修改的时间记录对象创建的时间

设置字段的默认值为当前时间

更新时机每次调用 save() 方法保存对象时自动更新对象第一次被创建时自动设置,之后不再更新

仅在创建对象时,如果未提供值,则设置默认值

是否可手动修改不可手动修改,Django 会强制更新不可手动修改,Django 会强制锁定

可以手动修改,如果提供了值,则使用提供的值

常用场景记录更新时间戳,例如 updated_at 字段记录创建时间戳,例如 created_at 字段

设置默认创建时间,但允许在创建时覆盖,或者允许后续更新

时间默认值推荐使用auto_now_add=True, 一般不要使用default=datetime.now / default=timezone.now,千万不要使用 default=datetime.now()  / default=timezone.now()

在禁用时区支持的情况下 Django ORM 模型的datetime字段中 default=datetime.now 跟 default=datetime.now() 的区别

默认值default=datetime.nowdefault=datetime.now()
类型函数对象datetime 对象
行为每次创建模型实例时,调用 datetime.now 获取当前时间。

模型类定义时,立即调用 datetime.now() 获取一个时间,并且以后都固定在这一刻。

结果每次创建实例,默认时间都是创建的时刻。模型定义时计算的静态时间,所有实例都使用相同的值
常用场景动态设置默认创建时间通常是错误用法,很难找到合理的使用场景。

如果在 Django ORM 模型的 DateTimeField 中使用 default=datetime.now(),每条记录的该字段得到的都是models.py首次被加载时的时间(大约等于执行 python manage.py runserver 的时间)

default=timezone.now 和 default=timezone.now() 同理

------------------------------------------------------

其他:

时间读取/显示方面:timezone设置,仅在模板渲染中会自动转换时区,在 Python 代码中(视图函数代码),需要手动通过 timezone.localtime() 转换时区。

record= MyTestTableModel.objects.get(id=1)
create_time = timezone.localtime(record.createdatetime)

print(f'数据库存储的是UTC+0:{record.createdatetime}, 我心目中想看到的是UTC+8:{create_time}')
# python代码:
record= MyTestTableModel.objects.get(id=1)

return render(request, 'template.html', {'record': record})


# HTML模板 - 直接显示转换后的结果(数据库存的是utc+0,这里显示的是utc+8)

<p>创建时间: {{ record.createdatetime }}</p>

======================

启用【时区支持】USE_TZ=True时,如果应用中 部分模块使用 Django ORM 读取数据,部分模块直接使用数据库连接 + 游标 + SQL 语句,那么时间显示很容易搞混

1、如果使用Django ORM模型来读写数据,那么存储写入的都是UTC+0的时间,但显示会转换成TIME_ZONE时区时间。

2、直接conn+cur+sql 原生SQL 的方式读取得到的是UTC+0的时间。

 =====================

以下为不改变settings.py(即TIME_ZONE = 'UTC', USE_TZ = True)情况下,得到的结果

from django.utils import timezone
from datetime import datetime

# 模型
class ActionLog(models.Model):
    ip = models.CharField(max_length=15)
    createdate = models.DateTimeField()
    # createdate = models.DateTimeField(default=timezone.now)
    # createdate = models.DateTimeField(default=datetime.now)
    # createdate = models.DateTimeField(auto_now_add=True)



# ORM操作,对应的插入语句如下:
action_log = ActionLog(ip=request.META['REMOTE_ADDR'])
action_log.save()


#服务器时区为(UTC+8)时,

createdate = models.DateTimeField() #得到createdate值为null
createdate = models.DateTimeField(default=timezone.now) #得到createdate值比服务器时间晚8小时
createdate = models.DateTimeField(default=datetime.now) #得到createdate值跟服务器时间相同
createdate = models.DateTimeField(auto_now_add=True) # 等效于default=timezone.now


=========

 另一参考:django---时区问题(USE_TZ) - 飞行的猪哼哼 - 博客园

在开发国际化网站的时候,难免会与时区打交道,通用CMS更是如此,毕竟其潜在用户可能是来自于全球各地的。Django在时区这个问题上下了不少功夫,但是很多资深的开发者都有可能尚未完全屡清楚Django中各种时间的实际意义和使用方法,导致写出错误的代码;作为安全研究人员,时区问题也可能和一些安全问题挂钩,比如优惠券的过期时间、订单的下单与取消时间等,如果没有考虑时区问题,有可能将导致一些逻辑漏洞。

本文就从多个常用模块开始,了解一下Django中的时区究竟是怎么回事,以及在时间的比较中可能出现的一些逻辑错误。

从“两种时间”说起

我们都知道,在Python中表示“时间”的对象是datetime.datetime

其实在Python中,这个对象被分成了两个类型

  • aware datetime
  • naive datetime

他们的区别是:如果datetime对象的tzinfo属性有设置时区值,则这个对象是一个aware datime;否则它是一个naive datetime。

举个例子,我们平时在编写Python脚本的时候,使用下面这行代码获取当前时间:

from datetime import datetime

t = datetime.now()

此时,t是一个naive datetime,因为我们没有给他设置时区:

image-20201011010557126.png

naive的中文意思大家应该都很熟悉,这里的大概意思就是“simple”,这是一个很简单、原始的时间对象。实际上就是指,计算机不知道这个时间,他的时区究竟是什么,它可能代表着北京时间,也可能是UTC时间,因为我们没有指定时区,我们无法“假设”其是计算机系统所在的时区,也无法“假设”其是UTC时区。也就是说,计算机拿到了一个naive datetime,是无法准确地定位到某一个时间点的,也无法直接转换成一个unix时间戳。

那么相对的,aware datetime就是计算机能准确知道其时区的时间对象,他是一个准确的时间点,就落在时间轴上的某个地方,不管从哪个时区看,这个点都是绝对固定的。所以,我们可以将一个aware datetime转换成unix时间戳。

有的同学可能比较好奇,你说naive datetime无法转换成时间戳,那么为什么这个对象有一个timestamp()方法呢:

image-20201011012655581.png

原因我们查文档可以得出结论,如果对象是naive datetime,则会以当前系统本地时区为准。

Django的时区配置

回到Django。由于Django是一个国际化框架,时区相关处理自然是其必不可少的组成部分。Django的配置项中,有下面两个选项与时区相关:

  • USE_TZ
  • TIME_ZONE

USE_TZ用来指定整个项目是否使用时区TIME_ZONE是默认时区的值。

如果USE_TZ的值设置为False,那么Django项目中所有时间都使用naive datetime(除非有明确指定时区的情况)。也就是说,网站内存储和使用的时间全部是TIME_ZONE的值所指定的时区。

这样做有一些弊端:

  • 数据库中保存的是naive datetime,导致在跨区域迁移数据的时候,可能无法准确定位到某个时间点
  • 国际化企业可能面向不同国家有不同的网站,但后台数据库相同,此时究竟使用哪个时区保存和展示时间,将引起混乱
  • 即使是同一个网站的用户,他们可能来自于全球各地,查看到的时间却是统一的服务器时间,对于高交互式的应用十分不友好
  • 即使网站面向的用户仅来自于某一个地区,也会涉及到“夏时令”(Daylight Saving Time)相关的问题,每年可能将会导致两次时间误差

默认情况下,用django-admin生成的项目,其设置中USE_TZ等于True,这也是Django官方建议的配置。此时,在网站内部存储与使用的是UTC时间,而与用户交互时使用TIME_ZONE或手工的时区。

我们后文中也以Django的默认配置USE_TZ=True为前提条件,否则也没有讨论的必要了。

Django的时间函数

Django的包django.utils.timezone中有下面几个常用的时间相关函数:

  • now(),返回当前的UTC时间
  • localtime(),返回当前的本地时间(默认是TIME_ZONE配置指定的时区时间)
  • is_aware(),传入的时间是否是aware datetime
  • is_naive(),传入的时间是否是naive datetime
  • make_aware(),将naive时间转换成aware时间
  • make_naive(),将aware时间转换成naive时间

因为开启了USE_TZ,Django内部操作时间时都应该使用aware时间,否则会出现异常。所以,我们在获取当前时间的时候,一定要使用Django自带的now()localtime()函数,而不能使用Python的datetime.datetime.now()函数。

数据库存储的时间

我们在使用ORM的DatetimeField时,常常会有这样的疑虑:我们究竟应该给DatetimeField传入哪个时区的时间呢?

可以做个试验,编写下面这个model:

class Archive(models.Model):
    title = models.CharField('title', max_length=256)

    now_time = models.DateTimeField(default=timezone.now)
    local_time = models.DateTimeField(default=timezone.localtime)

这个model有三个属性,title是他的名字,now_time和local_time是两个时间,他们的默认值分别是timezone.now和timezone.localtime。

也就是说,默认情况下,now_time字段传入的是UTC时区的当前时间,local_time字段传入的是本地时区的当前时间,我这里是Asia/Shanghai

然后,我们创建一个Archive对象:

image-20201011024130489.png

可以发现,不管我们使用a.now_time还是a.local_time,读取到的datetime对象的tzinfo都是UTC。

这也印证了Django文档中说到的,不管传入的时间对象时区是什么,其内部存储的时间均为UTC时区。但是,值得注意的是,如果我们传入了一个不带时区的naive datetime,将会出现一个警告,并使用默认时区填充其tzinfo:

image-20201011024854993.png

模板中展示的时间

对于网站的用户来说,他们想看到的时间显然不是UTC时间,而是某一个具体时区的时间。比如,我的网站几乎全部是中国用户,那么展示时使用的时区应该是Asia/Shanghai

这一部分的转换,Django放在的模板引擎中。

Django在渲染模板变量时,将会遇到两种与时间有关的情况:

<p>origin value: {{ object.now_time }}</p>
<p>date filter: {{ object.now_time | date:'Y-m-d H:i:s' }}</p>

前者是直接将时间渲染到页面中,后者是通过date这样的模板filter处理后渲染在页面中。这两种情况在内部处理方式略有不同此处不细表,总体而言,任意模板中变量的渲染,都会被转换时区。

那么,脱离模板引擎,我们会得到怎样的结果呢?

在流行的前后端分离架构中,后端服务器通常只提供JSON格式的接口给前端,那么,我们编写下面这样一个view,看看返回值是什么:

from django.shortcuts import get_object_or_404
from django.http.response import JsonResponse
from django.utils import timezone

from . import models


def json(request):
    object = get_object_or_404(models.Archive, pk=1)
    data = dict(
        id=object.pk,
        now_time=object.now_time,
        local_time=timezone.localtime(object.local_time)
    )
    return JsonResponse(data=data)

返回对象的now_time,我直接将object.now_time返回;返回对象的local_time,我将数据库值转换成本地时间timezone.localtime(object.local_time)返回。

我前文说过,这两个值在数据库中的值是完全相等的,不过在json返回中,now_time是UTC时间,而local_time是北京时间:

image-20201011031507193.png

也就是说,在前后端分离的网站中,如果直接使用Model的字段,那么前端需要负责进行时区的转换,否则将会出现时间的偏差。

时间的校验和比较

在一些业务场景下,我们可能会涉及到时间的校验和比较,如:

  • 付费服务、商品、用户的有效期检查
  • 活动的开始与结束时间检查
  • 订单、商品的收货、取消时间检查

我们就以付费用户为例:用户购买了30天的VIP会员,我们需要给用户表中设置一个过期时间,比如下面这个model。

from django.db import models
from django.utils import timezone

class Account(models.Model):
    username = models.CharField(max_length=256)
    password = models.CharField(max_length=64)

    created_time = models.DateTimeField(default=timezone.now)
    expired_time = models.DateTimeField()

如果某个用户某一个时刻对网站进行访问,我们如何判断他是否具有VIP权限呢?

通常情况下我们有两种常见的判断方法。一是,用户访问时,直接从model中取出这个对象,然后和now()进行比较:

image-20201011033452219.png

这种情况下,当前时间不管是now()还是localtime()都不影响比较的结果,因为两个datetime对象在比较时会考虑时差。

另一种情况是,通过ORM的queryset进行比较,等于在数据库层面进行操作:

if models.Account.objects.filter(expired_time__gt=timezone.now()).exists():
    # doing sth

image-20201011034352025.png

Django也帮我们考虑过这种情况,即使此时我们使用本地时间timezone.localtime()进行查询,系统也会将其转换成UTC时间传入SQL语句:

image-20201011034633005.png

但是,如果我们使用到了和日期、时间有关的lookups,将产生相反的结果。

怎么理解这个问题呢,我们还是来举个例子。比如,网站以用户注册当天的日子作为“会员日”(比如1月2日注册的会员,以后每月的2日都是他的会员日),会员日这一天会给这个用户赠送优惠券。

那么,发送优惠券时,我们如何筛选网站内会员日为今日的用户呢?

下面这个filter是否正确?

models.Account.objects.filter(created_time__day=timezone.now().day).all()

答案是否定的,我们应该使用timezone.localtime()表示今天,而非timezone.now()

models.Account.objects.filter(created_time__day=timezone.localtime().day).all()

这是为什么呢?你不是说数据库中存储的都是UTC时间吗,为何会使用到timezone.localtime()

原因是,Django在使用日期、时间有关的lookups时,会在数据库层面对时间进行时区的转换再进行比较,所以我们需要使用本地时间而不是UTC时间。

可以看看原始的SQL语句:

image-20201011041652087.png

可见,SQL语句中使用了django_datetime_extract('day', "sample_account"."created_time", 'Asia/Shanghai', 'UTC')将UTC时间转换成了北京时间,因此后面比较的时候,也应该使用北京时间。

这一点需要格外注意。时间比较的不谨慎,说小点是一个Bug,说大点就是漏洞,毕竟很多涉及到时间比较的情景,都是非常需要严谨的。

所以,我们总结一下:

  • 任何比较都使用aware时间,不能使用naive时间
  • 时间属性直接比较时,使用任何aware时间均可(会被自动转换成UTC)
  • queryset查询,不涉及__day__date__year等时间lookups时,使用任何aware时间均可(会被自动转换成UTC)
  • queryset查询,涉及到时间lookups时,使用本地时间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值