Is there an easy way to make a Rails ActiveRecord model read-only?

Ruby on-RailsRuby on-Rails-3Activerecord

Ruby on-Rails Problem Overview


I want to be able to create a record in the DB but then prevent Rails from making changes from that point on. I understand changes will still be possible at the DB level.

I believe attr_readonly does what I want on an attribute level, but I don't want to have to manually specify fields... I would rather have more of a white-list approach.

Also, I know there is a :read_only option for associations, but I don't want to limit the "readonlyness" of the object to if it was fetched via an association or not.

Finally, I want to be able to still destroy a record so stuff like :dependent => :destroy works in the associations.

So, to summarize: 1) allow the creation of records, 2) allow the deletion of records, and 3) prevent changing records that have been persisted.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

Looking at http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html">`ActiveRecord::Persistence`</a>;, everything ends up calling create_or_update behind the scenes.

def create_or_update
  raise ReadOnlyRecord if readonly?
  result = new_record? ? create : update
  result != false
end

So! Just:

def readonly?
  !new_record?
end

Solution 2 - Ruby on-Rails

I've found a more concise solution, which uses the after_initialize callback:

class Post < ActiveRecord::Base
  after_initialize :readonly!
end

Solution 3 - Ruby on-Rails

Why not just create a user on the database that has read only access, and have rails use that account.

However if you want model level access, you can add the following to a specific model:

 def readonly?
    true
  end

  def before_destroy
    raise ActiveRecord::ReadOnlyRecord
  end

Solution 4 - Ruby on-Rails

This blog post is still valid: http://ariejan.net/2008/08/17/activerecord-read-only-models/

Basically you can rely on ActiveRecord's validation if you add a method:

def readonly?
  true
end

Solution 5 - Ruby on-Rails

TL;DR for OP's

class YourModel < ActiveRecord::Base
  before_save { false } # prevent create & update, allows destroy

  # ... 
end

Generally

  • To prevent creates only: before_create { false }
  • To prevent updates only: before_update { false }
  • To prevent destroys only: before_destroy { false } # does not prevent delete

See also: http://guides.rubyonrails.org/active_record_callbacks.html

Solution 6 - Ruby on-Rails

This seems to be fairly effective and is probably a bit overkill, but for my case, I really want to be sure my application will never create, save, update, or destroy any records in the model, ever.

module ReadOnlyModel
  def readonly?() true end
  def create_or_update() raise ActiveRecord::ReadOnlyRecord end
  before_create { raise ActiveRecord::ReadOnlyRecord }
  before_destroy { raise ActiveRecord::ReadOnlyRecord }
  before_save { raise ActiveRecord::ReadOnlyRecord }
  before_update { raise ActiveRecord::ReadOnlyRecord }
end

class MyModel < ActiveRecord::Base
  include ReadOnlyModel
  # ...
end

Since OP asked to be able to create and destroy but not save or update I believe this will work

module SaveAndDestroyOnlyModel
  before_save { raise ActiveRecord::ReadOnlyRecord }
  before_update { raise ActiveRecord::ReadOnlyRecord }
end

class MyModel < ActiveRecord::Base
  include SaveAndDestroyOnlyModel
  # ...
end

Not exactly the right exception, but close enough I think.

Solution 7 - Ruby on-Rails

A custom validator can do this:

validate :nothing_changed, unless: :new_record? # make immutable

...

def nothing_changed
  errors.add(:base, "Record is read-only") if self.changed?
end

Solution 8 - Ruby on-Rails

Looking for a way to achieve the same control proposed by @Nate (avoiding any kind of create/update/delete) but using this only in specific parts of my application and for all models at once I have created this Ruby refinement:

module ReadOnlyRailsMode
  CLASS_METHODS    = ActiveRecord::Base.methods
    .select { |m| m =~ /(update|create|destroy|delete|save)[^\?]*$/ }

  INSTANCE_METHODS = ActiveRecord::Base.instance_methods
    .select { |m| m =~ /(update|create|destroy|delete|save)[^\?]*$/ }

  refine ActiveRecord::Base.singleton_class do
    CLASS_METHODS.each do |m|
      define_method(m) do |*args|
        raise ActiveRecord::ReadOnlyRecord
      end
    end
  end

  refine ActiveRecord::Base do
    def readonly?; true; end

    INSTANCE_METHODS.each do |m|
      define_method(m) do |*args|
        raise ActiveRecord::ReadOnlyRecord
      end
    end
  end
end

And to use it only in a specific portion of the code:

class MyCoolMailerPreview < ActionMailer::Preview
  using ReadOnlyRailsMode 
end

(This is a real use case, I was looking for a way to avoid people creating and editing real records from inside ActionMailer::Previews because I want to allow previews in production, but if by mistake anyone creates a preview which changes real data, this would became a chaos).

The code is a little ugly redefining all methods (create, create!, etc) because the intent is to change the behavior of all models, and callbacks like "before_create" can't be used for this purpose since they would not be locally only to the "using" scope, changing the whole application.

This approach is working for me, I can explicitly block all this methods for all models in just one class, and don't mess with the rest of the application. Unfortunately, until now, refinements don't apply to sub classes, so in my case I was not able to block all inserts by default into the parent class (ActionMailer::Preview), which was my original goal, but blocking per class is a good starting point.

My application requires refining all methods, but the control can be done for just the interesting methods like destroy, or update and them this can works for all cases, including the one from the original question.

Solution 9 - Ruby on-Rails

.default_scope seems to also work, though some of the above answers may be better as this way could be circumvented by using .unscoped. Though, that may be beneficial for some peoples needs.

Example with Postgres making a temporary, read only model to query the pg_type table.

Class.new(ActiveRecord::Base) { self.table_name = 'pg_type'; self.primary_key = :oid; default_scope { readonly } }.first.readonly?
# => true

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbrettishView Question on Stackoverflow
Solution 1 - Ruby on-RailsscragzView Answer on Stackoverflow
Solution 2 - Ruby on-RailskwarrickView Answer on Stackoverflow
Solution 3 - Ruby on-RailsMike LewisView Answer on Stackoverflow
Solution 4 - Ruby on-RailsapneadivingView Answer on Stackoverflow
Solution 5 - Ruby on-Railsuser246672View Answer on Stackoverflow
Solution 6 - Ruby on-RailsNateView Answer on Stackoverflow
Solution 7 - Ruby on-RailsBurnzoireView Answer on Stackoverflow
Solution 8 - Ruby on-RailsEverton J. CarpesView Answer on Stackoverflow
Solution 9 - Ruby on-RailsChad MView Answer on Stackoverflow