railsメソッドがどのように定義されているのか学んでいく企画第3弾はモデルからオブジェクトを生成するnewメソッド(Rails APIリンク)です。
実際にはnewメソッドはRubyで定義されています。
ので、正確にはタイトルの通り、railsのActiveModelによって定義されるモデルのインスタンスをrubyで定義されている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メソッドが動作します。
def initialize(attributes = {})
assign_attributes(attributes) if attributes
super()
end
非常にシンプルですね。
def initialize(attributes = {})
attributesにはハッシュが引き渡されます。
何も渡されない場合は空のハッシュ{}が渡されます。
assign_attributes(attributes) if attributes
冒頭のPerson.new(name: 'bob', age: '18')のように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行目は引数が渡されているだけなので飛ばします。
if !new_attributes.respond_to?(:stringify_keys)
条件式を見てみましょう。
new_attributesにはハッシュが渡されています。
respond_to?メソッドにはシンボル:stringify_keysが渡されています。
さっそく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行しかありませんが、裏に何行ものコードが隠れていますね。
def respond_to?(method, include_private_methods = false)
methodには先程述べた通り、:stringify_keysというシンボルが渡されています。
第2引数には何も渡されていないので、定義通りfalseが代入されます。
if super
if文の中に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メソッドを用いました。
=> [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メソッドに戻りましょう。
# 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を発する、ということになります。
メッセージもそのままですね。
「ハッシュいれてね。」
return if new_attributes.nil? || new_attributes.empty?
次に代入された属性に対してnil?とempty?のチェックがなされます。
該当する場合は、assign_attributesメソッドからは何も返されません。
ちなみにnil?メソッドはレシーバ(この場合はnew_attributes
)がnilのときにtrueを返し、empty?メソッドはレシーバの長さが0のときにtrueを返します。
attributes = new_attributes.stringify_keys
ここではstringify_keysによってハッシュnew_attributesのキーがシンボル形式から文字列形式に変換されます。
_assign_attributes(sanitize_for_mass_assignment(attributes))
_assign_attributesメソッドという、微妙に名前が違うメソッドが呼び出されます。
※ちなみに先頭のアンダースコアは、Rubyの慣習でプライベートメソッドの名前の先頭につけるものです。
またその引数にはsanitize_for_mass_assignmentにattributesが代入された状態で呼び出されています。
ますは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条件が満たされます。
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
ここでストロングパラメータを通しているか?permitted?メソッドで判定します。
許可されていない場合はエラー発生。
attributes.to_h
この行によって、ストロングパラメータに許可された値に限り、ActiveControllerクラスから"ただの"ハッシュが返されます。
assign_attributesメソッドに戻ります。
(再掲)
_assign_attributes(sanitize_for_mass_assignment(attributes))
sanitize_for_mass_assignment(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に値が格納されました。
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?メソッドによる確認の結果、定義されていない属性の場合は、
raise UnknownAttributeError.new(self, k.to_s)
そんな属性知らないよエラーが返ります。
ここ、newメソッドが呼び出されているの面白いですね。
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モデルの読解に戻ってみましょう。
# 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行目のメソッドについても見てみましょう
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とやらが何を実現するためのものかわかりそうですが、聞いたことがない以上使われていないものと思われるので、今回は省略します。
# 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メソッドの動作がわかりました。
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メソッドでレコードが保存される仕組み、が気になりますね。
今後の課題としましょう。