Rails force_reload
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'
We recommend you insert this immediately after the gem 'rails'
line, so that it's loaded in all appropriate Bundler groups.
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.
TL;DR
In Rails 4.2, you could forcibly reload a model's association from the DB by passing a boolean true
to the association caller. If we have a User
model, with a has_many :posts
association, then user.posts(true)
would force Rails to pull all Post
records from DB, even if they had previously been looked up and cached. This was useful when a script would make DB changes after an association was loaded.
The boolean option was removed in 5.0, and users were directed to the reload
method to achieve the same result. There's technically two methods -- under CollectionProxy
and Persistence
), both of which already existed in the 4.x line.
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's intent, but it comes with tradeoffs the we feel warrant the effort of maintaining the original functionality.
Background
"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. We (politely!) disagree with this assessment.
Our totally subjective opinion
- 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.
- The universal consistency of
@parent.association(true)
also better supports Least Surprise theory. - Looking at the meta,
SingularAssociation
andCollectionAssociation
behave a little differently, but are grouped and handled collectively, both within the Rails core and Rails-powered applications. The close relationship warrants maintaining parity where possible.
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.