Logo

Rails 时区操作

avatar jane 22 Feb 2022

事先声明,本文操作均使用 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">]

时区转换参考 timezone conversion in Postgresql

思考

1、如果将项目部署到多台同个国家的服务上,那 config.active_record.default_timezone 只能设置成 :utc

2、如果项目在服务上运行了一段时间并产生一些数据后,修改 config.time_zone 配置,会有什么影响?

Tags
rails
PG
Time Zone