Rails force_reload

Coverage Status

Starting in v5.0, Rails removed the force_reload option from ActiveRecord association readers. This gem adds that functionality back in.

# Collection association (has_many)

@user.posts(true)
# or @user.posts.reload

# Singular association (has_one)

@user.profile(true)
# or @user.reload.profile
# or @user.reload_profile
# See "Background" below for detail on the syntactical difference between these

Installation

Gemfile

gem 'rails-force-reload'

Command line

gem install 'rails-force-reload'

Compatibility

  • Ruby 2.2.2 or greater (tested against 2.2, 2.3, 2.4, and 2.5 lines)
  • Rails 5.0 or greater (tested against 5.0, 5.1 and 5.2 lines)

Tests are borrowed nearly verbatim from Rails source code.

Background

The decision to deprecate the @parent.association(true) syntax was intended to simplify the API (one way to reload associations), and better honor the Principle of Least Surprise ("what's this true mean here?"). See the original Groups thread and initial pull request for further context.

We agree with the spirit of the decision, but it came with tradeoffs.

"Reload-only" syntax limits readability, specifically when conditionally reloading a given association. GitHub user @heaven offered a succinct example (in the commit removing the functionality, no less)

That was pretty much useful #association(self.persisted?).

It was:

record.tags(record.persisted?).map(&:name)

It becomes:

(record.persisted? ? record.tags.reload : record.tags).map(&:name)

The first example is clearly more succinct and readily absorbed.

Reload syntax also breaks expectations when dealing with a singular association (Foo.has_one :bar) versus a collection association (Foo.has_many :bars).

The reload call comes after a collection...

@user.posts.reload

but before a singular

@user.reload.profile

In addition, the behavior on a singular association is not a one-for-one match with the force_reload syntax. Since we are reloading the parent, we also throw away any unsaved changes and existing existing caches. To compensate, a new reload_<association> dynamic method was introduced. Our above example would be rewritten as such:

@user.reload_profile

This is the recommended way to handle singular associations, given the side effects of reloading the parent, however both methods will technically work.

DHH expressed satisfaction with divergent handling to has_one vs has_many associations. Obviously, we (politely!) disagree with this assessment.

The "reload before/after" syntax, coupled with the introduction of a dynamic magic method to recover lost functionality would seem to be a net loss, when compared with the consistency of @parent.association(true)

Looking at the meta, Singular and Collection associations behave differently, but are grouped and handled collectively, both within the Rails core and Rails-powered applications. The close relationship warrants maintaining parity where possible. This is obviously a matter of opinion, but was a factor of consideration.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run bundle exec rake install.

We use the appraisal gem to help us generate the individual Gemfiles for each ActiveRecord version and to run the tests locally against each generated Gemfile. The bundle exec rake appraisal test command actually runs our test suite against all Rails versions in our Appraisal file. If you want to run the tests for a specific Rails version, use rake -T for a list.

$ bundle exec appraisal activerecord52 rake test

We provide a Vagrant box to spin up the entire gem in a clean environment. Doing so will avoid the need to prefix all commands with bundle exec

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/battlebrisket/rails-force-reload.

License

The gem is available as open source under the terms of the MIT License.