-
-
Save palkan/03eb5306a1a3e8addbe8df97a298a466 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true | |
module Common | |
module GraphQL | |
# See https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb | |
# | |
# Additionally added support for scopes. | |
class AssociationLoader < ::GraphQL::Batch::Loader | |
def self.validate(model, association_name) | |
new(model, association_name) | |
nil | |
end | |
def initialize(model, association_schema, scope_model: nil, scopes: nil) | |
@model = model | |
@association_schema = association_schema | |
@association_name = extract_association_id(association_schema) | |
if scope_model && scopes | |
@preload_scope = scopes.reduce(scope_model) { |acc, scope| acc.public_send(scope) } | |
end | |
validate | |
end | |
def load(record) | |
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model) | |
return Promise.resolve(read_association(record)) if association_loaded?(record) | |
super | |
end | |
# We want to load the associations on all records, even if they have the same id | |
def cache_key(record) | |
record.object_id | |
end | |
def perform(records) | |
# Pass unique records to preloader to avoid duplicates | |
preload_association(records.uniq) | |
records.each { |record| fulfill(record, read_association(record)) } | |
end | |
private | |
def validate | |
unless @model.reflect_on_association(@association_name) | |
raise ArgumentError, "No association #{@association_name} on #{@model}" | |
end | |
end | |
def preload_association(records) | |
::ActiveRecord::Associations::Preloader.new.preload( | |
records, | |
@association_schema, | |
@preload_scope | |
).then(&:first).then do |preloader| | |
next unless @preload_scope && preloader | |
# The result of previous preload is memoized, ActiveRecord won't load this association again. | |
if preloader.is_a?(::ActiveRecord::Associations::Preloader::AlreadyLoaded) | |
owner = preloader.send(:owners).first | |
# We can use batch_load with the _one_ set of scopes many times during the request | |
raise ArgumentError, | |
"Preloading association twice with scopes is not possible. " \ | |
"To resolve this problem add a scoped association (e.g., `has_many :records, -> { scope_name }, ...`) to the model" | |
end | |
# this commit changes the way preloader works with scopes | |
# https://github.com/rails/rails/commit/2847653869ffc1ff5139c46e520c72e26618c199#diff-3bba5f66eb1ed62bd5700872fcd6c632 | |
preloader.send(:owners).each do |owner| | |
preloader.send(:associate_records_to_owner, owner, preloader.records_by_owner[owner] || []) | |
end | |
end | |
end | |
def read_association(record) | |
record.public_send(@association_name) | |
end | |
def association_loaded?(record) | |
record.association(@association_name).loaded? | |
end | |
def extract_association_id(id_or_hash) | |
return id_or_hash unless id_or_hash.is_a?(Hash) | |
if id_or_hash.keys.size != 1 | |
raise ArgumentError, "You can only preload exactly one association! You passed: #{id_or_hash}" | |
end | |
id_or_hash.keys.first | |
end | |
end | |
end | |
end |
Great - thank you. I think scopes needs to be an array of symbols though: %i[keep ordered]
.
@palkan I'm getting a "Preloading association twice with scopes is not possible. " error when using a scope but having 0 associated records.
class Types::ItemType < Types::BaseObject
field :accounts, [Types::AccountType], 'Accounts', null: false
def accounts
AssociationLoader.for(
Item, :accounts, scope_model: Account, scopes: %i[by_kind]
).load(object)
end
end
In scenarios where I have accounts, everything is working fine, but in tests where there are no accounts are generated, I'm receiving the error.
I believe what's happening is that when there are no records, and @preload_scope
is set to []
in line 20, that in turn this makes preloader.is_a?(::ActiveRecord::Associations::Preloader::AlreadyLoaded)
evaluate to true
in line 60- thus triggering a false positive.
EDIT: The solution of using present?
in like 57, like so: next unless @preload_scope.present?
solved the problem but stopped scopes from being properly applying.
Any ideas on how to resolve this?
I've tried to use your config but got an error:
undefined method `then' for #<Array:0x00007fa7892f7a78>
/Users/my_user/projects/my_project/argon/app/graphql/loaders/association_loader.rb:51:in `preload_association'
/Users/my_user/projects/my_project/argon/app/graphql/loaders/association_loader.rb:34:in `perform'
/Users/my_user/.asdf/installs/ruby/2.5.8/lib/ruby/gems/2.5.0/gems/graphql-batch-0.4.3/lib/graphql/batch/loader.rb:62:in `resolve'
I've solved my issue with another approach, but I decided to write to you because it can be useful for future users.
Env:
- ruby 2.5.8
- rails 5.1.7
- graphql 1.11.1
- graphql-batch 0.4.3
undefined method `then' for #Array:0x00007fa7892f7a78
This is #then
from Ruby 2.6; try replacing it with #yield_self
.
This is
#then
from Ruby 2.6; try replacing it with#yield_self
.
That's work! Thanks a lot!
Don't think this works on Rails 7 - preloading changed a bit, sig of ActiveRecord::Associations::Preloader
changed, ::ActiveRecord::Associations::Preloader::AlreadyLoaded
doesn't exist any more, etc.
Does someone have a version of this gist that applies to Rails 7 by any chance?
Scopes could be specified like this: