はじめに
下記、Todo
model があると仮定します (Rails 5.2.0)
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"
#title のオーバーライド
この、Todo
model に下記実装を施すと
class Todo < ApplicationRecord
(中略)
def title
'ABC'
end
(中略)
end
pry(main)> todo.title
=> "ABC"
このように、#title
が override され、todo.title
の返り値が変更される
これは感覚的にも、よく分かるのですが、、、
#update のオーバーライド
下記実装を Todo
class に施すと、、、
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
が呼ばれ
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
の上書きの有無に関わらず、呼ばれているようでした。
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}=
この結果から、
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 するようです。