Readonly Attributes in Rails
In this article, we'll learn how to mark certain attributes as readonly on your active record models, to prevent them from any future updates after the record is created and saved to the database. We'll also learn how Rails implements this feature behind the scenes.
Sometimes, you want to prevent changes to certain attributes on your Active Record models after they are initialized and saved in the database. For example, you'd like to set the users' account number upon registration and want to ensure that it's never updated in the future.
Now you could override the write_attribute method or insert a
before_update callback to listen for the updates to these attributes, but it’d be nice if there was a Rails-style declarative solution to handle this.
Turns out, there is!
Rails provides the
attr_readonly method to mark some attributes on your Active Record models as readonly, to prevent them from further modification. For example, the following code snippet marks the
account_id property as readonly on a
class User < ApplicationRecord attr_readonly :account_id end
You can create a new
User record with the initial value for
account_id, but Rails won’t let you update its value once it’s saved in the database. If you try to change it, the value will only change in memory, but won’t be persisted in the database on calling
save. You can only set the value of a readonly attribute when the object isn't saved in the database.
This example, along with the tests that follow, shows how to use readonly attributes.
Note: Starting version 7.1 and above, Rails will raise an error if you try to modify a readonly attribute (covered in the next section).
class Book < ApplicationRecord attr_readonly :title end class BookTest < ActiveSupport::TestCase test "title is readonly" do book = Book.create(title: 'Harry Potter') assert_equal 'Harry Potter', book.title # Try changing the title book.update(title: "Chamber of Secrets") # Changed in memory assert_equal "Chamber of Secrets", book.title # Reload the book from database book.reload # Change was not persisted in database assert_equal "Harry Potter", book.title ## Convenience methods # Get a set of all readonly attributes on the model assert_equal ["title"].to_set, Post.readonly_attributes # Check if an attribute is readonly assert Post.readonly_attribute? "title" end end
Latest Rails will Raise an Error!
Until very recently, the updates were getting ignored silently. That meant assignment would succeed but silently not write to the database. However, this behavior was changed a few months ago in this pull request: Raise on assignment to readonly attributes
If you try to update a readonly attribute on a model that’s already saved in the database, Rails will raise
ActiveRecord::ReadonlyAttributeError. Set the
config.active_record.raise_on_assign_to_attr_readonly setting to
false to prevent Rails from raising this error.
How Rails Implements Readonly Attributes
This functionality is defined in the
ActiveRecord::ReadonlyAttributes module, which is a concern. The
ActiveRecord::Base class includes it and hence all your Active Record models get this behavior out of the box.
# activerecord/lib/active_record/readonly_attributes.rb module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern end end
To learn more about how concerns work, check out this article.
This concern defines a
_attr_readonly on your Rails model. By default, it’s set to an empty array.
class_attribute :_attr_readonly, instance_accessor: false, default: 
attr_readonly method is added as a class method to the Active Record model, such as
User. That’s why you can call it directly inside the class.
Here’s a simplified implementation of this method.
module ClassMethods def attr_readonly(*attributes) self._attr_readonly |= attributes.map(&:to_s) end def readonly_attributes _attr_readonly end def readonly_attribute?(name) # :nodoc: _attr_readonly.include?(name) end end
As you can see, when we mark an attribute as readonly using the
attr_readonly method, Rails will simply store it in the
_attr_readonly array, which we saw in the previous snippet.
Later, when the record is saved, Rails will exclude all the attributes in this array from the list of attributes to be saved in the database. See the
ActiveRecord::Associations::CollectionAssociation#merge_target_lists method to learn more.
readonly_attributes.rbfile. Try to understand the
HasReadonlyAttributesmodule and how it works.
- Rails API documentation for
- PR #46105: Raise on assignment to readonly attributes
- How class attributes work in Rails
I hope you found this article useful and that you learned something new.
If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.
Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.