Skip to content

Commit

Permalink
Strict loading using :n_plus_one_only does not eagerly load child a…
Browse files Browse the repository at this point in the history
…ssociations.

    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.
  • Loading branch information
reid-rigo authored and rafaelfranca committed Apr 19, 2024
1 parent d60a234 commit 7152760
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 1 deletion.
30 changes: 30 additions & 0 deletions activerecord/CHANGELOG.md
@@ -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*
Expand Down
Expand Up @@ -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? }
Expand Down
5 changes: 5 additions & 0 deletions activerecord/lib/active_record/core.rb
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions activerecord/test/cases/strict_loading_test.rb
Expand Up @@ -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? }
Expand Down

0 comments on commit 7152760

Please sign in to comment.