Rails5.2にアップデート中に遭遇したので、メモしておく。
環境
- ruby: 2.5.1
- rails: 5.2.0
- mongoid: 6.4.0
- mongo: 2.5.1
- bson: 4.3.0
attributes.sliceの結果が急に空になった
アップデート前 (5.1.4)
model.attributes.slice(:hoge) #=> {“hoge”: ... }
アップデート後 (5.2.0)
model.attributes.slice(:hoge) #=> {}
※ attributes.slice
のkeyにSymbolを使っている場合に発生
原因
- mongoidのattributesは
Hash
を継承したBSON::Document
のインスタンスが返ってくる- https://github.com/mongodb/bson-ruby/blob/v4.3.0/lib/bson/document.rb#L35
- BSON::DocumentはHashを継承しているのでsliceはRailsアプリ内で使っているとActiveSupportの
Hash#slice
が呼ばれていた ※v4.3.0時点 - BSON::Documentは
#[]
をorverrideして、keyがSymbolでもStringでも取り出すことができるようになっていた - ActiveSupportの
Hash#slice
は#[]
を使っていた為、x.attributes.slice(:hoge)
でもx.attributes('hoge')
でも同じ挙動になっていた- と思っていたけどよく見ると、
#convert_key
定義されていたらconvert_key通してから#[]
呼んでた - どちらにせよ、最終的には
self[key]
してるので、#[]
が呼ばれている
- と思っていたけどよく見ると、
- 5.2でActiveSupportの
Hash#slice
の実装が変わった- https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8
- これによって、Ruby標準の
Hash#slice
が呼ばれるようになった
- Ruby標準の
Hash#slice
は#[]
が呼ばれないようでBSON::Document#[]
を通らない為、keyをSymbolで呼び出すと結果が取れなくなった
検証してみる
ActiveSupport5.1.4の Hash#slice
を使う場合
# Gemfile
source 'https://rubygems.org'
ruby '2.5.1'
gem 'activesupport', '5.1.4'
# hash_test.rb
require 'active_support/all'
class MyHash < Hash
def [](key)
puts "called with key: #{key.inspect}"
super(key)
end
end
h = MyHash[{hoge: 100 }]
puts h.slice(:hoge)
❯ bundle exec ruby hash_test.rb
called with key: :hoge
{:hoge=>100}
たしかに、 #[]
を通っている
ActiveSupport5.2.0で Hash#slice
を使う場合
ruby2.5.1でactive_support5.2にすると、Hash#sliceがdefindedなので、Ruby標準の方が使われる
なので、activesupportをrequireしなければいいはずだけど、一応5.2.0のactivesupportをrequireしてやってみる
# Gemfile
source 'https://rubygems.org'
ruby '2.5.1'
gem 'activesupport', '5.2.0'
# hash_test.rb
require 'active_support/all'
class MyHash < Hash
def [](key)
puts "called with key: #{key.inspect}"
super(key)
end
end
h = MyHash[{hoge: 100 }]
puts h.slice(:hoge)
❯ bundle exec ruby hash_test.rb
{:hoge=>100}
うん、#[]
通らなくなりましたね。
ActiveSupportをrequireしないでやってみる
# hash_test.rb
# require 'active_support/all'
class MyHash < Hash
def [](key)
puts "called with key: #{key.inspect}"
super(key)
end
end
h = MyHash[{hoge: 100 }]
puts h.slice(:hoge)
↑コメントアウトしてみました。
❯ bundle exec ruby hash_test.rb
{:hoge=>100}
やっぱり #[]
は通ってないっぽいですね。
bson-rubyのmasterでは既に修正されている
↑このコミットで slice
をorverrideする形で、元の挙動と同じになるように修正されていました。
@since 4.3.1
4.3.1には入るようですね。
※ 2018/04/28 17:25 現在では 4.3.0までしかリリースされていないので、まだ未リリースの様子
まとめ
-
Hash#slice
のおかげで勉強になった - そもそも、sliceの振る舞いとして
{"hoge” => 100 }
からh.slice(:hoge)
で取ろうとするのが間違っている気がしてきた- できるとたしかに便利ではあるんだけど
- SymbolとStringは別モンだよね、っていう方が明らかに正しい
- bsonの修正はまだリリースされていないので、一旦はattributesのsliceにSymbol渡してる所を全て文字列を使うように直して対応した