canvas-lms/app/graphql/loaders
Jacob Burroughs e89feee11e Improve ignored_columns handling
- 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>
2024-03-01 01:10:34 +00:00
..
README.md graphql: "better" association loader 2019-03-06 20:05:56 +00:00
api_content_attachment_loader.rb RuboCop: Lint/MissingSuper 2021-11-08 14:15:35 +00:00
assessment_request_loader.rb RuboCop: Style/StringLiterals, Style/StringLiteralsInInterpolation 2021-11-25 14:03:06 +00:00
asset_string_loader.rb RuboCop: Layout app 2021-09-22 19:35:01 +00:00
association_count_loader.rb Stop leaking siteadmin ids from spec setup 2022-02-25 23:11:11 +00:00
association_loader.rb Some rails 7 tests passing 2022-06-06 14:23:03 +00:00
course_outcome_alignment_stats_loader.rb Introduce single table inheritance (STI) to the Assignment model 2023-12-05 22:09:24 +00:00
course_role_loader.rb RuboCop: Lint/MissingSuper 2021-11-08 14:15:35 +00:00
course_student_analytics_loader.rb RuboCop: Lint/MissingSuper 2021-11-08 14:15:35 +00:00
current_grading_period_loader.rb Some rails 7 tests passing 2022-06-06 14:23:03 +00:00
discussion_entry_counts_loader.rb RuboCop: Style/StringLiterals, Style/StringLiteralsInInterpolation 2021-11-25 14:03:06 +00:00
discussion_entry_draft_loader.rb show empty nodes when there are no drafts present 2021-09-24 03:46:03 +00:00
discussion_entry_loader.rb Make discussion redesign split-screen only 2023-12-14 16:18:26 +00:00
discussion_topic_participant_loader.rb Add anonymous_author field to discussion_type 2021-12-02 17:52:35 +00:00
entry_participant_loader.rb update EntryParticipantLoader rating to return correct boolean 2021-12-08 00:19:42 +00:00
foreign_key_loader.rb RuboCop: Style/BlockDelimiters, Style/Lambda 2021-11-23 21:30:47 +00:00
id_loader.rb RuboCop: Style/BlockDelimiters, Style/Lambda 2021-11-23 21:30:47 +00:00
media_object_loader.rb RuboCop: Style/HashSyntax 2021-11-25 14:02:35 +00:00
mentionable_user_loader.rb RuboCop: Lint/MissingSuper 2021-11-08 14:15:35 +00:00
outcome_alignment_loader.rb Improve ignored_columns handling 2024-03-01 01:10:34 +00:00
outcome_friendly_description_loader.rb WarmFix - Check IOM for Manually Created Courses 2021-12-13 17:59:15 +00:00
permissions_loader.rb RuboCop: Lint/MissingSuper 2021-11-08 14:15:35 +00:00
sisid_loader.rb update sisid_loader to scope query to current account context 2023-08-10 18:30:12 +00:00
submission_version_number_loader.rb add previewUrl to graphql submission interface 2024-02-29 20:02:39 +00:00
unsharded_id_loader.rb Add API for managing "internal settings" (aka `Setting`s) 2022-04-28 15:10:33 +00:00

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