Ruby on rails 使用Ruby on Rails从数据库中的yaml序列化字段返回大小数

Ruby on rails 使用Ruby on Rails从数据库中的yaml序列化字段返回大小数,ruby-on-rails,serialization,bigdecimal,Ruby On Rails,Serialization,Bigdecimal,使用RubyonRails,我有几个字段是序列化的(主要是数组或散列)。其中一些包含BigDecimals。这些大小数保持为大小数非常重要,但Rails正在将它们变成浮点数。如何取回BigDecimals 研究这个问题时,我发现在无Rails的纯Ruby中序列化一个大的十进制数,效果与预期的一样: BigDecimal.new("42.42").to_yaml => "--- !ruby/object:BigDecimal 18:0.4242E2\n...\n" 但在Rails控制台中

使用RubyonRails,我有几个字段是序列化的(主要是数组或散列)。其中一些包含
BigDecimal
s。这些大小数保持为大小数非常重要,但Rails正在将它们变成浮点数。如何取回
BigDecimal
s

研究这个问题时,我发现在无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
(这是我不想做的,因为它容易出错),我也可能会失去精度,这对我的应用程序来说是不可接受的

我在
activesupport-3.2.11/lib/active\u support/core\u ext/big\u 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核心扩展代码在主分支中“已经”修复(大约有一年的历史,并且撤销了一个旧的实现),但是由于Rails 3.2只获得安全更新,您的应用程序可能会被旧的实现卡住

我想你有三个选择:

  • 将Rails应用程序移植到Rails 4
  • 将Psych的
    BigDecimal#to_yaml
    实现(猴子补丁-猴子补丁)向后移植
  • 切换到Syck作为YAML发动机
  • 每种选择都有自己的缺点:

    移植到Rails 4在我看来是最好的选择,如果您有时间的话(上面提到的提交从v4.0.0.beta1开始在Rails中可用)。因为它还没有发布,所以你必须使用测试版。我不怀疑会有什么大的变化,尽管一些GSoC读到好像他们仍然可以进入4.0版本

    猴子补丁ActiveSupport猴子补丁应该不那么复杂。虽然我没有找到
    BigDecimal#to_yaml
    的原始实现,但在某种程度上导致了错误。我想我会让您(或其他StackOverflow用户)来决定如何对特定方法进行后端口

    作为快速而肮脏的解决方法,您可以简单地使用Syck作为YAML引擎。在同一个问题中,使用这段代码(您可以将其放在初始值设定项文件中):

    YAML::ENGINE.yamler='syck'
    类BigDecimal
    def to_yaml(opts={})
    YAML::快速发射(对象id,选项)执行|
    标量(“标签:induktiv.at,2007:BigDecimal”,self.to_s)
    结束
    结束
    结束
    YAML.add_domain_type(“induktiv.at,2007”,“BigDecimal”)do|type,val|
    BigDecimal.new(val)
    结束
    
    这里的主要缺点(除了Ruby 2.0.0上的Syck不可用之外)是,您无法在Rails上下文中读取正常的BigDecimal转储,每个想要读取YAML转储的人都需要相同类型的加载程序:

    BigDecimal.new('43.21')。至
    #=>“--!induktiv.at,2007/BigDecimal 43.21\n”
    
    (将标记更改为
    “tag:ruby/object:BigDecimal”
    也不会有帮助,因为它会产生
    !ruby/object/BigDecimal
    。)


    更新–我迄今学到的东西
  • 这种奇怪的行为似乎可以追溯到Rails 1.2的时代(你也可以说是2007年2月)

  • 以这种方式修改
    config/application.rb
    没有帮助:

    需要文件。展开路径('../boot',文件)
    #(a)
    %w[yaml psych bigdecimal]。每个{lib | require lib}
    类BigDecimal
    #备份旧方法定义
    @@old_to_yaml=实例方法:to_yaml
    @@old_to_s=实例_方法:to_s
    结束
    需要“rails/all”
    #(b)
    类BigDecimal
    #恢复旧的行为
    define_method:to_yaml do | opts={}|
    @@旧到旧绑定(自身)。(选项)
    结束
    定义_方法:to_s do | format='E'|
    @@旧到旧绑定(self)。(格式)
    结束
    结束
    #(c)
    
    在不同的点(这里是a、b和c),a
    BigDecimal.new(“42.21”)。to_yaml
    产生了一些有趣的输出:

    #(a)=>“--!ruby/object:BigDecimal 18:0.4221E2\n…”
    #(b)=>“--42.21\n…”
    #(c)=>“--0.4221E2\n…”
    
    其中a是默认行为,b是由ActiveSupport核心扩展引起的,c应该是与a相同的结果。也许我错过了什么

  • 在仔细阅读您的问题时,我有一个想法:为什么不以另一种格式序列化,比如JSON?向数据库中添加另一列,并按如下方式随时间迁移:

    class-Person

  • 如果您使用的是Rails 4.0或更高版本(但低于4.2),您可以通过使用
    删除方法
    BigDecimal\encode\u来解决这个问题

    您可以使用
    undef\u方法
    将其存档:

    require 'bigdecimal'
    require 'active_support/core_ext/big_decimal'
    
    class BigDecimal
      undef_method :encode_with
    end
    
    我把这段代码放在一个初始值设定项中,现在它可以工作了。
    Rails猴子补丁的“还原”在Rails 4.2中是不必要的,因为删除了猴子补丁。

    对于Rails 3.2,以下工作:

    # config/initializers/backport_yaml_bigdecimal.rb
    
    require "bigdecimal"
    require "active_support/core_ext/big_decimal"
    
    class BigDecimal
      remove_method :encode_with
      remove_method :to_yaml
    end
    
    如果没有此修补程序,在rails 3.2控制台中:

    irb> "0.3".to_d.to_yaml
    => "--- 0.3\n...\n"
    
    使用此修补程序:

    irb> "0.3".to_d.to_yaml
    => "--- !ruby/object:BigDecimal 18:0.3E0\n...\n"
    
    您可能希望将其包装在带有文档和弃用警告的版本测试中,例如:

    # BigDecimals should be correctly tagged and encoded in YAML as ruby objects
    # instead of being cast to/from floating point representation which may lose
    # precision.
    #
    # This is already upstream in Rails 4.2, so this is a backport for now.
    #
    # See http://stackoverflow.com/questions/16031850/getting-big-decimals-back-from-a-yaml-serialized-field-in-the-database-with-ruby
    #
    # Without this patch:
    #
    #   irb> "0.3".to_d.to_yaml
    #   => "--- 0.3\n...\n"
    #
    # With this patch:
    #
    #   irb> "0.3".to_d.to_yaml
    #   => "--- !ruby/object:BigDecimal 18:0.3E0\n...\n"
    #
    if Gem::Version.new(Rails.version) < Gem::Version.new("4.2")
      require "bigdecimal"
      require "active_support/core_ext/big_decimal"
    
      class BigDecimal
        # Rails 4.0.0 removed #to_yaml
        # https://github.com/rails/rails/commit/d8ed247c7f11b1ca4756134e145d2ec3bfeb8eaf
        if Gem::Version.new(Rails.version) < Gem::Version.new("4")
          remove_method :to_yaml
        else
          ActiveSupport::Deprecation.warn "Hey, you can remove this part of the backport!"
        end
    
        # Rails 4.2.0 removed #encode_with
        # https://github.com/rails/rails/commit/98ea19925d6db642731741c3b91bd085fac92241
        remove_method :encode_with
      end
    else
      ActiveSupport::Deprecation.warn "Hey, you can remove this backport!"
    end
    
    #bigdecimal应正确标记,并在YAML中编码为ruby对象
    #而不是从可能丢失的浮点表示中转换
    #精确性。
    #
    #这已经在Rails 4.2的上游,所以这是一个