-
Notifications
You must be signed in to change notification settings - Fork 21.7k
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
Issues with Associations::Preloader used as a Public API #32140
Comments
We need to fix this case.
This is not going to be allowed by the public API, it will raise in case you pass incompatible objects.
Also should be fixed. |
Depends where you call it; it'd be possible to have two members of an inheritance hierarchy with different definitions of an association. And it's currently [deliberately] callable on AR::Base, which makes anything possible. The check is there to disallow obviously-wrong things like But I think this is just a specialization of the "first record is loaded but others aren't" problem, so if we fix that this one should be fine too. cc @dinahshi |
In the case of an inheritance hierarchy, I think a child would typically inherit all associations from its parent. And #load_association being called at the parent level would raise errors for child elements.
|
This issue has been automatically marked as stale because it has not been commented on for at least three months. |
Whether the preloading belongs in the service or the controller is arguable, here. As the service is only used for one controller action, it seems reasonable to put it in the service, but that is not a definitive answer. Adding the preloads for MR project routes here doesn't seem to work, perhaps because of rails/rails#32140.
Whether the preloading belongs in the service or the controller is arguable, here. As the service is only used for one controller action, it seems reasonable to put it in the service, but that is not a definitive answer. Adding the preloads for MR project routes here doesn't seem to work, perhaps because of rails/rails#32140.
FWIW we're also using Preloader as a "public" API and hitting the "if 1st record is loaded, return Is that something that might happen? |
You can not just change the
|
@bogdan thanks for the quick reply! This is perhaps a naive question, but could that be done within Preloader itself, like |
No, because it will skip nested associations if any. So you would need something like this: if records.partition{|r| r.send(association).loaded?}.all?(&:any?) && nested_associations_given_to_preloader_for?(association)
raise NotImplementedError
else
records = records.reject{|r| r.send(association).loaded?}
end The logic for |
@bogdan ah okay, thanks again for the reply and explanation; that makes sense. We'll have to think about this a bit. I think I now understand an initially-cryptic comment in one of our manual- In retrospect, it was probably due to this behavior, i.e. that line gives the subsequent But, at the same time, it's throwing away any preloads that previous callers had setup. This seems surprisingly nuanced/hard right now with ActiveRecord; i.e. passing around a non-trivially sized (either # of instances or continually-changing-as-features-change) list/tree of active record instances and letting disparate parts of the codebase perform the specific preloads that they need. |
The way we achieved that in our project: disallow nested associations in manual preloader execution. |
I wrote a wrapper around https://gist.github.com/michaelgpearce/52446b4de80664cfb533ec766f601854 |
Recently @eileencodes made me aware of this PullRequest #32136 and plans to possibly make
Preloader
a Public API.We've been using
Associations:Preloader
at GitHub as part of our https://github.com/Shopify/graphql-batch setup to preload associations during GraphQL queries. Over the past month we've hit some pretty nasty issues with it. I believe these may have to be fixed before making the Preloader a public concern.Loading already loaded records
The main issue is this line: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/preloader.rb#L180
When preloading an association on a list of records, if this association is already loaded on the first record of the list, it is assumed that the whole list is loaded. In these cases, a
AlreadyLoaded
preloader is returned, and usesassociation(name).target
to read the preloaded records.In a public setting, I don't think we can make this assumption, in fact, using
preload
with a list of records might result in this scenario:At GitHub we worked around this problem by always checking if the association was already loaded, before passing a record to
Preloader
, filtering them out if needed. That solved most issues until we hit issues that were out of our control.Take through associations for example:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/preloader/through_association.rb#L8-L14
If we take
First, appointments are preloaded on patients. Then,
client
is preloaded on all patients. If the first appointment's client was already loaded, we'll get back only the client.When returning the preloaded records, we'll try to get the target association on all records, but the whole tail of the list of records will be
nil
because they weren't actually loaded.In certain versions of Rails,
association.reader
is used instead oftarget
if the association is not loaded. This is almost worst because it will silently generate N+1's.Another gotcha this behaviour introduces is that you cannot use
Preloader
with different types of records / associations.For example, one issue we hit was that we were preloading an association on two different types of records at the same time:
In our case,
appoitments
on one of the records was ahas_one through:
, and the other one was abelongs to
. During thehas_one through
preloading logic, an association was loaded on the other kind of record. When it was time to load the second kind of record, since the association was already loaded for the first record, Preloader was skipping the whole list again.If needed, I can provide failing tests for some of these tricky scenarios.
Preloading different object_ids
Another smaller issue is that the preloader currently
uniq!
the records before preloading them. This means that passing multiple objects for the same record will result in only the first record being preloaded.We work around that by deduping the records before passing them to Preloader, and we copy over the associations after.
Takeaways
Currently, Rails assumes the preloader is ran in a certain context (its a private API right now after all). It breaks quickly when its used in other ways. It would be really awesome for us to make it a Public API, and it would be great to find a way around these gotchas!
cc @eileencodes @tenderlove
The text was updated successfully, but these errors were encountered: