17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsで"Model.new"が呼ばれるとどうなるか読解してみた

Posted at

railsメソッドがどのように定義されているのか学んでいく企画第3弾はモデルからオブジェクトを生成するnewメソッド(Rails APIリンク)です。

実際にはnewメソッドはRubyで定義されています。
ので、正確にはタイトルの通り、railsのActiveModelによって定義されるモデルのインスタンスをrubyで定義されているnewメソッドが呼び出して生成したときに何が起こるか、について見ていきます。

new
class Person
  include ActiveModel::Model
  attr_accessor :name, :age
end

person = Person.new(name: 'bob', age: '18')
person.name # => "bob"
person.age  # => "18"

#newメソッドが呼ばれたら?
newメソッドによりインスタンスが生成されると、ActiveModel::Modelのinitializeメソッドが動作します。

newメソッドの定義
def initialize(attributes = {})
  assign_attributes(attributes) if attributes

  super()
end

非常にシンプルですね。

1行目
def initialize(attributes = {})

attributesにはハッシュが引き渡されます。
何も渡されない場合は空のハッシュ{}が渡されます。

2行目
assign_attributes(attributes) if attributes

冒頭のPerson.new(name: 'bob', age: '18')のようにattributesに値が渡されている場合はassign_attributesメソッドが呼び出されます。

#assign_attributesメソッド

assign_attributesメソッド
# File activemodel/lib/active_model/attribute_assignment.rb, line 26
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.nil? || new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

プロなりたての私からすると、相変わらずインパクトある見た目してます^^;
目をそらさずに読んでいきましょう。

1行目は引数が渡されているだけなので飛ばします。

2行目
if !new_attributes.respond_to?(:stringify_keys)

条件式を見てみましょう。
new_attributesにはハッシュが渡されています。
respond_to?メソッドにはシンボル:stringify_keysが渡されています。

さっそくrespond_to?メソッドについても読んでいきましょう。

#respond_to?メソッド

respond_to?メソッド
# File activemodel/lib/active_model/attribute_methods.rb, line 448
def respond_to?(method, include_private_methods = false)
  if super
    true
  elsif !include_private_methods && super(method, true)
    # If we're here then we haven't found among non-private methods
    # but found among all methods. Which means that the given method is private.
    false
  else
    !matched_attribute_method(method.to_s).nil?
  end
end

newメソッドは表面的には4行しかありませんが、裏に何行ものコードが隠れていますね。

1行目
def respond_to?(method, include_private_methods = false)

methodには先程述べた通り、:stringify_keysというシンボルが渡されています。
第2引数には何も渡されていないので、定義通りfalseが代入されます。

2行目
if super

if文の中にsuperメソッドが書かれています。superメソッドについて見ていきましょう。

かなりややこしいですが、親クラスのメソッドの中から同名のメソッドを呼び出す特殊なメソッドのようです。

superの使用例
class Car
  def accele
    print("アクセルを踏みました¥n")
  end
end

class Soarer < Car
  def accele
    super
    print("加速しました¥n")
  end
end

(引用)

つまりrepond_to?メソッドが定義されているクラスの親クラスにある同名のメソッドの中身を見ることが、このコードの意味を理解する役に立ちそうです。

respond_to?メソッドが定義されているのはAttributeMethodsモジュール。

モジュールに関してもクラスと同様に継承関係が生まれるようです。

class Example
  include ActiveModel::AttributeMethods
end

この場合は

Example < ActiveModel::AttributeMethods

の関係となります。
include(含む)という意味から生まれる感覚からすると逆が直感的と思いますが、違うのですね。

class Example
  prepend ActiveModel::AttributeMethods
end

この場合は

ActiveModel::AttributeMethods < Example

となります。

したがってsuperメソッドにより親モデルのメソッドを呼び出す、と考えるとどこかに後者のprependによる宣言が書かれていることが推測されます。

しかしながらprependにより明示的に、モジュールを呼び出している箇所がRailsライブラリ内に含まれていませんでした。

そこでRailsアプリ上で私が定義したモデルに対して、継承関係のうち祖先を返すメソッドであるancestorsメソッドを用いました。

ancestorsメソッドによる継承関係の出力
=> [OriginalModel(id: integer, created_at: datetime, updated_at: datetime),
 OriginalModel::GeneratedAttributeMethods,
 # 省略
 ActiveRecord::AttributeMethods,
 ActiveModel::AttributeMethods,
 ActiveModel::Validations::Callbacks,
 # 省略
 Object,
 # 省略
 Kernel,
 BasicObject]

2つ目のブロックの2段めに目的のActiveModel::AttributeMethodsがありますね。
しかしその親にあたるActiveModel::Validations::Callbacksにはモジュールの使用は宣言されていません。

この場合はancestorsメソッドで示された順に、そのクラス/モジュール内にメソッドが定義されていないか確認していきます。

親をたどっていくとようやく見つけました。3つ目のブロックObjectモデルはrespond_to?メソッドを定義しています。

ここで定義されているrespond_to?メソッドはオブジェクトがrespond_to?("・・・")のように渡された引数"・・・"の名前を持つメソッドを持っているか?ということを判定するメソッドです。

ちなみにObject.respond_to?(:respond_to?)はtrueを返します。

superメソッドに引数が明記されていない場合は、呼び出し元のメソッドの引数がそのまま渡されます。

したがってここでは、respond_to?(:stringify_keys)が動いていることになります。

stringify_keysメソッドはハッシュに対して定義されるため、正しく定義したモデルであれば、必ずtrueになりそうです。

ぶっちゃけrespond_to?メソッドでググれば、わかった内容ではありますが、moduleと継承のことを学べたので良しとしましょう。

さて、もとのassign_attributesメソッドに戻りましょう。

assign_attributesメソッド
# File activemodel/lib/active_model/attribute_assignment.rb, line 26
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.nil? || new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

二行目のif条件のうち、new_attributes.respond_to?(~)はnew_attributesがハッシュであればtrue、ハッシュお外であればfalseとなります。

!が先頭についているので全体としては、ハッシュでない場合はifの内容に進み、AugumentErrorを発する、ということになります。

メッセージもそのままですね。
「ハッシュいれてね。」

5行目
return if new_attributes.nil? || new_attributes.empty?

次に代入された属性に対してnil?とempty?のチェックがなされます。
該当する場合は、assign_attributesメソッドからは何も返されません。

ちなみにnil?メソッドはレシーバ(この場合はnew_attributes
)がnilのときにtrueを返し、empty?メソッドはレシーバの長さが0のときにtrueを返します。

6行目
attributes = new_attributes.stringify_keys

ここではstringify_keysによってハッシュnew_attributesのキーがシンボル形式から文字列形式に変換されます。

7行目
_assign_attributes(sanitize_for_mass_assignment(attributes))

_assign_attributesメソッドという、微妙に名前が違うメソッドが呼び出されます。
※ちなみに先頭のアンダースコアは、Rubyの慣習でプライベートメソッドの名前の先頭につけるものです。

またその引数にはsanitize_for_mass_assignmentにattributesが代入された状態で呼び出されています。

ますはsanitize_for_mass_assignmentについて見てみましょう。

sanitize_for_mass_assignmentメソッド
# File activemodel/lib/active_model/forbidden_attributes_protection.rb, line 21
      def sanitize_for_mass_assignment(attributes)
        if attributes.respond_to?(:permitted?)
          raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
          attributes.to_h
        else
          attributes
        end
      end

2行目のif条件を見てみましょう。
ここではrespond_to?メソッドが呼び出されます。

このメソッドはattributesがpermitted?メソッドが定義されたメソッドであるかを確認しています。

permitted?メソッドはActiveControllerのストロングパラメータモジュールに定義されたメソッドで、ストロングパラメータが通されてpermit: trueとなっているかを判定するメソッドです。

したがってparamsなどコントローラ上で生成された値がattributesに代入されている場合は1つ目のif条件が満たされます。

3行目
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?

ここでストロングパラメータを通しているか?permitted?メソッドで判定します。
許可されていない場合はエラー発生。

4行目
attributes.to_h

この行によって、ストロングパラメータに許可された値に限り、ActiveControllerクラスから"ただの"ハッシュが返されます。

assign_attributesメソッドに戻ります。

(再掲)

assign_attributesメソッド7行目
_assign_attributes(sanitize_for_mass_assignment(attributes))

sanitize_for_mass_assignment(attributes)によりハッシュが_assign_attributesメソッドに代入されることがわかりました。

_assign_attributesメソッド
# File activerecord/lib/active_record/attribute_assignment.rb, line 12
      def _assign_attributes(attributes)
        multi_parameter_attributes  = {}
        nested_parameter_attributes = {}

        attributes.each do |k, v|
          if k.include?("(")
            multi_parameter_attributes[k] = attributes.delete(k)
          elsif v.is_a?(Hash)
            nested_parameter_attributes[k] = attributes.delete(k)
          end
        end
        super(attributes)

        assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
        assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
      end

4行目にeach文があります。

このattributesは必ずハッシュであるため、キー+値ごとにeach文の内容が実行されます。
kはハッシュのキー、vはバリュー=値を意味します。

1つ目のif条件については、用途不明です。
multi_parameter属性というものがあるらしい?

2つ目のif条件についてはハッシュの値にさらにハッシュが格納されていた場合にtrueとなります。
(is_a?(~)はレシーバが~であるかどうか判定するメソッドですね。今回はハッシュであればtrueを返します)

これはOwnerとCarのように、親子関係のアソシエーションにある2つのモデルを同時に扱う際に、親の属性を格納したハッシュの中に、子の属性を格納したハッシュをネスト(入れ子に)するケースを扱うためのものと考えられます。

このとき、nested_parameter_attributes[k]にネストされたハッシュが格納されます。

(これまでの読解企画にも同上しているように右辺のdeleteはカッコ内のキーに対応する値を返すために用いられていると推測されます)

これでmulti_parameterとnested_parameterに値が格納されました。

11行目
super(attributes)

親クラスの同名メソッド(_assign_attributesメソッド)を参照します。

継承関係

 ActiveRecord::AttributeAssignment,
 ActiveModel::AttributeAssignment,
 ## 略
 Object,
 ## 略

_assign_attributesメソッドが定義されているactive_record/attribute_assignment.rbにおいて

module ActiveRecord
  module AttributeAssignment
    include ActiveModel::AttributeAssignment

    private
      def _assign_attributes(attributes)
      ## 略
      end

のようにActiveRecord::AttributeAssignmentはActiveModel::AttributesAssignmentをincludeしているので、superが参照するのはActiveModelに定義された_assign_attributesメソッドです。

def _assign_attributes(attributes)
  attributes.each do |k, v|
    _assign_attribute(k, v)
  end
end

def _assign_attribute(k, v)
  setter = :"#{k}="
  if respond_to?(setter)
    public_send(setter, v)
  else
    raise UnknownAttributeError.new(self, k.to_s)
  end
end

_assign_attributesメソッド内ではハッシュの各属性について_assign_attributeメソッドが呼び出されます。
(複数形と単数形の違いがありますよ)

7行目でsetterと名の通り、セッター名称を代入します。
セッター、ゲッターについてはこちらを初心者向けのとしてリンク貼っておきます。
xxx=という名称はRubyにおいてセッターを定義するメソッドの命名パターンですね。
※xxxにはモデルに定義した属性の名称が入ります。

8行目のif条件ではrespond_to?メソッドでsetterに代入された名称のセッターが定義されているか確認します。
存在する場合はpublic_sendメソッドを呼び出し。

public_sendメソッドはRubyリファレンスによるとObjectクラスに定義されたメソッドで、第一引数に渡された名称のメソッドに第2引数に渡された値を引き渡して実行します。

セッターメソッドはモデルの当該属性に値を代入するメソッド。
_assign_attributes(複数形の方ですよ)の引数は、最初にnewメソッドを呼び出したときに代入した値をハッシュに変換したもの。

例えばUserというモデルとその属性として、name、ageを定義していたならUser.new(name: John, age: 20)のように名前と年齢を代入しているかと思います

つまり各属性に指定した値を代入するという処理が行われています。

本来であればUser.name = Johnのように指定しないといけなかったところを、代わりにやってくれているのですね。

ちなみにif文でのrespond_to?メソッドによる確認の結果、定義されていない属性の場合は、

11行目
  raise UnknownAttributeError.new(self, k.to_s)

そんな属性知らないよエラーが返ります。
ここ、newメソッドが呼び出されているの面白いですね。

UnknownAttributeErrorモデル
class UnknownAttributeError < NoMethodError
  attr_reader :record, :attribute

  def initialize(record, attribute)
    @record = record
    @attribute = attribute
    super("unknown attribute '#{attribute}' for #{@record.class}.")
  end
end

のように定義されており、属性は2つrecordとattributeで、なおかつselfとk.to_sの2つ渡されているので、
11行目でnewメソッドが呼び出された場合、同じ11行目の分岐に入ることはないので、無限ループに陥ることはなさそうです。

なおエラーメッセージは

unknown attribute '#{キー名称}' for #{モデル名称}.

のように返すと読み取れます。

結局の所、ActiveModelに定義された_assign_attributesメソッドによってセッターが代入された属性の数分呼び出されるのがnewメソッドの肝でしたね^^;

これ以上は蛇足感が否めませんがせっかくなので、ActiveRecordに定義された_assign_attributesモデルの読解に戻ってみましょう。

_assign_attributesメソッド
# File activerecord/lib/active_record/attribute_assignment.rb, line 12
      def _assign_attributes(attributes)
        multi_parameter_attributes  = {}
        nested_parameter_attributes = {}

        attributes.each do |k, v|
          if k.include?("(")
            multi_parameter_attributes[k] = attributes.delete(k)
          elsif v.is_a?(Hash)
            nested_parameter_attributes[k] = attributes.delete(k)
          end
        end
        super(attributes)

        assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
        assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
      end

11行目のsuperメソッドのところをここまでで読みました。
現状は謎のmulti_parameter_attributesとモデルをネストしたときのためのnested_parameter_attributesに値が代入された上で、newによって生成したいインスタンスの各属性に対して
値が代入された状態です。

12行目のメソッドについても見てみましょう

assign_nested_parameter_attributes
def assign_nested_parameter_attributes(pairs)
  pairs.each { |k, v| _assign_attribute(k, v) }
end

何のことはありません。
最初にnewメソッドが呼び出された際に{name: John, age: 20, car_attributes: {color: red, type: sportscar } }
のように渡されていた部分から抜き出された{color: red, type: sportscar}がこのメソッドの引数として渡されています。
これらの属性に定義されたセッターメソッドを繰り返し呼び出しているだけです。

assign_multiparameter_attributesメソッドの定義についても読解すれば、multiparameterとやらが何を実現するためのものかわかりそうですが、聞いたことがない以上使われていないものと思われるので、今回は省略します。

assign_attributesメソッド
# File activemodel/lib/active_model/attribute_assignment.rb, line 26
def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
  end
  return if new_attributes.nil? || new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

これで最終行の_assign_attributesメソッドの動作がわかりました。

newメソッドの定義
def initialize(attributes = {})
  assign_attributes(attributes) if attributes

  super()
end

ようやく、もとのnewメソッドに戻ってきました。

残りはsuperのみですね。
superメソッドは()なしで呼ぶと、引数をそのまま、superにより呼び出されるメソッドに渡します。
ここでいうとattributesです。

super()の場合は引数なしで呼び出されます。
親モデルのinitializeが呼び出されることで、親モデルの属性がインスタンスの属性として定義されるのかと思います。
引数が渡されていないのでどういう動きになるのか少々疑問ですが、省略します。

以上です。

まとめ

newメソッドにより、モデルが生成されると、ActiveModelに定義されたinitializeにより、assign_attributesメソッドが呼ばれ、newメソッドに渡されたハッシュを分解して、属性ごとにセッターメソッドを呼び出す。

生Rubyだといちいち値を代入しなければならないところ、railsでは勝手にやってくれるのですね。
便利だな〜。

今回新たに生まれた疑問としては、じゃあ生Rubyのnewメソッド自体はどう定義されているのか、インスタンスを生成した際にinitializeメソッドが呼び出される仕組み、今度はsaveメソッドでレコードが保存される仕組み、が気になりますね。

今後の課題としましょう。

17
10
0

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
  3. You can use dark theme
What you can do with signing up
17
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?