【问题标题】:Getting big decimals back from a yaml-serialized field in the database with Ruby on Rails使用 Ruby on Rails 从数据库中的 yaml 序列化字段中获取大小数
【发布时间】:2013-04-08 13:14:03
【问题描述】:

使用 Ruby on Rails 我有几个序列化的字段(主要是数组或哈希)。其中一些包含BigDecimals。那些大小数仍然是大小数是非常重要的,但是 Rails 正在将它们变成浮点数。如何找回BigDecimals?

查看这个问题,我发现在没有 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_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

他们为什么要这样做?更重要的是,我该如何解决?

【问题讨论】:

    标签: ruby-on-rails serialization bigdecimal


    【解决方案1】:

    您提到的 ActiveSupport 核心扩展代码“已经”在 master 分支中修复(commit 大约有一年的历史,并且撤消了与Rails 2.1.0 一样古老的实现),但由于 Rails 3.2 仅获得安全更新,您的应用可能会卡在旧的实现上。

    我想你会有三个选择:

    1. 将您的 Rails 应用程序移植到 Rails 4。
    2. Backport Psych 的 BigDecimal#to_yaml 实现(猴子补丁猴子补丁)。
    3. 切换到 Syck 作为 YAML 引擎。

    每个选项都有自己的缺点:

    移植到 Rails 4 在我看来是最好的选择,如果你有时间的话(上面提到的提交在 Rails v4.0.0.beta1 之后可用)。由于它尚未发布,因此您必须使用测试版。我不怀疑会有任何重大变化,尽管一些 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...)


    更新——到目前为止我学到的东西

    1. 根据this blog entry,奇怪的行为似乎可以追溯到 Rails 1.2 时代(您也可以说是 2007 年 2 月)。

    2. 以这种方式修改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)
      

      在不同的点(此处为 abcBigDecimal.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 应该是与 一个。也许我错过了什么......

    3. 仔细阅读您的问题后,我有了这样的想法:为什么不以另一种格式序列化,例如 JSON?将另一列添加到您的数据库并随着时间的推移进行迁移,如下所示:

      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
      

    【讨论】:

    • 这是一个很好的答案。我无法评论它的正确性,但它确实可读且清晰。你很快给你的问题一个明确的答案,然后继续处理后果。讨论了每个建议的好处和可能的风险。一个不错的作品。干得好。
    • 谢谢。我只是尽力提供帮助(尽管有时英语会妨碍我;-))
    • 您好,谢谢您的回答。我正在运行最新的稳定版本的 Rails,所以我不会称之为卡在旧版本中。我们还不能升级到 Rails 4。您说其中一种选择是切换到 syck,但我们已经在使用 syck。也许我误以为代码正在运行,syck 也包含这种序列化大小数的方式?
    • 我指的是BigDecimal#to_yaml 的ye ole 实现:-) 我目前正在Rails 控制台中进行黑客攻击,并试图弄清楚如何规避上述实现。据我所知,两个引擎都会将 BigDecimals 转换为浮点数——尽管有条件 super 调用...
    【解决方案2】:

    如果您使用的是 Rails 4.0 或更高版本(但低于 4.2),您可以通过删除方法 BigDecimal#encode_with 来解决它。

    您可以使用 undef_method 将其归档:

    require 'bigdecimal'
    require 'active_support/core_ext/big_decimal'
    
    class BigDecimal
      undef_method :encode_with
    end
    

    我把这段代码放在一个初始化器中,现在它可以工作了。 在 Rails 4.2 中,Rails 猴子补丁的这种“还原”不是必需的,因为this commit 删除了猴子补丁。

    【讨论】:

      【解决方案3】:

      对于 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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-03
        • 2014-12-10
        • 1970-01-01
        • 1970-01-01
        • 2011-02-28
        • 1970-01-01
        相关资源
        最近更新 更多