ActiveRecord に用意されているものが一部の挙動にしか効果がない
漏れがないか不安になるが頑張って封じるモジュールを作るしかなさそう
lib/autoload/active_record_read_only.rb
あたりに配置して
module ActiveRecordReadOnly
extend ActiveSupport::Concern
# ActiveRecord::ReadOnlyRecord エラーで rollback されるはずだが
# 無駄に動かす必要もないのでもないので封じる
included do
before_save { self.class.raise_readonly! }
before_create { self.class.raise_readonly! }
before_update { self.class.raise_readonly! }
before_destroy { self.class.raise_readonly! }
end
# 基本の ReadOnly 制御
def readonly?
true
end
# touch を封じる
# 下記理由によりこれだけでは不足だが判定関数と実挙動を一致させるため上書き
def no_touching?
true
end
# touch を封じる
# no_touching? で封じるだけだとエラーにならないので直接封じる
def touch
self.class.raise_readonly!
end
module ClassMethods
# update_column や update_columns など readonly? 判定のないカラム指定処理を封じる
def readonly_attributes
attribute_names
end
# readonly? 判定のない単独処理なので直接封じる
def delete(_id)
raise_readonly!
end
# 一括操作でありカラムも指定しない処理なので直接封じる
def delete_all
raise_readonly!
end
# 一括操作でありカラムも指定しない処理なので直接封じる
def update_all(_attributes)
raise_readonly!
end
def raise_readonly!
raise ActiveRecord::ReadOnlyRecord,
"#{name}.included ActiveRecordReadOnly"
end
end
end
include するだけ
class User < ApplicationRecord
include ActiveRecordReadOnly
end
封鎖できてる
[1] pry(main)> ActiveRecord::Base.logger = nil
=> nil
[2] pry(main)> User.first.destroy
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
[3] pry(main)> User.first.update(code: 'test')
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
[4] pry(main)> User.first.update_columns(code: 'test')
ActiveRecord::ActiveRecordError: code is marked as readonly
from /usr/local/bundle/gems/activerecord-6.0.0/lib/active_record/persistence.rb:947:in `verify_readonly_attribute'
[5] pry(main)> User.first.update_column(:code, 'test')
ActiveRecord::ActiveRecordError: code is marked as readonly
from /usr/local/bundle/gems/activerecord-6.0.0/lib/active_record/persistence.rb:947:in `verify_readonly_attribute'
[6] pry(main)> User.first.touch
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
[7] pry(main)> User.create(code: 'test')
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
[8] pry(main)> User.delete(1)
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
[9] pry(main)> User.delete_all
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
[10] pry(main)> User.update_all(code: 'test')
ActiveRecord::ReadOnlyRecord: User.included ActiveRecordReadOnly
from /app/lib/autoload/active_record_read_only.rb:52:in `raise_readonly!'
【追記補足】コールバック封鎖の意図
モデル本体に適用する場合は封鎖後に定義できるので意味がありませんでした
下記のような閲覧用の継承モデルに分離する場合を想定しています
class User < ApplicationRecord
before_create -> { }
end
class ReadOnlyUser < User
include ActiveRecordReadOnly
end