はじめまして! 昨年の 4 月に入社いたしました、Shinsuke Kido です。
弊社が提供するエンジニア教育プログラム「e-Navigator」を受けて、未経験からエンジニア転職いたしました!
現在、Rails に触れて、一年が経とうとしています。 はじめたての頃は、Rails は裏でいろいろと動いてくれていて、楽! というイメージで code を書いていましたが、 レベルアップするにつれ、その「裏」では一体何が動いているのかを、しっかりと理解する必要性を感じるようになってきました。
今回の記事は、Rails で当たり前のように使っているであろう、update
method について調べたことを書きました!
疑問に思ったこと
下記、Todo
model が存在すると仮定します ( Rails 5.2.0 )
# app/models/todo.rb class Todo < ApplicationRecord (中略) end
pry(main)> todo = Todo.first Todo Load (0.4ms) SELECT "todos".* FROM "todos" ORDER BY "todos"."id" ASC LIMIT $1 [["LIMIT", 1]] => #<Todo:0x00007feff4c9f0b8 id: "ca648b82-a36e-4f76-a98d-13380cdf913e", title: "abc", text: "text", created_at: Wed, 18 Jul 2018 05:10:53 UTC +00:00, updated_at: Tue, 19 Feb 2019 07:45:12 UTC +00:00> pry(main)> todo.title => "abc"
この、Todo
model に下記実装を施すと
# app/models/todo.rb class Todo < ApplicationRecord (中略) def title 'ABC' end (中略) end
pry(main)> todo.title => "ABC"
このように、#title
が override され、todo.title
の返り値が変更される
これは感覚的にも、よく分かるのですが、、、
下記実装を Todo
class に施すと、、、
#app/models/todo.rb class Todo < ApplicationRecord (中略) def title=(arg) self[:title] = 'CBA' end (中略) end
pry(main)> todo.update(title: 'ABC') (0.3ms) BEGIN Todo Update (0.5ms) UPDATE "todos" SET "title" = $1, "updated_at" = $2 WHERE "todos"."id" = $3 [["title", "CBA"], ["updated_at", "2019-02-19 08:08:01.666492"], ["id", "ca648b82-a36e-4f76-a98d-13380cdf913e"]] (0.6ms) COMMIT => true pry(main)> todo.title => "CBA"
#update
が override されてしまい、
title
の値が 'ABC'
ではなく、 'CBA'
になる、、、
これが感覚的に分からなかったので、一体 Active Record のどの method が override されているのかが、気になり、調べてみるとにしました!
調べた結果
まずは、下記、ActiveRecord の #update
が呼ばれ
# vendor/bundle/ruby/2.5.0/gems/activerecord-5.2.0/lib/active_record/persistence.rb module ActiveRecord # = Active Record \Persistence module Persistence extend ActiveSupport::Concern module ClassMethods (中略) def update(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do assign_attributes(attributes) save end end (中略) end
その後、いくつかの method を経由し、下記、public_send
までは、Todo
model での #update
の上書きの有無に関わらず、呼ばれているようでした。
# vendor/bundle/ruby/2.5.0/gems/activemodel-5.2.0/lib/active_model/attribute_assignment.rb module ActiveModel module AttributeAssignment include ActiveModel::ForbiddenAttributesProtection (中略) def _assign_attribute(k, v) setter = :"#{k}=" if respond_to?(setter) public_send(setter, v) else raise UnknownAttributeError.new(self, k) end end (中略) end
次に、#update
の上書きの有無による public_send
から呼ばれる method の分岐を調べるために、 binding.pry
を仕込んで試してみると、、、
⑴ Todo
model で #update
を上書きした時、
pry(main)> todo.update(title: 'ABC') (0.3ms) BEGIN From: /Users/shinsukekido/Simple-TODO-API/vendor/bundle/ruby/2.5.0/gems/activemodel-5.2.0/lib/active_model/attribute_assignment.rb @ line 52 ActiveModel::AttributeAssignment#_assign_attribute: 48: def _assign_attribute(k, v) 49: setter = :"#{k}=" 50: if respond_to?(setter) 51: binding.pry => 52: public_send(setter, v) 53: else 54: raise UnknownAttributeError.new(self, k) 55: end 56: end pry(#<Todo>)> step From: /Users/shinsukekido/Simple-TODO-API/app/models/todo.rb @ line 10 Todo#title=: 9: def title=(arg) => 10: self[:title] = 'CBA' 11: end
⑵ Todo
model で #update
を上書きしなかった時、
pry(main)> todo.update(title: 'ABC') (0.2ms) BEGIN From: /Users/shinsukekido/Simple-TODO-API/vendor/bundle/ruby/2.5.0/gems/activemodel-5.2.0/lib/active_model/attribute_assignment.rb @ line 52 ActiveModel::AttributeAssignment#_assign_attribute: 48: def _assign_attribute(k, v) 49: setter = :"#{k}=" 50: if respond_to?(setter) 51: binding.pry => 52: public_send(setter, v) 53: else 54: raise UnknownAttributeError.new(self, k) 55: end 56: end pry(#<Todo>)> step From: /Users/shinsukekido/Simple-TODO-API/vendor/bundle/ruby/2.5.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/write.rb @ line 22 self.__temp__479647c656=: 17: ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name 18: sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key 19: 20: generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 21: def __temp__#{safe_name}=(value) => 22: name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} 23: #{sync_with_transaction_state} 24: _write_attribute(name, value) 25: end 26: alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= 27: undef_method :__temp__#{safe_name}=
この結果から、
# vendor/bundle/ruby/2.5.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/write.rb module ActiveRecord module AttributeMethods module Write extend ActiveSupport::Concern (中略) def define_method_attribute=(name) safe_name = name.unpack("h*".freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__#{safe_name}=(value) name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} #{sync_with_transaction_state} _write_attribute(name, value) end alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= undef_method :__temp__#{safe_name}= STR end (中略) end
こちらの、def __temp__#{safe_name}=(value)
を、
Todo
class の def title=(arg)
が override することで、#update
を上書きしている、ということが分かりました!
ちなみに、Rails 5.2.2 では、こちらを override するようです。
最後に
今回、gem pry-byebug
が用意している step
コマンドを使うことで、最終的に method を特定することができました。
しかし、最初の段階から step
だけで先に進んでしまうと、あまり重要でない箇所で深く深くに沈んでしまい、目当ての method まで到達しない、というケースが起こりうるかと思います。
ですので、この method は飛ばしても大丈夫、と思ったら、next
コマンドを使い、この method が怪しい、と思ったら step
コマンドを使う、といった使い分けが大切なのかな、と感じました!
今後の debug に活かしていきたいと思います!!