如果 timeZone 等于 +00:00,date 等于 new Date('2016-01-12 09:46:00'),到 UTC 的偏移等于 (timeZone - 本地时区) + timeZone:(00:00 - 08:00) + 00:00 = -08:00,即 2016-01-12 09:46:00-08:00,于是 format 后的结果是 2016-01-12 01:46:00。
如果 timeZone 等于 +08:00,date 等于 new Date('2016-01-12 09:46:00'),到 UTC 的偏移等于 (timeZone - 本地时区) + timeZone:(08:00 - 08:00) + 08:00 = 08:00,即 2016-01-12 09:46:00+08:00。于是 format 后的结果是 2016-01-12 09:46:00。
如果 timeZone 等于 Asia/Shanghai,结果也会是 2016-01-12 09:46:00,和 +08:00 等价。
sequelize 的 timezone 默认是 +00:00,所以,我们在 JavaScript 中的时间最后应用到数据库中都会被转换成 UTC 的时间(比实际的时间早 8 小时)。
MySQL -> JavaScript
这个转换过程实际上是更底层的 node-mysql 库来实现的。核心代码如下:
switch (field.type) { case Types.TIMESTAMP: case Types.DATE: case Types.DATETIME: case Types.NEWDATE: var dateString = parser.parseLengthCodedString(); if (dateStrings) { return dateString; } var dt; if (dateString === null) { return null; } var originalString = dateString; if (field.type === Types.DATE) { dateString += ' 00:00:00'; } if (timeZone !== 'local') { dateString += ' ' + timeZone; } dt = new Date(dateString); if (isNaN(dt.getTime())) { return originalString; } return dt; // 更多代码... }
处理过程大概是这样:
用 parser 将服务器返回的二进制数据解析为时间字符串
如果配置了强制返回字符串 dateStrings 而不是转换回 Date 类型,直接返回 dateString
如果字段类型是 DATE,时间字符串的时间部分统一为 00:00:00
如果配置的 timeZone 不是 local(本地时区),时间字符串加上时区信息
将时间字符串传给 Date 构造器,如果构造出的时间不合法,返回原始时间字符串,否则返回时间对象
默认情况下,sequelize 在进行连接时传递给 node-mysql 的 timeZone 是 +00:00,所以,第 4 步的时间字符串会是类似这样的值 2016-01-12 01:46:00+00:00,而这个值传递给 Date 构造器,在显示时转换回本地时区时间,就变成了 2016-01-12 09:46:00(比数据库中的时间晚 8 小时)。
一个例子
在使用 sequelize 定义模型时,其实是没有 TIMESTAMP 类型的,sequelize 只提供了一个 Sequelize.DATE 类型,生成建表语句时被转换为 DATETIME。
如果是在旧表上定义模型,而这张旧表刚好有 TIMESTAMP 类型的列,对 TIMESTAMP 类型的列定义模型时还是可以使用 Sequelize.DATE,对操作没有任何影响。但是 TIMESTAMP 是受 time_zone 设置影响的,这会引起一些困惑。下面我们来看一个例子。
sequelize 默认将 time_zone 设置为 +00:00,当我们执行下面代码时:
Test.create({ 'datetime': new Date('2016-01-10 20:07:00'), 'timestamp': new Date('2016-01-10 20:07:00') });
会进行上面提到的 JavaScript 时间到 MySQL 时间字符串的转换,生成的 SQL 其实是(时间被转换为了 UTC 时间,比本地时间早了 8 小时):
INSERT INTO `tests` (`id`,`datetime`,`timestamp`) VALUES (DEFAULT,'2016-01-10 12:07:00','2016-01-10 12:07:00');
当我们执行 Test.findAll() 来查询数据时,会进行上面提到的 MySQL 时间到 JavaScript 时间的转换,其实就是返回这样的结果(显示时时间从 UTC 时间转换回了本地时间):
> new Date('2016-01-10 12:07:00+00:00') Sun Jan 10 2016 20:07:00 GMT+0800 (CST)
和我们插入时的时间是一致的。
如果我们通过 MySQL 命令行来查询数据时,发现其实是这样的结果:
id
datetime
timestamp
1
2016-01-10 12:07:00
2016-01-10 20:07:00
这很好理解,因为我们数据库服务器的 time_zone 默认是东八区,TIMESTAMP 是受时区影响的,查询时被数据库服务器从 UTC 时间转换回了 time_zone 时区时间;DATETIME 不受影响,还是 UTC 时间。
如果我们先执行 SET time_zone = '+00:00',再进行查询,那结果就都会是 UTC 时间了。所以,不要以为数据出错了哦。
总结下就是,sequelize 会将本地时间转换为 UTC 时间后入库,查询时再将 UTC 时间转换为本地时间。这能达到最好的兼容性,存储总是使用 UTC 时间,展示时应用端自己转换为本地时区时间后显示。当然这个的前提是数据类型选用 DATETIME。
兼容老数据
这里要说的最后一个问题是基于旧表定义 sequelize 模型,并且表中时间值插入时没有转换为 UTC 时间(全部是东八区时间),而且 DATETIME 和 TIMESTAMP 混用,该怎么办?
在默认配置下,情况如下: