Ruby中DateTime的毫秒分辨率

33

我有一个字符串,类似于2012-01-01T01:02:03.456,我正在使用ActiveRecord将其存储在Postgres数据库中的时间戳(TIMESTAMP)字段。

不幸的是,Ruby似乎会截去毫秒部分:

ruby-1.9.3-rc1 :078 > '2012-12-31T01:01:01.232323+3'.to_datetime
 => Mon, 31 Dec 2012 01:01:01 +0300 

PostgreSQL支持微秒级别的精度。我该如何使我的时间戳相应地保存?我至少需要毫秒级别的精度。

(PS 是的,我可以在PostgreSQL中添加一个毫秒级整数列;但这样做有点违背了ActiveRecord的初衷。)

更新:
非常有帮助的答复表明Ruby的 DateTime 并没有砍掉毫秒级别的精度。使用 #to_f 可以看到。 但是,执行以下操作:

m.happened_at = '2012-01-01T00:00:00.32323'.to_datetime
m.save!
m.reload
m.happened_at.to_f

去掉毫秒。

有趣的是,created_at 时间戳在 Rails 和 Postgres 中都会显示毫秒。但是其他时间戳字段(如上面的 happened_at)不会显示毫秒。(也许 Rails 在 created_at 上使用了 NOW() 函数而不是传入 DateTime)。

这引出了我的最终问题:
我如何让 ActiveRecord 保留时间戳字段的毫秒分辨率?

4个回答

24

ActiveRecord应该从数据库中保留完整的精度,只是你没有正确地查看它。使用strftime%N格式来查看小数部分。例如,psql显示如下:

=> select created_at from models where id = 1;
         created_at         
----------------------------
 2012-02-07 07:36:20.949641
(1 row)

而 ActiveRecord 表示:

> Model.find(1).created_at.strftime('%Y-%m-%d %H:%M:%S.%N')
 => "2012-02-07 07:36:20.949641000" 

所以一切都在那里,你只需要知道如何看到它。

还要注意,ActiveRecord 可能会给你 ActiveSupport::TimeWithZone 对象而不是 DateTime 对象,但是 DateTime 也可以保留一切:

> '2012-12-31T01:01:01.232323+3'.to_datetime.strftime('%Y-%m-%d %H:%M:%S.%N')
 => "2012-12-31 01:01:01.232323000" 
请查看 ActiveRecord 源码中的 connection_adapters/column.rb 文件,并查看 string_to_time 方法的功能。你的字符串将按照 fallback_string_to_time 的路径进行转换,根据我所知,它会保留小数秒。其他地方可能发生了一些奇怪的事情,考虑到我在 Rails 源码中看到的一些奇怪的事情,特别是数据库部分,这并不让我感到惊讶。我建议尝试手动将字符串转换为对象,以便 ActiveRecord 不会对其进行更改。

1
有趣的是,尽管 created_at 显示毫秒分辨率,但当我在模型中有一个日期时间字段,保存并重新加载它时,它就会失去毫秒分辨率。 - SRobertJames
问题不在于created_at,而是在其他字段。 - SRobertJames
1
我只用了 created_at,因为这是我手边有的。你在哪里失去了精度?是在进入数据库之前?还是在数据库中?还是在取出之后? - mu is too short
1
如果您自己将字符串转换为DateTimeActiveSupport::TimeWithZone,而不是让AR来处理,会发生什么? - mu is too short
1
不要忘记使用 %z 来指定时区。代码应该像这样:.strftime('%Y-%m-%d %H:%M:%S.%N %z') - Fabian
@Fabian 但是这是Rails,所以该列将是一个普通的timestamp(而不是timestamp with time zone),时区将是UTC。 - mu is too short

9

将上述代码中的 m.happened_at = '2012-01-01T00:00:00.32323'.to_datetime 改为 m.happened_at = '2012-01-01T00:00:00.32323' 可以解决问题,但我不知道原因。


1
确认一下,将其保存为字符串而不是日期时间可以保留小数秒。 - Dan Sandberg
10
请注意,DateTime.now.to_s不会返回小数秒。我改用了DateTime.now.iso8601(6) - user1003545
2
这个问题真是让人抓狂,我刚刚浪费了两个小时在上面。感谢你的回答。 - sidney
很可能喜欢使用.to_s(:db)格式化程序,例如.created_at.to_s(:db) # "2017-03-25 00:39:49",还有:DateTime.now.to_s(:db) # => "2017-03-28 16:54:01" - Dorian

3
当我在使用RVM提供的二进制Ruby 2.0.0-p247时(在OS X Mavericks上),从Postgres检索时间时会导致将秒舍入为整数。重建Ruby自己(rvm reinstall 2.0.0 --disable-binary)解决了我的问题。请参见https://github.com/wayneeseguin/rvm/issues/2189,我是通过https://github.com/rails/rails/issues/12422找到它的。我知道这不是这个问题的答案,但我希望这个笔记能帮助遇到类似问题的人。

1

to_datetime 不会破坏数据的毫秒分辨率 - 它只是隐藏了,因为 DateTime#to_s 不显示它。

[1] pry(main)> '2012-12-31T01:01:01.232323+3'.to_datetime
=> Mon, 31 Dec 2012 01:01:01 +0300
[2] pry(main)> '2012-12-31T01:01:01.232323+3'.to_datetime.to_f
=> 1356904861.232323

话虽如此,我怀疑当持久化数据时,ActiveRecord错误地隐藏了该信息;请记住它是数据库无关的,因此采用的方法保证适用于其所有数据库目标。虽然Postgres在时间戳中支持微秒级信息,但MySQL不支持,因此我怀疑AR选择了最低公共分母。如果不深入了解AR的内部情况,我无法确定。您可能需要一个Postgres特定的monkeypatch来启用此行为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接