Skip to content

Issues with Associations::Preloader used as a Public API #32140

@xuorig

Description

@xuorig

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 uses association(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:

a = Record.create!
b = Record.create!
c = Record.create!

a.some_association.create!

preloaders = ActiveRecord::Associations::Preloader.new.preload([a,b,c], [:some_association])

preloaders.first.preloaded_records
=> [Only the association for a is preloaded]

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

has_many :patients, through: :appointments , source: :client

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 of target 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:

Preloader.new.preload([doctor, patient, patient_b], [:appointment])

In our case, appoitments on one of the records was a has_one through:, and the other one was a belongs to. During the has_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions