新規・更新を条件分岐できるpersisted?のソースコードを読んでみた

環境やバージョン

$rails -v
Rails 5.2.3
$ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin18]

ソースコードリーディング

Returns true if the record is persisted, i.e. it's not a new record and it was not destroyed, otherwise returns false.

persisted?メソッドは,オブジェクトが新しいレコードでない,かつオブジェクトが削除されていない時に,Trueを返すメソッドだ.

この性質から,Viewのnewとeditにて,共通のformパーシャルを切り出した際に,分岐条件として利用される.

teratail.com

persisted?の実装を追うことで理解を深める.

bundle/ruby/2.5.0/gems/activerecord-5.2.3/lib/active_record/persistence.rbを確認すると,persisted?メソッドは,以下のような実装になっている.

def persisted?
  sync_with_transaction_state
  !(@new_record || @destroyed)
end

これは,ド・モルガンの法則より,!(P || Q) == !P && !Qとなる.

つまり,@new_recordでない,かつ,@destroyedでないということだ.これは,冒頭での説明文と一致する.

まずは,sync_with_transaction_stateについて調べてみる.

privateメソッドであるsync_with_transaction_stateは,transactions.rbに定義されている.

ここで,@transaction_stateは,core.rbにてnilで初期化されており,update_attributes_from_transaction_stateメソッドでの処理は,特に行われないことがわかる.

def sync_with_transaction_state
  update_attributes_from_transaction_state(@transaction_state)
end

def update_attributes_from_transaction_state(transaction_state)
  if transaction_state && transaction_state.finalized?
    restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
    force_clear_transaction_record_state if transaction_state.fully_committed?
    clear_transaction_record_state if transaction_state.fully_completed?
  end
end

次に,persisted?メソッド内の,@new_record@destroyedのインスタンス変数は,どこで定義されているのだろうか?

user = User.new(name: "persist")
user.persisted?

上記のように,ユーザーオブジェクトを生成した時,bundle/ruby/2.5.0/gems/activerecord-5.2.3/lib/active_record/core.rbのinitializeメソッドにて,init_internalsが呼び出される.init_internalsは,見てわかるように該当する2つのインスタンス変数の初期化を行なっている.

def initialize(attributes = nil)
  self.class.define_attribute_methods
  @attributes = self.class._default_attributes.deep_dup

  init_internals

  initialize_internals_callback

  assign_attributes(attributes) if attributes

  yield self if block_given?
  _run_initialize_callbacks
end

~~中略~~

def init_internals
  @readonly                 = false
  @destroyed                = false // 注目するべきインスタンス変数
  @marked_for_destruction   = false
  @destroyed_by_association = nil
  @new_record               = true // 注目するべきインスタンス変数
  @_start_transaction_state = {}
  @transaction_state        = nil
end

まとめ

  • persisted?は,二行のシンプルな構成となっている.
  • persisted?は,@new_record@destroyedの値を元に,booleanを返すことがわかった.
  • editアクションでは,既にデータベースに保存したオブジェクトを参照するため,@new_recordではないためpersisted?は,falseになる.

感想

OSSは,怖くない.

余談だが,初期段階で,rails newしたActiveRecordとRailsドキュメントからのGitHubリンク先の実装が異なっていたので,時間を浪費した.ドキュメントのリンクは,旧versionの実装であったためだ.OSSは,頻繁にコードが変更するため最新のコードを確認することが重要であることを学んだ.