3
1

More than 3 years have passed since last update.

ActiveRecord の特定モデルを ReadOnly にしたい

Last updated at Posted at 2020-06-22

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
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1