12

Ruby on Rails を使用すると、シリアル化されたフィールドがいくつかあります (主に配列またはハッシュ)。それらのいくつかにはBigDecimals が含まれています。これらの大きな小数が大きな小数のままであることは非常に重要ですが、Rails はそれらを浮動小数に変えています。BigDecimals を戻すにはどうすればよいですか?

この問題を調べると、Rails を使用せずにプレーンな Ruby で大きな 10 進数をシリアル化すると、期待どおりに動作することがわかりました。

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.rbBigDecimal で次のメソッドをオーバーライドする原因を突き止めました。

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

なぜ彼らはそれをするのでしょうか? さらに重要なことに、どうすれば回避できますか?

4

3 に答える 3

17

あなたが言及した ActiveSupport コア拡張コードは master ブランチで「すでに」修正されています (コミットは約 1 年前のもので、 Rails 2.1.0と同じくらい古い実装を元に戻します) が、Rails 3.2 はセキュリティ アップデートのみを取得するため、アプリは古い実装に固執しました。

次の 3 つのオプションがあると思います。

  1. Rails アプリを Rails 4 に移植します。
  2. Psych のBigDecimal#to_yaml実装をバックポートします (モンキー パッチ モンキー パッチ)。
  3. YAML エンジンとして Syck に切り替えます。

各オプションには独自の欠点があります。

時間があれば、 Rails 4 への移植が最良の選択肢のように思えます (上記のコミットは v4.0.0.beta1 以降の Rails で利用可能です)。まだリリースされていないため、ベータ版で作業する必要があります。いくつかの GSoCのアイデアは、まだ 4.0 リリースに入れることができるかのように読めますが、大きな変更が来るとは思えません...

ActiveSupport モンキーパッチへのモンキー パッチ適用は、それほど複雑ではありません。の元の実装は見つかりませんでしたがBigDecimal#to_yaml、多少関連する質問がこのコミットにつながりました。その特定のメソッドをバックポートする方法は、あなた (または他の StackOverflow ユーザー) に任せると思います。

簡単な回避策として、 Syck を YAML エンジンとして使用するだけです。同じ質問で、ユーザーrampionが次のコードを 投稿しました (初期化ファイルに配置できます)。

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

ここでの主な欠点 (Ruby 2.0.0 で Syck が利用できないことに加えて) は、Rails コンテキスト内で通常のBigDecimal ダンプを読み取ることができないことと、YAML ダンプを読み取りたいすべての人が同じ種類のローダーを必要とすることです。

BigDecimal.new('43.21').to_yaml
#=> "--- !induktiv.at,2007/BigDecimal 43.21\n"

(タグを に変更しても効果"tag:ruby/object:BigDecimal"はありません。結果が得られるため!ruby/object/BigDecimalです ...)


更新 – これまでに学んだこと

  1. この奇妙な動作は、 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)
    

    異なるポイント (ここではab、およびc ) で、 aBigDecimal.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 Core Extension によって引き起こされ、cはaと同じ結果になるはずです。多分私は何かが足りない...

  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
    
于 2013-04-18T21:35:24.923 に答える
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 のモンキー パッチを「元に戻す」必要はありません。このコミットによってモンキー パッチが削除されるからです。

于 2015-02-02T21:08:28.723 に答える
1

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
于 2015-05-05T03:49:35.670 に答える