起因
老大发现,一个sql在rails console里执行的结果是1,在数据库(PostgreSQL)的console里执行结果是23 :
SELECT COUNT(*) FROM “users” WHERE “users”.”active” = ‘t’ AND (expire < NOW())
然后老大很快就发现这是时区的问题。。
概念
首先了解一下概念:
UTC - Coordinated Universal Time (不知道缩写为啥不是CUT。。。)是世界上主要的时间标准。
GMT - Greenwich Mean Time 初中学过!格林威治时间,是一些欧洲和非洲官方使用的时区。它的UTC offset就是”+0000”。
而中国使用的北京时间就是”UTC+0800”,UTC加8个小时的意思。
真相
那偏差又是怎么来的呢?
原来,数据库的每个连接,都是可以设置时区的,而rails用的是UTC(也就是说,rails在从数据库读活着写时间的时候,都会转换成UTC):
irb(main):015:0> User.connection.execute("SELECT current_setting('TIMEZONE');")[0]
(0.2ms) SELECT current_setting('TIMEZONE');
=> {"current_setting"=>"UTC"}
而每次读出来后,rails都会根据TIMEZONE环境变量来自动做offset,让人看到的时间是正确的。
而服务器上的时间设置的+0800,所以通过数据库终端进去的时区和rails console里进去是不一样的,这就是误差的来源。在把数据库终端的连接也设置成UTC之后,查询的结果就一致了。
另外:
如果不想使用默认的UTC,想用本地的timezone,可以这样设置:
config.active_record.default_timezone = :local
默认情况,ActiveRecord::Base 保持所有的datetime的列意识到timezone的存在(UTC吧):
config.active_record.time_zone_aware_attributes = true
设置为false即可关掉这个功能。
而如果这个功能打开了,而又不想在读某些属性的时候让它转换成你本地的Time.zone,可以设置skip_time_zone_conversion_for_attributes
比如:
class Topic < ActiveRecord::Base
self.skip_time_zone_conversion_for_attributes = [:written_on]
end
延伸
Ruby中有三个类来处理日期和时间:使用了date库的Date 和DateTime ,以及使用了自己time库的Time。
DateTime和Time都可以用来处理年月日时分秒等,但是区别在于,在后端(backend side),Time存储的是整型数,表示从Epoch到现在有多少秒。