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.

3 min read

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_save / 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 User model.

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.

Example Usage

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.

Concerns in Rails: Everything You Need to Know
Concerns are an important concept in Rails that can be confusing to understand for those new to Rails as well as seasoned practitioners. This post explains why we need concerns, how they work, and how to use them to simplify your code.

This concern defines a class_attribute named _attr_readonly on your Rails model. By default, it’s set to an empty array.

class_attribute :_attr_readonly, instance_accessor: false, default: []

The 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.

💡
Your challenge for the day: Open the latest Rails source and go to the readonly_attributes.rb file. Try to understand the HasReadonlyAttributes module and how it works.

Additional Resources


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.