【Rails】ActiveModelのsliceは賢い!…でもキーの型には要注意
こんにちは!
最近フルサイクルエンジニアになるべくフロントエンドの勉強を始めた @ourly_nobuo です!
Railsで開発していると、 Post.find('uuid')
のようにデータベースから取得したActiveRecordオブジェクトを扱う場面が頻繁にあります。このオブジェクトから一部のプロパティだけを抜き出したい時、ActiveRecordがincludeしているActiveModelの slice
メソッドは非常に便利です。
しかし、この slice
メソッド、実は私たちが思っているよりも少し賢く、そして少しトリッキーな一面を持っています。この記事では、ActiveModelオブジェクトに対する slice
の挙動と、そこに潜む「静かな罠」について共有します!
slice
は気が利く
前提: ActiveModel の slice
メソッドは、ActiveModelに定義されているインスタンスメソッドです。
実際に挙動を試してみると、以下のように動作します。
post = Post.find_by(title: "Railsのコツ")
# :title, :view_countキーでsliceを実行
sliced_post = post.slice(:title, :view_count)
# 試してみると、StringでもSymbolでもどちらでも値にアクセスできる!
puts sliced_post['title']
#=> "Railsのコツ"
puts sliced_post[:title]
#=> "Railsのコツ"
# クラスを確認すると、その理由がわかる
puts sliced_post.class
#=> ActiveSupport::HashWithIndifferentAccess
ActiveModelオブジェクトのsliceは、気を利かせて ActiveSupport::HashWithIndifferentAccess を返してくれるのです。これにより、 slice
した後もキーがStringかSymbolかを気にすることなく値にアクセスできます。非常に便利ですね。
ちなみに補足として、HashWithIndifferentAccessの内部処理としては、Symbolが渡ってきた場合に、Stringに変換するというシンプルなことをしています。
本当の罠: キーの型はStringだった
これで万事解決、便利な slice
をどんどん使っていこう!…と言いたいところですが、ここに静かな罠が潜んでいます。
この罠は、値にアクセスしているだけでは現れません。キーそのものを扱おうとした時に初めて姿を表します。
例えば、特定のキー(例::view_count)だったら特別な処理をしたい、というコードを見てみましょう。
post = Post.find_by(title: "Railsのコツ")
sliced_post = post.slice(:title, :view_count)
TARGET_METRICS_KEYS = [:view_count]
sliced_post.each do |key, value|
if TARGET_METRICS_KEYS.include?(key) # `[:view_count].include?("view_count")` となり、falseになる
# この中の処理は永遠に実行されない…
puts "【特別指標】#{key}: #{value}"
else
puts "#{key}: #{value}"
end
end
#実行結果
# => title: Railsのコツ
# => view_count: 580
:view_countは特別扱いされず、他のキーと同じように出力されてしまいました。
原因は、HashWithIndifferentAccessの内部仕様にあります。このクラスは、キーをString/Symbolのどちらで受け取っても、内部ではすべて文字列 (String) に統一して保持します。
そのため、 .each
や .keys
でキーを取り出すと、その型は常にStringです。Symbolの配列と比較しても、型が違うため一致することはなく、バグは静かに潜伏し続けます。
解決策
原因が分かれば解決は簡単です。キーの型を合わせてあげましょう。
TARGET_METRICS_KEYS = [:view_count]
sliced_post.each do |key, value|
# 比較する際に、文字列のキーをシンボルに変換する
if TARGET_METRICS_KEYS.include?(key.to_sym)
puts "【特別指標】#{key}: #{value}"
else
puts "#{key}: #{value}"
end
end
# 修正後の実行結果
# => title: Railsのコツ
# => 【特別指標】view_count: 580
上記かもしくは比較したいキーをあらかじめStringで定義しておくのも有効でしょう。
TARGET_METRICS_KEYS = ['view_count']
これで意図通りの動作になりました👏
まとめ
ここまでお読みいただきありがとうございました!!
ActiveModelの slice
は便利です!
返り値はHashWithIndifferentAccessなので、String/Symbolどちらでも値にアクセスできます。
しかしキーの型には要注意!
キー自体を扱う( .keys
で取り出す、比較する等)場合、その型はStringです。
この 「値の参照ではString/Symbolを区別しないが、キー自体の型はStringに統一される」 という特性を理解しておくことで、見つけにくい静かなバグを未然に防ぐことができます。便利なRailsの機能を、より深く理解して使いこなしていきましょう。
Discussion