[Ruby] I want to make a specific model of ActiveRecord ReadOnly

1 minute read

What is provided in ActiveRecord only affects some behaviors
I’m worried about leaks, but I’ll have to do my best to make a module to seal it.

Place it around lib / autoload / active_record_read_only.rb

module ActiveRecordReadOnly
  extend ActiveSupport::Concern

  # ActiveRecord::Should be rolled back with a ReadOnlyRecord error
  #It's not necessary to move it in vain, so seal it
  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

  #Basic ReadOnly control
  def readonly?
    true
  end

  #Seal touch
  #This is not enough for the following reasons, but it is overwritten to match the judgment function with the actual behavior.
  def no_touching?
    true
  end

  #Seal touch
  # no_touching?If you just seal it with, it will not cause an error, so seal it directly
  def touch
    self.class.raise_readonly!
  end

  module ClassMethods
    # update_column and update_columns etc readonly?Block column specification processing without judgment
    def readonly_attributes
      attribute_names
    end

    # readonly?Since it is a single process without judgment, it is sealed directly
    def delete(_id)
      raise_readonly!
    end

    #Since it is a batch operation and no column is specified, it is sealed directly.
    def delete_all
      raise_readonly!
    end

    #Since it is a batch operation and no column is specified, it is sealed directly.
    def update_all(_attributes)
      raise_readonly!
    end

    def raise_readonly!
      raise ActiveRecord::ReadOnlyRecord,
            "#{name}.included ActiveRecordReadOnly"
    end
  end
end

Just include

class User < ApplicationRecord
  include ActiveRecordReadOnly
end

It’s closed

[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!'

[Additional supplement] Intention to block the callback

When applied to the model body, it was meaningless because it can be defined after the blockade
It is assumed that it will be separated into the inheritance model for viewing as shown below.

class User < ApplicationRecord
  before_create -> { }
end

class ReadOnlyUser < User
  include ActiveRecordReadOnly
end