はじめに
Railsが提供する楽観的ロックの仕組みであるlock_versionについて、ネストしたフォームでの使用例を学習した記録です。
lock_versionとは
Railsのモデルにlock_versionという整数カラムを追加すると、自動的に楽観的ロックが有効になります。
- 複数ユーザーが同時に編集することを許容しつつ、保存時に衝突を検知する仕組み
- Active Recordはレコードが更新されるたびに
lock_versionの値を1ずつ増やす - 保存時に送信された
lock_versionとDBの値を比較し、不一致ならActiveRecord::StaleObjectErrorを発生させる
ネストしたフォームでの使用例
モデル定義
# app/models/project.rb
class Project < ApplicationRecord
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks
end
# app/models/task.rb
class Task < ApplicationRecord
belongs_to :project
end
マイグレーション
- 両方のテーブルに
lock_versionカラムを追加する
add_column :projects, :lock_version, :integer, default: 0, null: false
add_column :tasks, :lock_version, :integer, default: 0, null: false
ビュー(フォーム)
- 各モデルに
lock_versionをhidden_fieldで埋め込む
<%= form_with(model: @project) do |f| %>
<%= f.text_field :name %>
<%= f.hidden_field :lock_version %>
<%= f.fields_for :tasks do |task_form| %>
<%= task_form.text_field :title %>
<%= task_form.hidden_field :lock_version %>
<% end %>
<%= f.submit %>
<% end %>
コントローラ
- ストロングパラメータで
lock_versionを許可する
def project_params
params.require(:project).permit(
:name, :lock_version,
tasks_attributes: [:id, :title, :lock_version, :_destroy]
)
end
動作イメージ
親モデル(Project)でも子モデル(Task)でも、基本的な流れは同じです。
- ユーザーAとユーザーBが同じレコードを編集開始(両者のフォームの
lock_versionは 0) - ユーザーAが先に保存 → DB の
lock_versionが1に更新される - ユーザーBが保存しようとすると、送信された
lock_version=0とDBの1が不一致
→ActiveRecord::StaleObjectErrorが発生
ポイント
- 親と子の
lock_versionは独立して管理される - 親が更新できても子で衝突することがあるため、親子を常に一貫性のある状態で保存したい場合は、トランザクションでまとめるのが安全
おわりに
フォーム内にhidden_fieldでlock_versionを含めるだけで衝突を検知できるのはシンプルで便利です。ただし、親と子のlock_versionは独立しているため、親子を常に一貫性のある状態で保存したい場合はトランザクションを使う必要があります。またStaleObjectErrorをrescueしてユーザーに再編集を促す仕組みを入れると、実運用でより安心です。