search
LoginSignup
30

More than 5 years have passed since last update.

posted at

updated at

Organization

Marshal.loadする時の注意点

Ruby オブジェクトをファイル(または文字列)に書き出したり、読み戻したり する機能を提供するモジュール。
Ruby 2.1.0 リファレンスマニュアル | module Marshal

それが Marshal です。
オブジェクトをファイルに書きだすということで、HashをMarshal.dumpして文字列化したものをRedisやMongoといったデータストアに保存するという使い方があるかと思います。
(世間一般ではjsonでやることのほうが多そうなイメージがありますが)

sample1.rb
hoge = {
  foo: 'bar'
}
p str = Marshal.dump(hoge) # => "\x04\b{\x06:\bfooI\"\bbar\x06:\x06ET"
# strをデータストアに入れる
p Marshal.load(str) # => {:foo=>"bar"}

で、この前ハマったのが以下のパターン。

set.rb
require 'redis'
require 'date'

conf = {
  # ここにはRedisの設定が記述してあります
}
redis = Redis.new(conf)
date = Date.new(1999, 2, 24)
redis.set('key', Marshal.dump(date))
get.rb
require 'redis'

conf = {
  # set.rbと同様のRedisの設定が記述してあります
  # 本来はちゃんと共通のmodule化してます。
}
redis = Redis.new(conf)
Marshal.load(redis.get('key'))
# => TypeError: instance of Date needs to have method `marshal_load'

えっ。

Marshal.dump

  • 名前のついてないオブジェクト
  • システムがオブジェクトを保持するもの

などを書きだそうとするとTypeErrorを起こすとマニュアルには記載してあります。

error_example.rb
Marshal.dump(Dir.new('/')) # Dirが「システムがオブジェクトを保持するもの」に該当している
# => TypeError: no _dump_data is defined for class Dir

しかしMarshal.loadのタイミングで起きるとはどういうことだ?
と、しばしハマっていました。

で、結論ですが

get.rb
require 'redis'
require 'date' # <= これ

conf = {
  # set.rbと同様のRedisの設定が記述してあります
}
redis = Redis.new(conf)
Marshal.load(redis.get('key'))
# => #<Date: 1993-02-24 ((2449043j,0s,0n),+0s,2299161j)>

Marshal.loadするときに対象のオブジェクトの状態を復元するには同じ環境になっていないといけない、という感じです。(こんな表現で伝わるか不明ですが…)

他のオブジェクトであれば割と分かりやすいエラーです。

decimal_set.rb
require 'redis'
require 'bigdecimal'

conf = {
  # ここにはRedisの設定が記述してあります
}
redis = Redis.new(conf)
decimal = BigDecimal.new('0.1234')
redis.set('key', Marshal.dump(decimal))
decimal_get.rb
require 'redis'

conf = {
  # decimal_set.rbと同様のRedisの設定が記述してあります
}
redis = Redis.new(conf)
Marshal.load(redis.get('key'))
# => ArgumentError: undefined class/module BigDecimal

これなら「あ、bigdecimalをrequireし忘れてた」となるのですが、DateはrequireせずともDateってオブジェクトがいるんですよね。

p Date # => Date

これのせいで余計な混乱を招いた形です。

こいつがなんなのか、よく分かっていないのですがDate.methodsしてもdateライブラリのメソッドが出てこない辺り、require 'date'をするとDateクラスを再オープンしてるのかなー、とか想像しています。Date.freezeしたあとにrequire 'date'やったらRuntimeError: can't modify frozen classになったから多分そう。

ということで、Marshal.loadの実行時には環境を合わせましょうね、というtipsでした。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
30