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

标签 ruby-on-rails serialization bigdecimal

使用 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 仅获得安全更新,您的应用程序可能坚持旧的实现。

我想你会有三个选择:

  • 将您的 Rails 应用程序移植到 Rails 4。
  • Backport Psych 的 BigDecimal#to_yaml实现(猴子补丁猴子补丁)。
  • 切换到 Syck 作为 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 ...)

    更新 - 到目前为止我学到的东西
  • 根据 this blog entry,奇怪的行为似乎可以追溯到 Rails 1.2 时代(您也可以说是 2007 年 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)
    

    在不同的点(这里是 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 的结果相同。也许我遗漏了什么...
  • 在仔细重读你的问题时,我有一个想法:为什么不以另一种格式序列化,比如 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
    
  • 关于ruby-on-rails - 使用 Ruby on Rails 从数据库中的 yaml 序列化字段取回大小数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16031850/

    相关文章:

    java - 在 Android 上序列化复杂对象 (Java)

    java - 为什么 ObjectOutputStream.writeObject 不采用可序列化?

    Java BigDecimal 可以用逗号代替点吗?

    ruby-on-rails - mongoid嵌入式文档回调

    ruby-on-rails - 构建一个集成我的 rails 环境的 ruby​​ 守护进程

    ruby-on-rails - form_for 与关联 - 如何提供父 ID?

    Java-将 BigDecimal 作为字符串返回 : Returns int

    ruby-on-rails - 推荐用于软件即服务应用程序的 Rails 插件

    java - 多态对象的序列化

    java - BigDecimal 是如何实现的?