Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Preloader should no longer overwrites built records #50129

Merged
merged 1 commit into from
Dec 7, 2023

Conversation

MaxLap
Copy link
Contributor

@MaxLap MaxLap commented Nov 22, 2023

In short, this change makes it so that using ActiveRecord::Associations::Preloader doesn't overwrite / lose state.

Right now, here is how things are:

post = Post.last
post.comments.build
ActiveRecord::Associations::Preloader.new(records: [post], associations: :comment)
post.comments.to_a

The last line would not return the comment that was built. This is unintuitive and was changed in Rails 6.0. In Rails 5.2, this code behave as one would expect and still contains the persisted comment.

Considering that the commit that introduced this behaviors was to fix a problem that, it turns out, was also fixed differently, this change is just a partial revert of said commit with no ill effects.

It would be nice to backport this to at least 7.1 since it's so new. The code hasn't changed since Rails 7.0 so that would be easy too. I'm willing to do it if there is openness to such a backport.

Investigation

In effect, this PR undoes changes made in 5f9e050 which introduced this bug.

Here is the story:

Starting after Rails 5.2

(Bug1) If you have 2 distinct but equal records given to preload, only one of them will receive the preloaded records.

(BugfixA) 9897f50
This fixes Bug1. The #uniq was removing the extra distinct but equal records, so only one of them would receive sub-records in its associations.
But there is a new bug!

(Bug2) If you have a single instance twice in the records to preload on, it receives the sub-records multiple time.

(BugfixB): 5f9e050
This fixes Bug2. And is what I am reverting.
Instead of adding the preloaded data to what was already in the association, the received data overwrites the data that was already there.
But there is a new bug!

(Bug3) If you had unpersisted data in the association being preloaded (using #build for example), it will be gone!
This is the bug this PR wants to fix.

(BugfixC) a4b20e4
This fix doesn't resolve Bug3.
But it also fixes Bug2, and does it more correctly. It does a better #uniq than the one that was removed by BugfixA due to Bug1.
Unlike BugfixB, BuugfixC doesn't introduce Bug3.

So at this point, it's safe to undo BugfixB, at least the part that causes Bug3:

  • Bug2 doesn't get brought back because BugfixC also fixes it.

This is what this PR is doing, while also adding tests.

I'd like to thank git bisect for making this investigation so much simpler than it would have been otherwise xD

Before this, using ActiveRecord::Associations::Preloader would replace any records that were built on the association
@MaxLap MaxLap changed the title Preloader should not longer overwrites built records Preloader should no longer overwrites built records Nov 22, 2023
@MaxLap
Copy link
Contributor Author

MaxLap commented Nov 25, 2023

@kamipo Could you take a look? You made BugfixB (which I'm partially reverting) and you merged the PRs for BugfixA and BugfixC which interact with this. Thank you

@MaxLap
Copy link
Contributor Author

MaxLap commented Dec 5, 2023

I did not notice that kamipo was not really active anymore. Could someone else provide feedback?

@skipkayhil
Copy link
Member

The best way to get feedback would be to bring it up on Discord

@MaxLap MaxLap changed the title Preloader should no longer overwrites built records Fix: Preloader should no longer overwrites built records Dec 6, 2023
@tenderlove
Copy link
Member

At first glance, this seems fine to me. My only hesitation is that the preloader isn't supposed to be public API. Is there a way to tickle this bug without directly calling the preloader? IOW is it possible for this bug to occur via public APIs?

@MaxLap
Copy link
Contributor Author

MaxLap commented Dec 7, 2023

Thank you for the feedback!

I don't think it's possible with only public API to trigger this bug.

Having an array of records that need some form of mass loading of an association is a super frequent need when dealing with a complex application. I encompass this into a #preload_now function on Array. Just in my codebase, I have 80 uses of it.

As #39156 mentions, Preloader is not public API but is popular anyways. In my opinion, the only reason it's not a public API is that you don't get much requests for it mainly because it's pretty easy to use even as a private API. If it was painful, there would be more pressure for it to be a public API.

I can't think of a situation where the current behavior makes sense other than a "it will never happen". Should Preloader ever become a public API, then this would become an obvious problem.

@tenderlove tenderlove merged commit c5a8621 into rails:main Dec 7, 2023
4 checks passed
@MaxLap
Copy link
Contributor Author

MaxLap commented Dec 12, 2023

Any chance this can be cherry-picked back to 7-1-stable? 7.2 is a long way to go. I just tested it and it's a clean cherry-pick without conflicts. Thank!

dustinbrownman added a commit to dustinbrownman/rails that referenced this pull request Dec 13, 2023
When preloading a has_many association, we were simply concatenating
the preloaded records without regard to whether they were already
loaded on the owner. Even though there is a check for `loaded?` in this
part of the preloader, some persisted records may not be marked as such.
For example, if a record is created via `owner.association.create`. This
change reverts to the previous behavior of replacing the target
association while also preserving non-persisted records, which was the
goal of rails#50129.

Co-authored-by: John Hawthorn <john@hawthorn.email>
dustinbrownman added a commit to dustinbrownman/rails that referenced this pull request Dec 13, 2023
When preloading a has_many association, we were simply concatenating
the preloaded records without regard to whether they were already
loaded on the owner. Even though there is a check for `loaded?` in this
part of the preloader, some persisted records may not be marked as such.
For example, if a record is created via `owner.association.create`. This
change reverts to the previous behavior of replacing the target
association while also preserving non-persisted records, which was the
goal of rails#50129.

Co-authored-by: John Hawthorn <john@hawthorn.email>
@jhawthorn
Copy link
Member

Sorry, this isn't the type of change we should be backporting. Even if the API was public it could change expectations of users (and I could see an argument that the previous behaviour wasn't a bug)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants