Ruby on Railsアクティブレコード更新メソッドの比較

Posted by Kohei Hayashi
on

Ruby on RailsのActiveRecord::Persistenceモジュールにはアクティブレコード更新のために様々なメソッドが用意されていますが、似たような名前のメソッドが多く、それぞれの違いがわからなかったり、どれを使うのが良いのか迷ったりすることがあります。
そこで、更新メソッドの違いを比較することで、それぞれをどのような場面で使うのが適切なのかを見ていきたいと思います。

update

更新メソッドの中で最も頻繁に使うのはupdateでしょう。
@thing.update(thing_params)のようにコントローラーのupdateアクションで使うのが一般的です。
モデルのバリデーション、コールバック共に実行されます。
もしバリデーションでエラーになった場合はfalse、データベースに無事に保存されればtrueが戻り値になりますので、if..elseで使うのが適しています。

      
class ThingsController < ApplicationController
  def update
    if @thing.update thing_params
      redirect_to @thing
    else
      render 'edit'
    end
  end
end
      
    

update!

update!ではupdateと同じくバリデーション、コールバック共に実行されます。但しバリデーションがエラーになった場合は、例外となります。

      
def update
  @thing.update! thing_params
  redirect_to @thing

rescue => e 
    render 'edit'
end
      
    

updateではなくupdate!を使用すべき場面としては、複数の異なるモデルの保存を一括で行わなければならず、どれか一つでもモデル保存がエラーになったら例外を投げてデータベーストランザクションをロールバックしたい時が当てはまります。

      
def update
  @thing.transaction do
    @thing.update!
    @foo.update!
  end
  redirect_to @thing
rescue
  render 'edit'
end
      
    

同じコードをupdateを使って書くと以下のように自身で例外を起こさなければなりません。

      
def update
  @thing.transaction do
    raise ActiveRecord::RecordInvalid.new 'thing failed' unless @thing.update
    raise ActiveRecord::RecordInvalid.new 'foo failed' unless @foo.update
  end
  redirect_to @thing
rescue ActiveRecord::RecordInvalid => e
  render 'edit'
end
      
    

update_attribute

update_attributeは1つのモデルattributeのみを更新します。バリデーションはスキップされますが、コールバックは実行されます。
使用例としては、データベースレコードの論理削除などがあります。バリデーションを気にせず確実にdeleted_atにタイムスタンプを記録した後、コールバックで処理完了の通知を送りたい場合などです。
updateを使うとバリデーションで引っかかってしまうリスクがありますし、後述のupdate_columnだとコールバックが呼ばれません。ソースコードの裏側ではsave(validation: false)が実行されます。なので、もしメソッド実行以前に他のattributeがdirtyになっている場合は一緒に更新されてしまうので、注意が必要です。

      
@user.update_attribute deleted_at: Time.current
      
    

update_attributes

update_attributesは、一見update_attributeの複数形なので、複数のattributesをバリデーションなしで更新するためのメソッドに見えますが、実はupdateのエイリアスです。updateとどちらを使うかはエンジニアの好み、もしくは文脈により意図がよりはっきり伝わる方を選べば良いかと思います。

save

saveでupdateと同じことをしようとすると、以下のようにアサインと保存を分けることになります。実際、updateのソースコードではassign_attributesとsaveが呼ばれています。

      
@thing.assign_attributes thing_params
@thing.save
      
    

通常はupdateを使って一行で済ませますが、アサインメントと保存の間に処理を加えたい場合は、saveを使います。例えば@thingにparamsをアサイン後、@thingインスタンスのプロパティ値を変更したりする場合です。

      
@thing.assign_attributes thing_params
@thing.modify_some_attributes
@thing.save
      
    

バリデーションエラーやコールバックでの例外が起こった場合はfalse, データベースで無事に更新された場合はtrueが戻り値になります。

save!

save!はsaveと似ていますが、バリデーションエラーやコールバック中に例外が起こると、例外が起こります。もしエラーがない場合、trueを返します。

      
def update
  @thing.transaction do
    @thing.assign_attributes thing_params
    @thing.modify_some_attributes
    @thing.save!
    @foo.save!
  end
  redirect_to @thing
rescue
  render 'edit'
end
      
    

toggle

toggleはブーリアン型のattributeを反対の値にします。つまり、元の値がtrueだったらfalse, falseならtrueになります。データベースへの保存は行われず、オブジェクトメモリーの値が変化します。
戻り値はselfのため、メソッドチェインが可能です。

      
def toggle_enabled
  if @thing.toggle(:enabled).save
    redirect_to @thing
  else
    render 'edit'
  end
end
      
    

toggle!

toggle!はtoggleと同じようにブーリアン型のattributeを反対の値にしますが、データベースへの保存がされます。ソースコードでは、toggleとupdate_attributeのメソッドチェインとなっています。ゆえに、戻り値もselfではなくブーリアンとなります。

      
def toggle_enabled
  if @thing.toggle!(:enabled)
    redirect_to @thing
  else
    render 'edit'
  end
end
      
    

touch

touchはupdated_at/updated_on、及び任意のタイムスタンプ・カラムを現在時刻で更新します。
after_touch, after_commit/after_rollbackののコールバックしか実行されないので、 タイムスタンプカラムの更新で通常のafter_saveなどのコールバックを実行したくない時に使えます。

      
@user.touch(:deleted_at)
      
    

update_column

update_columnは後述のupdate_columnsと挙動は一緒ですが、複数ではなく単数のattributeを更新したい場合に使用します。

      
@thing.update_column :status, 'active'
      
    

以下のようにupdate_columnsを使って単数のattributeを更新しても挙動は同じですが、update_columnを使ったほうが単数カラムの更新というコードの意図がよりはっきりします。

      
@thing.update_columns status: 'active'
      
    

update_columns

update_columnsは複数のattributesを更新します。バリデーション、コールバック共にスキップされ、updated_atも更新されないため、生のUPDATE SQLと同義です。
使用例としては、新規レコード作成後のActiveRecordコールバックの中で、アサインされたプライマリーidを基に他のコラムを更新したい時があります。また、バグなどにより本番データベースレコードの整合性を修正したいが、生のSQLで間違って別のレコードを更新するようなミスを防ぎたいようなケースが該当します。

      
@thing.update_columns status: 'active', amount: 100
      
    

どのメソッドを使うべきかの判断

前述の各更新メソッドの特徴をまとめると、以下の表の通りとなります。

更新メソッド比較表
メソッド バリデーション コールバック実行 戻り値 エラーの場合 更新されるattributeの数 updated_at更新有無
update
(update_attributes)
あり あり ブーリアン 戻り値がfalse 複数 あり
update! あり あり ブーリアン 例外 複数 あり
update_attribute なし あり ブーリアン 戻り値がfalse 単数 あり
save あり あり ブーリアン 戻り値がfalse 複数 あり
save! あり あり ブーリアン 例外 複数 あり
toggle なし なし self n/a 単数 なし
toggle! なし あり ブーリアン 戻り値がfalse 単数 あり
touch なし あり ブーリアン n/a 複数 あり
update_column なし なし ブーリアン n/a 単数 なし
update_columns なし なし ブーリアン n/a 複数 なし

上記の表で条件にあうメソッドを絞り込んで決定しても良いですし、以下のようなディシジョンツリーを使ってどのメソッドを使うべきかを判断しても良いかと思います。
アクティブレコード更新メソッドのディシジョンツリー

補足:更新メソッド間の依存関係

各メソッドのソースコードを読むと、いくつかのメソッドは他のメソッドのラッパーメソッドであることが確認出来ます。例えば、updateのソースコードではsaveが呼ばれているため、updateの基本的な挙動はsaveに準拠することがわかります。以下の図は、どのメソッドがどのメソッドに依存しているかをまとめたものですが、この依存関係を頭に入れておくと、saveやupdate_columnsなどベースとなる数個のメソッドの特徴を覚えるだけで、他のメソッドの動作も理解出来るはずです。
アクティブレコード更新メソッドの依存関係

最後に

このように、Ruby on Railsではモデルの更新を行うのに似たようなメソッドがいくつもあるので混乱しがちですが、異なった文脈に応じて適切なメソッドを使うことでrubyらしく人間に取って読みやすいコードを書くことが出来ます。

コンテンツの配信