LoginSignup
2
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-21

はじめに

下記、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"

#title のオーバーライド

この、Todo model に下記実装を施すと

app/models/todo.rb
class Todo < ApplicationRecord

(中略)

  def title
    'ABC'
  end

(中略)

end
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
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 するようです。

2
1
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
2
1