Strict loading using :n_plus_one_only does not eagerly load child associations.

Before:

    ```ruby
    person = Person.find(1)
    person.strict_loading!(mode: :n_plus_one_only)
    person.posts.first
    # SELECT * FROM posts WHERE person_id = 1; -- non-deterministic order
    ```

    After:

    ```ruby
    person = Person.find(1)
    person.strict_loading!(mode: :n_plus_one_only)
    person.posts.first # this is 1+1, not N+1
    # SELECT * FROM posts WHERE person_id = 1 ORDER BY id LIMIT 1;
    ```

    Strict loading in `:n_plus_one_only` mode is designed to prevent performance issues when
    deeply traversing associations. It allows `Person.find(1).posts`, but _not_
    `Person.find(1).posts.map(&:category)`. With this change, child associations are no
    longer eagerly loaded, to match intended behavior and to prevent non-deterministic
    order issues caused by calling methods like `first` or `last`.
    Fixes #49473.
This commit is contained in:
Reid Lynch 2023-07-22 12:32:21 -04:00 committed by Rafael Mendonça França
parent d60a23457c
commit 715276071f
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
4 changed files with 48 additions and 1 deletions

@ -1,3 +1,33 @@
* Strict loading using `:n_plus_one_only` does not eagerly load child associations.
With this change, child associations are no longer eagerly loaded, to
match intended behavior and to prevent non-deterministic order issues caused
by calling methods like `first` or `last`. As `first` and `last` don't cause
an N+1 by themselves, calling child associations will no longer raise.
Fixes #49473.
Before:
```ruby
person = Person.find(1)
person.strict_loading!(mode: :n_plus_one_only)
person.posts.first
# SELECT * FROM posts WHERE person_id = 1; -- non-deterministic order
person.posts.first.firm # raises ActiveRecord::StrictLoadingViolationError
```
After:
```ruby
person = Person.find(1)
person.strict_loading!(mode: :n_plus_one_only)
person.posts.first # this is 1+1, not N+1
# SELECT * FROM posts WHERE person_id = 1 ORDER BY id LIMIT 1;
person.posts.first.firm # no longer raises
```
*Reid Lynch*
* Allow `Sqlite3Adapter` to use `sqlite3` gem version `2.x`
*Mike Dalessio*

@ -303,7 +303,7 @@ def null_scope?
def find_from_target?
loaded? ||
owner.strict_loading? ||
(owner.strict_loading? && owner.strict_loading_all?) ||
reflection.strict_loading? ||
owner.new_record? ||
target.any? { |record| record.new_record? || record.changed? }

@ -704,6 +704,11 @@ def strict_loading_n_plus_one_only?
@strict_loading_mode == :n_plus_one_only
end
# Returns +true+ if the record uses strict_loading with +:all+ mode enabled.
def strict_loading_all?
@strict_loading_mode == :all
end
# Marks this record as read only.
#
# customer = Customer.first

@ -86,6 +86,18 @@ def test_strict_loading_n_plus_one_only_mode_with_belongs_to
end
end
def test_strict_loading_n_plus_one_only_mode_does_not_eager_load_child_associations
developer = Developer.first
developer.strict_loading!(mode: :n_plus_one_only)
developer.projects.first
assert_not_predicate developer.projects, :loaded?
assert_nothing_raised do
developer.projects.first.firm
end
end
def test_strict_loading
Developer.all.each { |d| assert_not d.strict_loading? }
Developer.strict_loading.each { |d| assert_predicate d, :strict_loading? }