使用 Ruby on Rails 我有几个序列化的字段(主要是数组或散列)。其中一些包含 BigDecimal
s。那些大小数仍然是大小数是非常重要的,但是 Rails 正在把它们变成浮点数。我如何获得 BigDecimal
回来了?
在研究这个问题时,我发现在没有 Rails 的情况下,在普通 Ruby 中序列化一个大十进制数可以按预期工作:
BigDecimal.new("42.42").to_yaml
=> "--- !ruby/object:BigDecimal 18:0.4242E2\n...\n"
但在 Rails 控制台中,它没有:
BigDecimal.new("42.42").to_yaml
=> "--- 42.42\n"
这个数字是大十进制的字符串表示,所以没问题。但是当我读回它时,它被读取为一个浮点数,所以即使我将它转换为
BigDecimal
(我不想做的事情,因为它很容易出错),我可能会失去精度,这对我的应用程序来说是 Not Acceptable 。我将罪魁祸首追查到
activesupport-3.2.11/lib/active_support/core_ext/big_decimal/conversions.rb
它覆盖了 BigDecimal 中的以下方法:YAML_TAG = 'tag:yaml.org,2002:float'
YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
# This emits the number without any scientific notation.
# This is better than self.to_f.to_s since it doesn't lose precision.
#
# Note that reconstituting YAML floats to native floats may lose precision.
def to_yaml(opts = {})
return super if defined?(YAML::ENGINE) && !YAML::ENGINE.syck?
YAML.quick_emit(nil, opts) do |out|
string = to_s
out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain)
end
end
他们为什么要这样做?更重要的是,我如何解决它?
最佳答案
您提到的 ActiveSupport 核心扩展代码“已经”固定在 master 分支中(commit 大约有一年历史,并撤消了与 Rails 2.1.0 一样古老的实现),但由于 Rails 3.2 仅获得安全更新,您的应用程序可能坚持旧的实现。
我想你会有三个选择:
BigDecimal#to_yaml
实现(猴子补丁猴子补丁)。 每个选项都有其自身的缺点:
移植到 Rails 4 在我看来,这是最好的选择,如果你有时间的话(上面提到的提交从 v4.0.0.beta1 开始就在 Rails 中可用)。由于它还没有发布,你必须使用测试版。尽管有些 GSoC ideas,但我不怀疑会发生任何重大变化。读起来好像他们仍然可以进入 4.0 版本......
猴子补丁 ActiveSupport 猴子补丁应该不那么复杂。虽然我没有找到
BigDecimal#to_yaml
的原始实现,有点related question导致 this commit .我想我会将如何向后移植该特定方法的问题留给您(或其他 StackOverflow 用户)。作为快速的解决方法,您可以简单地 使用 Syck 作为 YAML 引擎 .在同一问题中,用户 rampion posted这段代码(你可以放在初始化文件中):
YAML::ENGINE.yamler = 'syck'
class BigDecimal
def to_yaml(opts={})
YAML::quick_emit(object_id, opts) do |out|
out.scalar("tag:induktiv.at,2007:BigDecimal", self.to_s)
end
end
end
YAML.add_domain_type("induktiv.at,2007", "BigDecimal") do |type, val|
BigDecimal.new(val)
end
这里的主要缺点(除了 Syck 在 Ruby 2.0.0 上不可用)是,您无法在 Rails 上下文中读取正常的 BigDecimal 转储,并且每个想要读取您的 YAML 转储的人都需要相同类型的加载器:
BigDecimal.new('43.21').to_yaml
#=> "--- !induktiv.at,2007/BigDecimal 43.21\n"
(将标签更改为
"tag:ruby/object:BigDecimal"
也无济于事,因为它会产生 !ruby/object/BigDecimal
...)更新 - 到目前为止我学到的东西
config/application.rb
就这样不是 帮助:require File.expand_path('../boot', __FILE__)
# (a)
%w[yaml psych bigdecimal].each {|lib| require lib }
class BigDecimal
# backup old method definitions
@@old_to_yaml = instance_method :to_yaml
@@old_to_s = instance_method :to_s
end
require 'rails/all'
# (b)
class BigDecimal
# restore the old behavior
define_method :to_yaml do |opts={}|
@@old_to_yaml.bind(self).(opts)
end
define_method :to_s do |format='E'|
@@old_to_s.bind(self).(format)
end
end
# (c)
在不同的点(这里是 a、b 和 c)a
BigDecimal.new("42.21").to_yaml
产生了一些有趣的输出:# (a) => "--- !ruby/object:BigDecimal 18:0.4221E2\n...\n"
# (b) => "--- 42.21\n...\n"
# (c) => "--- 0.4221E2\n...\n"
其中 a 是默认行为,b 是由 ActiveSupport 核心扩展引起的,c 应该与 a 的结果相同。也许我遗漏了什么...
class Person < ActiveRecord::Base
# the old serialized field
serialize :preferences
# the new one. once fully migrated, drop old preferences column
# rename this to preferences and remove the getter/setter methods below
serialize :pref_migration, JSON
def preferences
if pref_migration.blank?
pref_migration = super
save! # maybe don't use bang here
end
pref_migration
end
def preferences=(*data)
pref_migration = *data
end
end
关于ruby-on-rails - 使用 Ruby on Rails 从数据库中的 yaml 序列化字段取回大小数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16031850/