Readonly Associations and Upgrading to Rails 4

Readonly Associations and Upgrading to Rails 4

After upgrading your application from Rails 3 to Rails 4, you might start seeing the following deprecation warning if you are using the readonly option in an association.

DEPRECATION WARNING: The following options in your Company.has_many :users declaration are deprecated: :readonly. Please use a scope block instead. ...

In this blog post, we’ll discuss in detail what the readonly option is and how to handle the deprecation warning.

While upgrading, you may encounter deprecation warnings that indicate that certain code will no longer work in some future version and have to be replaced with newer alternatives. These warnings can help you keep your code up to date and avoid potential issues in the future.

The “readonly” Option

Let’s see what the readonly option is and what we can do with it.

:readonly => false

By default, all associations are configured as readonly == false. The following declarations are both doing the same job:

class Company < ActiveRecord::Base
  has_many :users
end

# OR

class Company < ActiveRecord::Base
  has_many :users, :readonly => false
end
irb(main):003:0> @company.users.first.readonly?
=> false

:readonly => true

Rails Docs opens a new window says

:readonly => If true, the associated object is readonly through the association.

This means that, when accessing an object using the association cannot be modified. The same record queried without using the association can be modified freely. For example, consider the following code:

class Company < ActiveRecord::Base
  has_many :users, :readonly => true
end

# Let's take the first user
irb(main):007:0> user = Company.first.users.first
=> #<User id: 1, name: "Ashwini", company_id: 1, created_at: "2023-05-04 09:39:10", updated_at: "2023-05-04 09:39:10">

# we can see that it's a readonly record
irb(main):003:0> user.readonly?
=> true
  • It will allow you to add new users, through the association.
# This will create the user record and associate it with the company
irb(main):005:0> Company.first.users << User.create(name: "Bali")

Company Load (0.1ms)  SELECT "companies".* FROM "companies" LIMIT 1
   (0.0ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "users" ("company_id", "created_at", "name", "updated_at") VALUES (?, ?, ?, ?)  [["company_id", nil], ["created_at", Thu, 04 May 2023 11:05:12 UTC +00:00], ["name", "Bali"], ["updated_at", Thu, 04 May 2023 11:05:12 UTC +00:00]]
   (1.2ms)  commit transaction
   (0.0ms)  begin transaction
   (0.3ms)  UPDATE "users" SET "company_id" = 1, "updated_at" = '2023-05-04 11:05:12.657734' WHERE "users"."id" = 8
  • It will not stop you from changing a record when not queried with the association.

Let’s try changing the name of user Ashwini directly:

# Take the first user record
irb(main):011:0> user = User.first
=> #<User id: 1, name: "Ashwini", company_id: 1, created_at: "2023-05-04 09:39:10", updated_at: "2023-05-04 09:39:10">

irb(main):012:0> user.update_attributes(name: "Anuja")
=> #<User id: 1, name: "Anuja", company_id: 1, created_at: "2023-05-04 09:39:10", updated_at: "2023-05-04 09:39:10">

# We can see name is updated
irb(main):012:0> user = User.first
=> #<User id: 1, name: "Anuja", company_id: 1, created_at: "2023-05-04 09:39:10", updated_at: "2023-05-04 09:39:10">

Now let’s try updating the user Anuja which is our first record, through the association:

# Let's access the first user through the association
irb(main):007:0> user = Company.first.users.first
=> #<User id: 1, name: "Anuja", company_id: 1, created_at: "2023-05-04 09:39:10", updated_at: "2023-05-04 09:39:10">

# we can see that it's a readonly record
irb(main):003:0> user.readonly?
=> true

irb(main):004:0> user.update_attributes(name: "New user name")
  Company Load (0.2ms)  SELECT "companies".* FROM "companies" LIMIT 1
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."company_id" = 1 LIMIT 1
   (0.0ms)  begin transaction
   (0.0ms)  rollback transaction
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
	from /Users/ashwini/.rbenv/versions/2.3.8/lib/ruby/gems/2.3.0/gems/activerecord-3.2.22.5/lib/active_record/persistence.rb:347:in `create_or_update'
	from /Users/ashwini/.rbenv/versions/2.3.8/lib/ruby/gems/2.3.0/gems/activerecord-3.2.22.5/lib/active_record/callbacks.rb:264:in `block in create_or_update'
	from /Users/ashwini/.rbenv/versions/2.3.8/lib/ruby/gems/2.3.0/gems/activesupport-3.2.22.5/lib/active_support/callbacks.rb:414:in `_run__4261117392449202468__save__3088752762642460985__callbacks'

Any attempt to modify the associated object through the association will result in an error ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord when the readonly option is set to true.

How to address the deprecation warning

This deprecation does not only affect the readonly option but has an impact on most of the options passed to associations using the old options-based syntax. Rails 4 includes the activerecord-deprecated_finders gem and this change also affects the find_... methods.

The options-based syntax has to be replaced with the new scope-based syntax:

# Before: In Rails 3.2
class Company < ActiveRecord::Base
  has_many :users, :readonly => true
end

# After: In Rails 4
class Company < ActiveRecord::Base
  has_many :users, -> { readonly }
end

Note that, if more options are passed to the associations, those must be migrated to the new syntax too since it does not allow to have both a scope and separated options:

# Before: In Rails 3.2
class Company < ActiveRecord::Base
  has_many :users, :conditions => "active = true", :readonly => true
end

# After: Invalid in Rails 4
class Company < ActiveRecord::Base
  has_many :users, -> { readonly }, :conditions => "active = true"
  # raises "Invalid mix of scope block and deprecated finder options on ActiveRecord association"
end

# After: Valid in Rails 4
class Company < ActiveRecord::Base
  has_many :users, -> { where(active: true).readonly }
end

Conclusion

The readonly option can be useful in situations where you want to prevent accidental modifications of records through the association. However, it’s important to note that this option does not prevent modifications to the associated object through other means, such as direct database updates.

In general, it’s a good practice to use the readonly option only when necessary, and to be aware of its limitations. If you need to enforce more strict read-only access to the associated object, you may want to consider using other techniques, such as database views or custom read-only models.

If you’re not on Rails 4.0 yet, we can help! Just contact us opens a new window .

Happy Learning!

Get the book