【Rails】update メソッドのオーバーライドについて

# はじめに

下記、`Todo` model があると仮定します (Rails 5.2.0)

```app/models/todo.rb
class Todo < ApplicationRecord

(中略）

end
```

```ruby
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 に下記実装を施すと

```app/models/todo.rb
class Todo < ApplicationRecord

(中略）

  def title
    'ABC'
  end

(中略)

end
```
```ruby
pry(main)> todo.title
=> "ABC"
```
このように、`#title` が override され、`todo.title` の返り値が変更される  
これは感覚的にも、よく分かるのですが、、、

# #update のオーバーライド

下記実装を `Todo` class に施すと、、、

```app/models/todo.rb
class Todo < ApplicationRecord

(中略）

  def title=(arg)
    self[:title] = 'CBA'
  end

(中略)

end
```
```ruby
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` を上書きした時、

```ruby
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` を上書きしなかった時、

```ruby

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 では、[こちら](https://github.com/rails/rails/blob/v5.2.2/activerecord/lib/active_record/attribute_methods/write.rb#L21)を override するようです。