Rails 时区操作
事先声明,本文操作均使用 PG 数据库,保存到数据库的时间不带时区。
配置
默认转成 UTC 时间 存储、读取(显示)。
# 存储到DB时间:2021-10-16 14:34:27 +0800
dbconsole中查询:2021-10-16 06:34:27
console中查询:2021-10-16 06:34:27 UTC
Rails 中有两个配置用于更新存储和读取时区:
1) config.active_record.default_timezone
修改数据库存储时区
可能取值为:local
, :utc
(默认),设置为:local
的话表示使用本地系统时区:
# 存储到DB时间:2021-10-16 14:34:27 +0800
dbconsole中查询:2021-10-16 14:34:27
console中查询:2021-10-16 06:34:27 UTC
注意:DB 中存的时间数据不会因 default_timezone 的切换而改变,不过时区更改会影响读取结果。
例如:假设数据库中存有一个时间:2021-10-16 06:34:27
(默认为UTC时间),那么 config.active_record.default_timezone
配置改成 :local
之后,dbconsole 中查询得到 2021-10-16 06:34:27
,console中查询得到 2021-10-15 22:34:27 UTC
2) config.time_zone
修改时间读取/显示时区
假设设置成 Beijing
:
# 存储到DB时间:2021-10-16 14:34:27 +0800
dbconsole中查询:2021-10-16 14:34:27 # :local
console中查询:2021-10-16 14:34:27 CST +0800
处理
1) 获取当前时间/时区
# 没设置 config.time_zone
> Time.now # 获取系统本地时间
=> 2021-10-17 21:27:26 +0800
> Time.current # 获取config.time_zone设置时区时间,没设置默认为UTC
=> Sun, 17 Oct 2021 13:28:06.791697000 UTC +00:00
> Time.zone.name # 获取应用时区
=> "UTC"
2) 类型转换
- String 转 Time
to_time
& Time.parse
: 字符串中没包含时区,则默认使用系统时区
> "2021-10-16 14:00:00".to_time
=> 2021-10-16 14:00:00 +0800
Time.parse("2021-10-16 14:00:00")
=> 2021-10-16 14:00:00 +0800
to_datetime
& DateTime.strptime
: 字符串中不包含时区,默认为UTC时区
> "2021-10-16 14:00:00".to_datetime
=> Sat, 16 Oct 2021 14:00:00 +0000
> DateTime.strptime("2021-10-16 14:00:00", "%Y-%m-%d %H:%M:%S")
=> Sat, 16 Oct 2021 14:00:00 +0000
> DateTime.strptime("2021-10-16 14:00:00 +8", "%Y-%m-%d %H:%M:%S %z")
=> Sat, 16 Oct 2021 14:00:00 +0800
strptime 具体操作可看官方文档 strptime (DateTime) - APIdock
- Time转String
> "2021-10-16 14:00:00".to_time.strftime('%m/%d/%Y %H:%M:%S %p %z')
=> "10/16/2021 14:00:00 PM +0800"
3) 时区转换
# 没设置 config.time_zone
time = Time.current # 2021-10-17 13:49:09 UTC
# --- use_zone 设置的时区只在 block 中有效,是一次性的 ---
Time.use_zone "Central Time (US & Canada)" do
puts time.to_time # 2021-10-17 13:49:09 +0000
puts Time.zone.name # "Central Time (US & Canada)"
puts time.in_time_zone # 2021-10-17 08:49:09 -0500
puts Time.current # 2021-10-17 08:49:09 -0500
puts Time.now # 2021-10-17 21:49:09 +0800
end
puts Time.current # 2021-10-17 13:49:09 UTC
# --- in_time_zone 不传参数默认用应用设置时区,操作对象可以是 Stirng 或 Time ---
puts time.in_time_zone("Beijing") # 2021-10-17 21:49:09 +0800
更新
设置的时间如果没加时区,会直接将时间存入数据库,由active_record.default_timezone
确定实际表示的时区
# config.active_record.default_timezone = :local
# config.time_zone = "UTC"
update(operated_at: "2021-10-16 14:00:00")
# console中查询:2021-10-16 06:00:00 UTC +0000
查询
1) 获取时间段内的记录
比如这句是获取 “2021-10-29 00:00:00 +0800” 之后更新的用户数据
# config.active_record.default_timezone = :utc
# config.time_zone = "Beijing"
> User.where("updated_at > ?", "2021-10-29 00:00:00".to_time)
# User Load (0.2ms) SELECT "users".* FROM "users" WHERE (updated_at > '2021-10-28 16:00:00')
=> [#<User ... updated_at: "2021-10-28 16:22:50">]
换个写法试下,发现返回结果并不是我们想要的。
> User.where("updated_at > '#{'2021-10-29 00:00:00'.to_time}'")
# User Load (0.2ms) SELECT "users".* FROM "users" WHERE (updated_at > '2021-10-29 00:00:00 +0800')
=> #<ActiveRecord::Relation []>
从第一种写法的输出日志可以看出, SQL 语句会把查询时间转换成数据库时区后再去做比较查询,而第二种写法被解析成查询 2021-10-29 00:00:00 +0000
之后的记录了。
正确的 pg 时区查询语法应该是这样的:
> User.where("updated_at > (timestamp '2021-10-29 00:00:00' at time zone 'Asia/Chongqing')")
# User Load (0.6ms) SELECT "users".* FROM "users" WHERE (updated_at > (timestamp '2021-10-29 00:00:00' at time zone 'Asia/Chongqing'))
=> [#<User ... updated_at: "2021-10-28 16:22:50">]
有返回 2021-10-29 00:00:00 +8000
之后的记录。
2) PG Date + 时区
Date 默认是以数据库时区为准,假设数据库和读取时区设置不相同,那要怎么获取 config.time_zone
设置的时区的日期?带着这个问题我门继续往下看。
假设时区设置为:
config.active_record.default_timezone = :utc
config.time_zone = "Beijing"
如果想获取某个时间对应的北京时间日期,可以这样写:
1. 直接加时差
date(updated_at + interval '8 hour')
2. 使用 date 方法前先转换下时区。由于数据库没有存时区,所以需要先转成 UTC 再转成 中国时区。
User.where(id: 111).select("updated_at, date((updated_at at time zone 'UTC') at time zone 'Asia/Chongqing')")
# User Load (1.0ms) SELECT updated_at, date((updated_at at time zone 'UTC') at time zone 'Asia/Chongqing') FROM "users" WHERE "users"."id" = $1 [["id", 111]]
=> [#<User id: nil, updated_at: "2022-01-16 18:00:00", date: "2022-01-17">]
3. date 改成用 date_trunc
User.where(id: 111).select("updated_at, date_trunc('day', (updated_at at time zone 'UTC') at time zone 'Asia/Chongqing')")
# User Load (3.2ms) SELECT updated_at, date_trunc('day', (updated_at at time zone 'UTC') at time zone 'Asia/Chongqing') FROM "users" WHERE "users"."id" = $1 [["id", 111]]
=> [#<User id: nil, updated_at: "2022-01-16 18:00:00", date_trunc: "2022-01-17 00:00:00">]
思考
1、如果将项目部署到多台同个国家的服务上,那 config.active_record.default_timezone
只能设置成 :utc
。
2、如果项目在服务上运行了一段时间并产生一些数据后,修改 config.time_zone
配置,会有什么影响?