![]() - Actually enumerate columns when any are ignored to avoid loading unknown attributes - Remove old ignored_columns so we don't unnecessary bloat queries when not ignoring - Various minor fixes for places we do unusual AR things to ensure they work with explicit columns - Tweak some migrations to clear column information so future migrations are happy refs AE-747 Change-Id: I60b1c3eae73f4fa9f0b6b6ab4d2b00abd8f8395f Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/339971 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Cody Cutrer <cody@instructure.com> Migration-Review: Cody Cutrer <cody@instructure.com> QA-Review: Jacob Burroughs <jburroughs@instructure.com> Product-Review: Jacob Burroughs <jburroughs@instructure.com> |
||
---|---|---|
.. | ||
README.md | ||
api_content_attachment_loader.rb | ||
assessment_request_loader.rb | ||
asset_string_loader.rb | ||
association_count_loader.rb | ||
association_loader.rb | ||
course_outcome_alignment_stats_loader.rb | ||
course_role_loader.rb | ||
course_student_analytics_loader.rb | ||
current_grading_period_loader.rb | ||
discussion_entry_counts_loader.rb | ||
discussion_entry_draft_loader.rb | ||
discussion_entry_loader.rb | ||
discussion_topic_participant_loader.rb | ||
entry_participant_loader.rb | ||
foreign_key_loader.rb | ||
id_loader.rb | ||
media_object_loader.rb | ||
mentionable_user_loader.rb | ||
outcome_alignment_loader.rb | ||
outcome_friendly_description_loader.rb | ||
permissions_loader.rb | ||
sisid_loader.rb | ||
submission_version_number_loader.rb | ||
unsharded_id_loader.rb |
README.md
GraphQL Batch Loaders
Motivation
In a Canvas REST API end-point. N+1 Queries are commonly found, but easy to avoid. When a controller action runs, we know what data is being requested and can preload appropriately.
As a GraphQL type resolver executes, it is too late to preload data. Consider the following query:
query assignmentsAndGroupSets {
course(id: "1") {
assignmentsConnection {
nodes {
id
groupSet { # N+1 !?
name
}
}
}
}
}
When resolving the groupSet
field above, the only context we have is an
individual assignment. It's not possible to use the normal
scope.preload(...)
approach of preventing N+1 queries.
To solve this problem, instead of returning an ActiveRecord instance in the
groupSet
resolver, we use the graphql-batch
gem to return a deferred value.
Example:
# bad example
def group_set
assignment.group_set # this will result in N+1 queries
end
# good example
def group_set
Loaders::AssociationLoader.for(Assignment, :group_category).load(assignment)
end
# short (but still good) example
def group_set
# a helper method is provided since this is such a common use-case
load_association(:group_category)
end
# bad example (async confusion)
def group_set
group_set = nil
Loaders::AssociationLoader.for(Assignment, :group_category).
load(assignment).
then {
group_set = assignment.group_category
}
group_set # this will still be nil when at this point
# (you must return a promise when dealing with loaders)
end
See graphql-batch for more information.
Available Batch Loaders
Loaders::AssociationLoader
can be used for any instances that would have used
.preloads
or ActiveRecord::Associations::Preloader.new
in the past (it uses
those methods under the hood).
Loaders::IDLoader
and Loaders::ForeignKeyLoader
can be used to batch-load
records by id.
It may also be necessary to write your own batch loader from time to time.
How To Write a New Batch Loader
Writing a new batch loader is easy. At a minimum, you must define a class that
inherits from GraphQL::Batch::Loader
which defines a perform
method (and
probably a constructor).
Example:
class CustomLoader < GraphQL::Batch::Loader
def initialize(*args)
# constructor arguments can be used to provide information in the perform
# method. they also define "buckets" for batching
end
def perform(objects)
results = do_something_to_batch_load_data
objects.each { |o|
# fulfill provides the value for the deferred object we returned in
# our resolver
fulfill(o, results[o])
}
end
end