Hash#sliceとActiveRecord::Core#sliceの違い
概要
RailsではActiveRecord::BaseあるいはApplicationRecordを継承したクラスのインスタンスメソッドとしてslice
というメソッドを使用することができます。
Fruit.first.slice(:name)
Fruit Load (0.1ms) SELECT "fruits".* FROM "fruits" ORDER BY "fruits"."id" ASC LIMIT ? [["LIMIT", 1]]
hello
=> {"name"=>"organge"}
一方でこれとは別にRubyのHash#slice
が存在しています。
{name: 'tarou', age: 20}.slice(:name)
=> {:name=>"tarou"}
一見似たような処理をしてそうに思えますがレシーバのクラスが異なる上、例えばモデルのインスタンスに対してテーブルに存在しないカラム名を引数として渡すとエラーとなります。
Fruit.first.slice(:hogehoge)
#=> `method_missing': undefined method `hogehoge' for #<Fruit id: 1, name: nil, color: "red", created_at: "2022-09-05 10:15:41.670542000 +0000", updated_at: "2022-09-05 10:15:41.670542000 +0000"> (NoMethodError)
そこで今回はソースコードを辿ることでメソッド内で行われている処理を確認します。
ActiveRecord::Core#slice
前半としてmethods.flatten
の箇所を見ていきましょう。
まず、複数の値をmethods
という引数に配列として受け取っています。また、Array#flatten
で配列を平坦化しています。
[1, [2, 3, [4], 5]].flatten
=> [1, 2, 3, 4, 5]
よって以下の二つの呼び出し方が可能になります。
Fruit.first.slice(:name, :color)
# 引数methodsには[[:name, :color]]として渡るが平坦化されるので上と同じ
Fruit.first.slice([:name, :color])
続いて後半で、index_with { |method| public_send(method) }
の箇所を見ていきます。
Enumerable#index_with
はRailsの拡張メソッドです。各要素をキーとして、指定した引数やブロックを評価した結果を値とするHashを生成します。
# 引数に指定した値を値とするHashを作る
[1,2].index_with(10)
=> {1=>10, 2=>10}
# 評価されたブロックの結果を値とするHashを作る
['a', 'b'].index_with{ |str| str.upcase }
=> {"a"=>"A", "b"=>"B"}
# name, colorというカラム(=publicメソッド)を持つFruitモデル
['name', 'color'].index_with{ |attr_name| Fruit.first.public_send(attr_name) }
=> {"name"=>"apple", "color"=>"red"}
最後のwith_indifferent_access
はHashを拡張したHashWithIndifferentAccess
に変換するメソッドです。定義時のキーがシンボルであっても文字列で呼び出したりすることが可能になるなどの機能を提供します。
hash = { name: 'tarou' }
hash.class #=> Hash
hash['name'] #=> nil
hash[:name] #=> tarou
hash = hash.with_indifferent_access
hash.class #=> => ActiveSupport::HashWithIndifferentAccess
# シンボル・文字列どっちでも呼び出せる
hash['name'] #=> "tarou"
hash[:name] #=> "tarou"
これらより、ActiveRecord::BaseあるいはApplicationRecordを継承したクラスのインスタンスに対してsliceを呼び出すと引数と同名のpublicメソッドの結果を値としたActiveSupport::HashWithIndifferentAccess
のインスタンスを返すことがわかりました。
補足
一般的な使用方法としては、カラム名を引数とすることで必要なカラム名と値を抽出することが多いと思いますが、原理的にはモデル内で定義されたpublicメソッドであれば引数にして呼び出すことが可能です。
class Fruit < ApplicationRecord
def hoge
'hogehoge'
end
end
Fruit.first.slice(:hoge)
#=> {"hoge"=>"hogehoge"}
Discussion