I recently worked on a project that was built around several hundred GB of data. Several large databases were populated by one team and then consumed by several other applications.
The project I was working on needed to access that data but would never need to modify it. Perhaps I was being overly paranoid, but I wanted to specify that everything from those databases was read-only to make sure nothing was accidently changed.
ActiveRecord is one of the coolest things about rails. With almost no work I get data models, including dead-simple CRUD, in my applications. But the inability to designate a model as being read-only is really frustrating to me.
I know that rails can mark individual instances with :readonly
, but I wanted to ensure that every instance of these objects was read-only. Here’s what I came up with:
class FooBar < ActiveRecord::Base # Prevent creation of new records and modification to existing records def readonly? return true end # Prevent objects from being destroyed def before_destroy raise ActiveRecord::ReadOnlyRecord end end |
And that’s all there is to it! FooBar objects are now read-only.
# You will not be able to create new records >> fb = FooBar.create(:name => "zork") ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord # You will not be able to save modified records >> fb = FooBar.find(:first) >> fb.name = "plugh" >> fb.save ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord # You will not be able to destroy existing records >> fb = FooBar.find(:first) >> fb.destroy ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord |
For the sake of simplicity the code here is in a regular rails model. But if you’re working with a large number of models like I was, you might consider extracting these methods into a new class “ReadOnlyActiveRecord” which can then used as the parent for the other models. DRY, right?
Depending on your situation, you may need to lock down the record more tightly. With this code you could still create and manipulate new FooBar objects in your code. The exception isn’t thrown until you try to persist to the database.
I certainly don’t consider myself an expert on rails. If anyone knows a better solution to this problem I’d love to hear it.
Thanks for this post! It came up on a Google search for “activerecord and readonly”. I have a situation where a proposal is generated, and after that, several records need to be locked down. This is perfect for that!
I’m really glad that it was helpful to you Jeremy!
Thanks,
I’m using this with a materialized view that gets updated by plpgsql database functions and triggers.
Thanks for this post. I was doing something quite similar and found that you can still call delete and delete_all, as they do not use callback methods. You can override those methods on your read only model and raise the ReadOnly exception in the exact same way:
def self.delete_all
raise ActiveRecord::ReadOnlyRecord
end
def delete
raise ActiveRecord::ReadOnlyRecord
end
why not ensure read only access on the db layer through proper user rights?
this would be more secure (your solution does not cover native sql)
pascal
I agree that enforcing constraints at the database level is a good approach. For the specific project I was working on when I wrote this article, I did not have access to make changes to the database itself. Because I tend to be paranoid, I wanted some assurance that data wasn’t accidentally modified. 🙂