Python操做MongoDB的时区问题

最近使用Python写一段缓存用户登录信息的一段代码,使用MongoDB保存token及用户信息,同时设置过时时间为当前时间1小时后,代码以下:html

db['user'].insert_one(
    {'token': token, 'info': user, 'expire_at': datetime.fromtimestamp(time.time() + 60 * 60 * 1)}
)
复制代码

结果发现过了预期的时间好久后,数据依然存在。python

rs0:PRIMARY> db.user.getIndexes()
[
	{
		"v" : 2,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_",
		"ns" : "sign.user"
	},
	{
		"v" : 2,
		"key" : {
			"expire_at" : 1
		},
		"name" : "expire_at_1",
		"ns" : "sign.user",
		"expireAfterSeconds" : 0
	}
]
rs0:PRIMARY> db.user.find({"_id" : ObjectId("5e0cfc630f37131c94412622")}, {expire_at: 1}).pretty()
{
	"_id" : ObjectId("5e0cfc630f37131c94412622"),
	"expire_at" : ISODate("2020-01-02T10:10:22.160Z")
}
复制代码

一开始有点纳闷,仔细一看ISODate中有个"Z",大概知道怎么回事了:时区在做怪!mongodb

我们是东8时,比标准时间快了8小时,也就是说我这里设置的是1小时后过时,实际上存储的时间是标准时间的9小时后...api

因而想到下面两种方案来处理这个问题。缓存

方案1:使用pymongo保存时间时,指定时区bash

首先尝试用几种不一样的方法插入几条测试数据函数

db['user'].insert_one({'test': datetime.now()})
db['user'].insert_one({'test': datetime.utcnow()})
db['user'].insert_one({'test': datetime.fromtimestamp(time.time())})
db['user'].insert_one({'test': datetime.utcfromtimestamp(time.time())})
复制代码

python环境下查询结果以下测试

{'_id': ObjectId('5e0d7369db0af443d23152e5'), 'test': datetime.datetime(2020, 1, 2, 12, 36, 57, 550000)}
{'_id': ObjectId('5e0d737bdb0af443d23152e6'), 'test': datetime.datetime(2020, 1, 2, 4, 37, 15, 745000)}
{'_id': ObjectId('5e0d73e1db0af443d23152e7'), 'test': datetime.datetime(2020, 1, 2, 12, 38, 57, 925000)}
{'_id': ObjectId('5e0d73eadb0af443d23152e8'), 'test': datetime.datetime(2020, 1, 2, 4, 39, 6, 430000)}
复制代码

mongo客户端环境下查询结果以下spa

{ "_id" : ObjectId("5e0d7369db0af443d23152e5"), "test" : ISODate("2020-01-02T12:36:57.550Z") }
{ "_id" : ObjectId("5e0d737bdb0af443d23152e6"), "test" : ISODate("2020-01-02T04:37:15.745Z") }
{ "_id" : ObjectId("5e0d73e1db0af443d23152e7"), "test" : ISODate("2020-01-02T12:38:57.925Z") }
{ "_id" : ObjectId("5e0d73eadb0af443d23152e8"), "test" : ISODate("2020-01-02T04:39:06.430Z") }
复制代码

因而可知,若是直接使用pymongo及datetime保存时间字段是,若是不设置时区,就会与标准时间产生误差。设计

经过查阅pymongo的帮助文档发现,MongoClient这个类的构造函数中有一个参数tz_aware,也正如其字面含义(知道、察觉时区)。

使用MongoClient链接时将这个参数设为True,查询结果以下

{'_id': ObjectId('5e0d7369db0af443d23152e5'), 'test': datetime.datetime(2020, 1, 2, 12, 36, 57, 550000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
{'_id': ObjectId('5e0d737bdb0af443d23152e6'), 'test': datetime.datetime(2020, 1, 2, 4, 37, 15, 745000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
{'_id': ObjectId('5e0d73e1db0af443d23152e7'), 'test': datetime.datetime(2020, 1, 2, 12, 38, 57, 925000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
{'_id': ObjectId('5e0d73eadb0af443d23152e8'), 'test': datetime.datetime(2020, 1, 2, 4, 39, 6, 430000, tzinfo=<bson.tz_util.FixedOffset object at 0x7fa4b0a8fac0>)}
复制代码

这样可以知道查询到的时间使用的是什么时区,使用utcnowutcfromtimestamp这两个函数保存的时间查询后能够根据时区获得真实的时间,不过若是保存时就已经发生了错误,就没有办法了。

除此之外,有没有方法来控制保存时间时采用的时区呢?除了上文提到的utcnowutcfromtimestamp还有其它办法吗?继续查阅pymongo文档,其中提到了pytz这个库。

from datetime import datetime
import pytz

db['user'].insert_one(
    {'test': pytz.timezone('Asia/Shanghai').localize(datatime.now())}
)
复制代码

实际上,datetime也支持一个默认参数tz,能够传入tzinfo类型的值来指定时区。

db['user'].insert_one(
    {
        'token': ticket, 
        'info': user, 
        'expire_at': datetime.fromtimestamp(time.time() + 60 * 60 * 1, tz=pytz.timezone('Asia/Shanghai'))
    }
)
复制代码

这几种办法保存时间,采用的都是将时间先转化成标准时间,再进行保存。因此读取时也须要将标准时间转换成目标时区的时间。

from bson.codec_options import CodecOptions

collection = db.user.with_options(
    codec_options=CodecOptions(tz_aware=True, tzinfo=pytz.timezone('Asia/Shanghai'))
)
result = collection.find()
复制代码

得到带时区的datetime后,astimezone函数能够进行时区转换

import datetime
import pytz


if __name__ == '__main__':
    y = datetime.datetime(2020, 1, 2, 4, 37, 15, 745000, tzinfo=pytz.timezone('UTC'))
    # print(y.astimezone(pytz.timezone('Asia/Shanghai')))
    print(y.astimezone(datetime.timezone(datetime.timedelta(hours=8))))
    print(y.astimezone(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S'))
复制代码

结果以下

2020-01-02 12:37:15.745000+08:00
2020-01-02 12:37:15
复制代码

方案2:设置MongoDB的时区

本想应该能够设置时间保存的时区,结果却没有找到相应的方法设置MongoDB中ISODate的默认时区,暂且搁置,若是有新的发现再来补充。

总结

最后思考了下,出现这个问题的缘由,一方面是本身思考不足,另外一方面跟MongoDB的设计思路有关,它的TTL索引字段只支持ISODate类型,若是没有这个限制,在全部时间字段一概使用时间戳,就避免了这个问题。

相关文章
相关标签/搜索