This commit addresses CI failure of Active Record isolation tests.
https://buildkite.com/rails/rails/builds/79936
Follow up #41392
* Steps to reproduce
```ruby
$ cd activerecord
$ bin/test test/cases/associations/belongs_to_associations_test.rb test/cases/associations/has_many_associations_test.rb test/cases/associations/has_many_through_disable_joins_associations_test.rb test/cases/associations/has_one_associations_test.rb test/cases/connection_adapters/schema_cache_test.rb test/cases/inheritance_test.rb test/cases/migration_test.rb test/cases/store_test.rb test/cases/strict_loading_test.rb
```
Since the entire output is too long, here is the minimum case fixed by
this commit.
```ruby
$ cd activerecord
$ bin/test test/cases/associations/belongs_to_associations_test.rb:38
Using sqlite3
Run options: --seed 34180
E
Error:
BelongsToAssociationsTest#test_belongs_to:
NameError: Rails couldn't find a valid model for Club association. Please provide the :class_name option on the association declaration. If :class_name is already provided, make sure it's an ActiveRecord::Base subclass.
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/reflection.rb:431:in `rescue in compute_class'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/reflection.rb:422:in `compute_class'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/reflection.rb:372:in `klass'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/reflection.rb:722:in `association_primary_key'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/reflection.rb:727:in `join_primary_key'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_row.rb:150:in `block in resolve_sti_reflections'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_row.rb:138:in `each_value'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_row.rb:138:in `resolve_sti_reflections'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_row.rb:92:in `fill_row_model_attributes'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_row.rb:70:in `initialize'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_rows.rb:36:in `new'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_rows.rb:36:in `block in build_table_rows_from'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_rows.rb:35:in `each'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_rows.rb:35:in `map'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_rows.rb:35:in `build_table_rows_from'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixture_set/table_rows.rb:18:in `initialize'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:692:in `new'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:692:in `table_rows'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:633:in `block (2 levels) in insert'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:632:in `each'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:632:in `block in insert'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:629:in `each'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:629:in `insert'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:615:in `read_and_insert'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/fixtures.rb:567:in `create_fixtures'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/test_fixtures.rb:268:in `load_fixtures'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/test_fixtures.rb:122:in `setup_fixtures'
/home/yahonda/src/github.com/rails/rails/activerecord/lib/active_record/test_fixtures.rb:10:in `before_setup'
bin/test test/cases/associations/belongs_to_associations_test.rb:38
Finished in 0.083668s, 11.9520 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
$
```
Strict loading mode `:n_plus_one_only` is only
supported on single records, not associations or
models. Using an ivar instead of class_attribute
ensures that this cannot be set globally. This
fixes a bug where setting `strict_loading_mode`
caused errors to be silent when
`strict_loading_by_default` is true.
Fixes#42576
Co-Authored-By: John Hawthorn <john@hawthorn.email>
If you have an application that has strict_loading set and then call
`reload` that would cause the preload values to get lost and
applications would start throwing a stict loading violation error.
In order to fix this we capture the association cache and re-apply that
to the reloaded records to avoid the strict loading error. Of course if
you never preloaded your records, this will still raise a strict loading
violation.
This change also removes the `reload` definition from associations.rb
because we can get the same behavior when we reassign the association
cache.
Co-authored-by: Aaron Patterson <tenderlove@ruby-lang.org>
The return type was changed in the PR #41704 after addition of mode
option. The current documentation is misleading since
documentation puropose strict_loading! would return boolean whereas
it returns the current mode set.
I can across this issue while debugging issue: #41827 and thought
this should be brought to the attention.
PR fixes the issue and would always return boolean based on
strict_loading is enabled or disabled.
```
user.strict_loading! # => true
user.strict_loading!(false) # => false
user.strict_loading!(mode: :n_plus_one_only) # => true
```
Add an optional mode argument to
Core#strict_loading! to support n_plus_one_only
mode. Currently, when we turn on strict_loading
for a single record, it will raise even if we are
loading an association that is relatively safe to
lazy load like a belongs_to. This can be helpful
for some use cases, but prevents us from using
strict_loading to identify only problematic
instances of lazy loading.
The n_plus_one_only argument allows us to turn
strict_loading on for a single record, and only
raise when a N+1 query is likely to be executed.
When loading associations on a single record,
this only happens when we go through a has_many
association type. Note that the has_many
association itself is not problematic as it only
requires one query. We do this by turning
strict_loading on for each record that is loaded
through the has_many. This ensures that any
subsequent lazy loads on these records will raise
a StrictLoadingViolationError.
For example, where a developer belongs_to a ship
and each ship has_many parts, we expect the
following behaviour:
developer.strict_loading!(mode: :n_plus_one_only)
# Do not raise when a belongs_to association
# (:ship) loads its has_many association (:parts)
assert_nothing_raised do
developer.ship.parts.to_a
end
refute developer.ship.strict_loading?
assert developer.ship.parts.all?(&:strict_loading?)
assert_raises ActiveRecord::StrictLoadingViolationError do
developer.ship.parts.first.trinkets.to_a
end
Fixes: #41453Closes: #41461
Previously when a model had strict loading set to true and then had a
relation set strict_loading to false the false wasn't considered when
deciding whether to raise/warn about strict loading.
Code example:
```ruby
class Dog < ActiveRecord::Base
self.strict_loading_by_default = true
has_many :treats, strict_loading: false
end
```
In the example, `dog.treats` would still raise even though
`strict_loading` was set to false. This is a bug effecting more than
Active Storage which is why I made this PR superceeding #41461. We need
to fix this for all applications since the behavior is a little
surprising. I took the test from ##41461 and the code suggestion from #41453
with some additions.
Co-authored-by: Radamés Roriz <radamesroriz@gmail.com>
If you're using `strict_loading` in an application the previous message
only told you what class was lazily loaded. This change updates the
error message to include both the class and the association name. This
is useful because now you won't need to lookup the association name in
the application, the error message will tell you exactly which symbol
preload is missing.
We don't want strict loading to throw an error on validations since
those need to load the record in order to validate it.
This change check the owners's `validation_context`. If it's `nil` then
we know we're not currently validating an object in create/update. If
it's set then we're validating and want to skip raising for strict
loading.
Fixes: #40767
Originally strict loading violations would always raise an error if
turned on for an association. This change allows for an application to
optionally log instead of raise. The behavior here is similar to how
strong parameters work. By default all environments will raise. An
application may want to use log in production while working on finding
all lazily loaded associations.
Set `config.active_record.action_on_strict_loading_violation` to `:log`
in your application to log instead of raise.
This will allow to enable/disable strict_loading mode by default for a model.
The configuration's value is inheritable by subclasses, but they can override that value and
it will not impact parent class:
```ruby
class Developer < ApplicationRecord
self.strict_loading_by_default = true
has_many :projects
end
dev = Developer.first
dev.projects.first
\# => ActiveRecord::StrictLoadingViolationError Exception: Developer is marked as strict_loading and Project cannot be lazily loaded.
```
What is great about this feature that it could help users to nip N+1 queries in
the bud, especially for fresh applications, by setting
`ActiveRecord::Base.strict_loading_by_default = true` / `config.active_record.strict_loading_by_default = true`.
That is also a great way to prevent new N+1 queries in the existing applications
after all the N+1 queries are eliminated.
(See https://guides.rubyonrails.org/v6.0/active_record_querying.html#eager-loading-associations,
https://github.com/seejohnrun/prelude for details on how to fight against N+1 queries).
Related to https://github.com/rails/rails/pull/37400, https://github.com/rails/rails/pull/38541
This commit does the following:
- Hides `StrictLoadingScope` on API doc(https://edgeapi.rubyonrails.org/)
- Documents `ActiveRecord::Base#strict_loading?` and `ActiveRecord::Base#strict_loading!`
methods.
- Adds the test case for `ActiveRecord::Base#strict_loading!` since it is
a public API.
Add `#strict_loading` to any record to prevent lazy loading of associations.
`strict_loading` will cascade down from the parent record to all the
associations to help you catch any places where you may want to use
`preload` instead of lazy loading. This is useful for preventing N+1's.
Co-authored-by: Aaron Patterson <aaron.patterson@gmail.com>