🦧

Hash#sliceとActiveRecord::Core#sliceの違い

2022/09/13に公開

概要

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

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/activerecord/lib/active_record/core.rb#L729-L731

前半として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"}
GitHubで編集を提案

Discussion