はじめに
Rubyにはreplace
というメソッドがあります。
Rubyの配列 (Array) やハッシュ (Hash) などのオブジェクトに対して使われ、対象オブジェクトの内容を別のオブジェクトの内容で置き換えるメソッドです。
よく見るのは
a = [1, 2, 3]
a.replace [4, 5, 6]
p a #=> [4, 5, 6]
hash1 = { a: 1, b: 2 }
hash2 = { c: 3, d: 4 }
hash1.replace(hash2)
puts hash1.inspect #=> { c: 3, d: 4 }
この辺りでしょうか??
今回はreplace
を配列やハッシュではなく、ActiveRecordと一緒に使ってみることにします。
replaceを使ったコード例
設計内容や実装メソッドの意図に関しては思いつきなので、あまり気にしないでください
シナリオ
今回は、ユーザー (User) モデルがプロジェクト (Project) とそのプロジェクトに関連するタスク (Task)、さらにタスクに紐づくコメント (Comment) に関連しているという設定を考えます。
モデル関係
User: 複数の Project を持つ。
Project: 複数の Task を持つ。
Task: 複数の Comment を持つ。
UserProject: ユーザに関連するプロジェクト。
やること
Userに紐づいているUserProjectを全て削除する
新たに紐づいているTaskを取得し、それをキーにUserProjectをbuildする
今までdestroy_all
を使用して実装していたので、今回はreplace
を使用して実装してみます。
コード
class User < ApplicationRecord
# association
has_many :user_projects, dependent: :destroy
has_many :projects
# callback
before_save :set_user_projects
def set_user_projects
user_projects.replace(
project_tasks.map do |task|
user_projects.build(task: task)
end
)
end
private
def project_tasks
Task.joins(project: :user)
.where(projects: { id: project_ids })
.distinct
end
end
説明
ここでは、user_projects に対して replace を使用して、関連するタスク (Task) の情報を一括で更新しています。
user_projects.replace
既存の user_projects を置き換える処理です。このメソッドを使うと、destroy_all を使って既存のレコードを一旦削除する必要がなく、パフォーマンスの改善や無駄な削除処理の回避が可能です。
project_tasks.map
ユーザーに関連するプロジェクト、タスクを辿ってタスク (Task) を取得しています。
user_projects.build
それぞれの Task に対して、新たに user_project を作成しています。
replace メソッドを使うことで、既存のデータを無駄に削除することなく、効率的に関連テーブルのデータを更新できます。
destroy_allでの実装時との違い
類似の処理を行うには、destroy_all を使って既存の関連を削除し、その後新しい関連を追加する方法もあります。
例えば、destroy_all を使った場合のコードは以下のようになります。
def set_user_projects
user_projects.destroy_all
project_tasks.map do |task|
user_projects.build(task: task)
end
end
こちらはぱっと見ると理解しやすいですが、次のようなデメリットもあります。
パフォーマンスの低下: destroy_all
は個別に削除を行うため、多くの関連オブジェクトがある場合にパフォーマンスが低下する可能性がある。
トランザクション管理の煩雑さ: 一連の操作が複数のステップに分かれているため、エラーが発生した場合にトランザクションの管理が複雑になります。
まとめ
個人的にあまりreplace
を使用する場面に出会わなかった(気づいていなかった)のですが、今回の例に出したように時にdestroy_all
の代わりに使用したりと、使い所はあるなと感じました。