### Schema Setup Source: https://context7.com/shopify/graphql-batch/llms.txt Register GraphQL::Batch as a plugin on your schema to enable query batching and automatic loader cache clearing for mutations. ```APIDOC ## Schema Setup — `GraphQL::Batch.use` Register GraphQL::Batch as a plugin on your schema. It installs multiplex instrumentation that starts/ends a batch executor per query execution and sets up lazy promise resolution. If a mutation type is defined, it automatically wraps each mutation field so the loader cache is cleared between mutations to ensure correct sequential behaviour. ```ruby require 'graphql/batch' class MySchema < GraphQL::Schema query Types::QueryType mutation Types::MutationType # must be defined BEFORE use GraphQL::Batch use GraphQL::Batch # Custom executor class can be injected: # use GraphQL::Batch, executor_class: MyCustomExecutor end ``` ``` -------------------------------- ### Add graphql-batch to Gemfile Source: https://github.com/shopify/graphql-batch/blob/main/README.md Include the graphql-batch gem in your application's Gemfile for installation. ```ruby gem 'graphql-batch' ``` -------------------------------- ### Obtaining Loader Instances with Loader.for Source: https://context7.com/shopify/graphql-batch/llms.txt Use `Loader.for` to get a loader instance for a specific group of arguments. Calls with the same class and arguments share a single loader instance, resulting in a single batched query. ```ruby # Two fields requesting different models — two separate loader instances, two batched queries: def product(id:) RecordLoader.for(Product).load(id) end def image(id:) RecordLoader.for(Image).load(id) end # Two fields requesting the same model — one shared loader instance, one batched query: def product1(id:); RecordLoader.for(Product).load(id); end def product2(id:); RecordLoader.for(Product).load(id); end # Both ids are collected → perform([id1, id2]) — only one DB query fired ``` -------------------------------- ### Pre-populate Cache with `Loader#prime` Source: https://context7.com/shopify/graphql-batch/llms.txt Utilize `prime` to insert pre-fetched records into the loader's cache, preventing redundant database queries. This is particularly useful in tests or when data is already available. ```ruby def liked_products products = Product.where(liked: true).to_a loader = RecordLoader.for(Product) products.each do |product| loader.prime(product.id, product) # pre-fill cache end # Subsequent load(id) calls for these products are served from cache — no DB query products end ``` ```ruby # In tests — prime to avoid DB dependency: def test_product_title loader = RecordLoader.for(Product) loader.prime(42, Product.new(id: 42, title: "Test Shirt")) result = GraphQL::Batch.batch { loader.load(42).then(&:title) } assert_equal "Test Shirt", result end ``` -------------------------------- ### Batch independent queries with Promise.all Source: https://github.com/shopify/graphql-batch/blob/main/README.md Use Promise.all to execute multiple independent queries concurrently. This allows each query in the group to be batched with other similar queries. ```ruby def all_collections Promise.all([ CountLoader.for(Shop, :smart_collections).load(context.shop_id), CountLoader.for(Shop, :custom_collections).load(context.shop_id), ]).then(&:sum) end ``` -------------------------------- ### Define a custom RecordLoader Source: https://github.com/shopify/graphql-batch/blob/main/README.md Create a custom loader by inheriting from GraphQL::Batch::Loader. The initialize method accepts arguments for grouping, and the perform method handles batch loading logic. ```ruby class RecordLoader < GraphQL::Batch::Loader def initialize(model) @model = model end def perform(ids) @model.where(id: ids).each { |record| fulfill(record.id, record) } ids.each { |id| fulfill(id, nil) unless fulfilled?(id) } end end ``` -------------------------------- ### Obtaining a Loader Instance Source: https://context7.com/shopify/graphql-batch/llms.txt `Loader.for(*group_args, **group_kwargs)` returns or creates a loader instance for the given arguments. Calls with the same class and arguments share a single loader instance. ```APIDOC ## `Loader.for(*group_args, **group_kwargs)` — Obtaining a Loader Instance `Loader.for` returns (or creates) the loader instance for the given group arguments within the current executor. All calls with the same class and arguments share one instance and therefore one batched `perform` call. ```ruby # Two fields requesting different models — two separate loader instances, two batched queries: def product(id:) RecordLoader.for(Product).load(id) end def image(id:) RecordLoader.for(Image).load(id) end # Two fields requesting the same model — one shared loader instance, one batched query: def product1(id:); RecordLoader.for(Product).load(id); end def product2(id:); RecordLoader.for(Product).load(id); end # Both ids are collected → perform([id1, id2]) — only one DB query fired ``` ``` -------------------------------- ### Window Key Loader for Paginated Has-Many Source: https://context7.com/shopify/graphql-batch/llms.txt Loads the first N associated records per parent using a PostgreSQL ROW_NUMBER() window function. Use this for efficient paginated association loading in a single batched query. ```ruby class WindowKeyLoader < GraphQL::Batch::Loader def initialize(model, foreign_key, limit:, order_col:, order_dir: :asc) super() @model = model @foreign_key = foreign_key @limit = limit @order_col = order_col @order_dir = order_dir end def perform(foreign_ids) ranked_sql = @model.select( "*", "row_number() OVER (PARTITION BY #{@foreign_key} ORDER BY #{@order_col} #{@order_dir}) AS rank" ).where(@foreign_key => foreign_ids).to_sql records = @model.from("(#{ranked_sql}) AS #{@model.table_name}") .where("rank <= #{@limit}").to_a foreign_ids.each do |fid| fulfill(fid, records.select { |r| fid == r.public_send(@foreign_key) }) end end end ``` ```ruby # Field resolver — fetches first 5 events per category, all categories in one query: def events(first: 5) WindowKeyLoader.for(Event, :category_id, limit: first, order_col: :start_time, order_dir: :desc) .load(object.id) end ``` ```sql # SQL produced (for 3 categories): # SELECT * FROM ( # SELECT *, row_number() OVER (PARTITION BY category_id ORDER BY start_time DESC) AS rank # FROM events WHERE category_id IN (1,2,3) # ) AS events WHERE rank <= 5 ``` -------------------------------- ### Transform and Chain Loads with `.then` Source: https://context7.com/shopify/graphql-batch/llms.txt Leverage `.then` to transform loaded values or chain dependent asynchronous operations. Blocks returning Promises are automatically flattened, simplifying promise composition. ```ruby # Transform the loaded value: def product_title(id:) RecordLoader.for(Product).load(id).then { |product| product&.title } end ``` ```ruby # Chain a dependent load (inner load is also batched with other concurrent loads): def product_image(id:) RecordLoader.for(Product).load(id).then do |product| RecordLoader.for(Image).load(product.image_id) end end ``` ```ruby # Error handling — second lambda argument handles exceptions: def product_safe(id:) CacheLoader.for(Product).load(id).then(nil, ->(err) { raise err unless err.is_a?(Redis::BaseConnectionError) RecordLoader.for(Product).load(id) # fallback to DB }) end ``` -------------------------------- ### Defining a Loader Source: https://context7.com/shopify/graphql-batch/llms.txt Subclass `GraphQL::Batch::Loader`, implement `perform(keys)`, and use `fulfill(key, value)` to load data. Loaders can be configured with grouping arguments in `initialize`. ```APIDOC ## Defining a Loader — `GraphQL::Batch::Loader` Subclass `GraphQL::Batch::Loader`, optionally accept grouping arguments in `initialize`, and implement `perform(keys)`. Inside `perform`, call `fulfill(key, value)` for every key, and `fulfill(key, nil)` for any key that had no result (or use `fulfilled?` to guard). ```ruby class RecordLoader < GraphQL::Batch::Loader # initialize args define the "group" — loaders with different args are separate instances def initialize(model, column: model.primary_key, where: nil) super() @model = model @column = column.to_s @column_type = model.type_for_attribute(@column) @where = where end def load(key) # Cast the key to the correct column type before caching super(@column_type.cast(key)) end def perform(keys) scope = @model scope = scope.where(@where) if @where scope.where(@column => keys).each { |record| fulfill(record.public_send(@column), record) } keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } end end # Usage in a field resolver: def product(id:) RecordLoader.for(Product).load(id) # => returns a Promise end def products(ids:) RecordLoader.for(Product).load_many(ids) # => returns a Promise> end # Load by a non-primary-key column with a scope: def active_product_by_slug(slug:) RecordLoader.for(Product, column: :slug, where: { active: true }).load(slug) end ``` ``` -------------------------------- ### Define a Custom RecordLoader Source: https://context7.com/shopify/graphql-batch/llms.txt Subclass `GraphQL::Batch::Loader` to implement batched data loading. Implement `perform(keys)` to fetch data and use `fulfill(key, value)` to return results. Loaders with different `initialize` arguments are treated as separate instances. ```ruby class RecordLoader < GraphQL::Batch::Loader # initialize args define the "group" — loaders with different args are separate instances def initialize(model, column: model.primary_key, where: nil) super() @model = model @column = column.to_s @column_type = model.type_for_attribute(@column) @where = where end def load(key) # Cast the key to the correct column type before caching super(@column_type.cast(key)) end def perform(keys) scope = @model scope = scope.where(@where) if @where scope.where(@column => keys).each { |record| fulfill(record.public_send(@column), record) } keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } end end ``` -------------------------------- ### Register GraphQL::Batch Plugin Source: https://context7.com/shopify/graphql-batch/llms.txt Register GraphQL::Batch as a plugin in your GraphQL schema. Ensure the mutation type is defined before `use GraphQL::Batch` for correct cache clearing between mutations. ```ruby require 'graphql/batch' class MySchema < GraphQL::Schema query Types::QueryType mutation Types::MutationType # must be defined BEFORE use GraphQL::Batch use GraphQL::Batch # Custom executor class can be injected: # use GraphQL::Batch, executor_class: MyCustomExecutor end ``` -------------------------------- ### Execute Parallel Independent Loads with `Promise.all` Source: https://context7.com/shopify/graphql-batch/llms.txt Use `Promise.all` to group independent data loads, allowing them to be batched concurrently. This method combines the results into an array once all promises resolve. ```ruby def collection_counts Promise.all([ CountLoader.for(Shop, :smart_collections).load(context[:shop_id]), CountLoader.for(Shop, :custom_collections).load(context[:shop_id]), ]).then(&:sum) # Both CountLoader calls are batched; total = smart + custom in one pass end ``` ```ruby # Mixed promises and plain values are supported: Promise.all([RecordLoader.for(Product).load(1), "static", 42]).sync # => [, "static", 42] ``` -------------------------------- ### Run Batched Loads Outside Schema with `GraphQL::Batch.batch` Source: https://context7.com/shopify/graphql-batch/llms.txt Employ `GraphQL::Batch.batch` to execute batched operations within a standalone context, ideal for unit testing loaders in isolation. Nested calls to `batch` will reuse the existing executor. ```ruby # Unit test a loader without a full schema execution: def test_single_record_load product = products(:snowboard) loaded = GraphQL::Batch.batch do RecordLoader.for(Product).load(product.id).then(&:title) end assert_equal product.title, loaded end ``` ```ruby # Nested calls share the same executor — the inner batch is a no-op: GraphQL::Batch.batch do p1 = RecordLoader.for(Product).load(1) GraphQL::Batch.batch do # reuses outer executor p2 = RecordLoader.for(Product).load(1) # p1 == p2 (same cached Promise) end p1 end ``` -------------------------------- ### Prime the loader cache Source: https://github.com/shopify/graphql-batch/blob/main/README.md Pre-populate the loader cache with specific key-value pairs using RecordLoader.for.prime(key, value). This is useful when you already have the data and want to avoid an unnecessary database lookup. ```ruby def liked_products liked_products = Product.where(liked: true).load liked_products.each do |product| RecordLoader.for(Product).prime(product.id, product) end end ``` -------------------------------- ### Require graphql-batch library Source: https://github.com/shopify/graphql-batch/blob/main/README.md Ensure the graphql-batch library is required in your application before use. ```ruby require 'graphql/batch' ``` -------------------------------- ### Implement Instrumentation with `Loader#around_perform` Source: https://context7.com/shopify/graphql-batch/llms.txt Override `around_perform` in custom loaders to add instrumentation like timing or logging around the `perform` method without altering the batching logic. ```ruby class InstrumentedRecordLoader < RecordLoader def around_perform start = Process.clock_gettime(Process::CLOCK_MONOTONIC) yield ensure elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start Rails.logger.info "[RecordLoader] perform took #{(elapsed * 1000).round(2)}ms" StatsD.timing("record_loader.perform", elapsed) end end ``` -------------------------------- ### Loading a Single Value with Loader#load Source: https://context7.com/shopify/graphql-batch/llms.txt The `load(key)` method returns a Promise for the value associated with the key. Results are cached, so calling `load` with the same key multiple times returns the same Promise. ```ruby class Types::ProductType < GraphQL::Schema::Object field :title, String, null: false field :category, Types::CategoryType, null: true def title # Synchronous value — no promise needed, returned directly object.title end def category # Returns Promise; the executor resolves it after all concurrent loads RecordLoader.for(Category).load(object.category_id) end end ``` -------------------------------- ### Use GraphQL::Batch as a schema plugin Source: https://github.com/shopify/graphql-batch/blob/main/README.md Integrate GraphQL::Batch into your GraphQL schema by using it as a plugin after defining mutations. This ensures the cache is cleared after mutations are resolved. ```ruby class MySchema < GraphQL::Schema query MyQueryType mutation MyMutationType use GraphQL::Batch end ``` -------------------------------- ### Loading a Single Value Source: https://context7.com/shopify/graphql-batch/llms.txt `Loader#load(key)` returns a `Promise` for the value associated with `key`. Results are cached, and calling `load` with the same key twice returns the same Promise object. ```APIDOC ## `Loader#load(key)` — Loading a Single Value Returns a `Promise` for the value associated with `key`. Results are cached: calling `load` with the same key twice returns the same Promise object. ```ruby class Types::ProductType < GraphQL::Schema::Object field :title, String, null: false field :category, Types::CategoryType, null: true def title # Synchronous value — no promise needed, returned directly object.title end def category # Returns Promise; the executor resolves it after all concurrent loads RecordLoader.for(Category).load(object.category_id) end end ``` ``` -------------------------------- ### Unit test loaders with GraphQL::Batch.batch Source: https://github.com/shopify/graphql-batch/blob/main/README.md Test loaders outside of a GraphQL query by wrapping the loader calls within GraphQL::Batch.batch. This method sets up the necessary thread-local state for batch loading and clears it afterward. ```ruby def test_single_query product = products(:snowboard) title = GraphQL::Batch.batch do RecordLoader.for(Product).load(product.id).then(&:title) end assert_equal product.title, title end ``` -------------------------------- ### Custom Executor for Monitoring Source: https://context7.com/shopify/graphql-batch/llms.txt Inject a custom Executor subclass to add cross-cutting concerns like monitoring unbatched queries. The executor.loading flag is true while perform is running. ```ruby class InstrumentedExecutor < GraphQL::Batch::Executor # executor.loading is true while perform is running; # subscribe to ActiveSupport::Notifications to flag queries issued outside perform end ``` ```ruby class MySchema < GraphQL::Schema use GraphQL::Batch, executor_class: InstrumentedExecutor end ``` ```ruby # Or in standalone batch blocks: GraphQL::Batch.batch(executor_class: InstrumentedExecutor) do RecordLoader.for(Product).load(1) end ``` -------------------------------- ### Active Storage Loader for Attachments Source: https://context7.com/shopify/graphql-batch/llms.txt Batch-loads Active Storage attachments, supporting STI. Use `association_type: :has_many_attached` for multiple attachments. ```ruby module Loaders class ActiveStorageLoader < GraphQL::Batch::Loader def initialize(record_type, attachment_name, association_type: :has_one_attached) super() @record_type = record_type @attachment_name = attachment_name @association_type = association_type end def perform(record_ids) attachments = ActiveStorage::Attachment.includes(:blob).where( record_type: @record_type.to_s, record_id: record_ids, name: @attachment_name ) if @association_type == :has_one_attached attachments.each { |a| fulfill(a.record_id, a) } record_ids.each { |id| fulfill(id, nil) unless fulfilled?(id) } else record_ids.each do |record_id| fulfill(record_id, attachments.select { |a| a.record_id == record_id }) end end end end end ``` ```ruby # Field resolvers: def image Loaders::ActiveStorageLoader.for(:Event, :image).load(object.id).then do |attachment| attachment && Rails.application.routes.url_helpers.url_for(attachment.variant(quality: 75)) end end def pictures Loaders::ActiveStorageLoader.for(:Event, :pictures, association_type: :has_many_attached) .load(object.id) .then { |pics| pics.map { |p| Rails.application.routes.url_helpers.url_for(p.variant(quality: 75)) } } end ``` -------------------------------- ### Load a single record using RecordLoader Source: https://github.com/shopify/graphql-batch/blob/main/README.md Resolve a field by loading a single record using RecordLoader.for.load(id). This method returns a promise for the requested record. ```ruby field :product, Types::Product, null: true do argument :id, ID, required: true end def product(id:) RecordLoader.for(Product).load(id) end ``` -------------------------------- ### Load multiple records using RecordLoader Source: https://github.com/shopify/graphql-batch/blob/main/README.md Resolve a field by loading multiple records using RecordLoader.for.load_many(ids). This is useful for fetching arrays of records based on their IDs. ```ruby field :products, [Types::Product, null: true], null: false do argument :ids, [ID], required: true end def products(ids:) RecordLoader.for(Product).load_many(ids) end ``` -------------------------------- ### Usage of RecordLoader in Field Resolvers Source: https://context7.com/shopify/graphql-batch/llms.txt Use `RecordLoader.for(Model).load(id)` to load a single record and `load_many(ids)` for multiple records. This returns a Promise that the executor resolves. ```ruby # Usage in a field resolver: def product(id:) RecordLoader.for(Product).load(id) # => returns a Promise end def products(ids:) RecordLoader.for(Product).load_many(ids) # => returns a Promise> end # Load by a non-primary-key column with a scope: def active_product_by_slug(slug:) RecordLoader.for(Product, column: :slug, where: { active: true }).load(slug) end ``` -------------------------------- ### HTTP Loader for Concurrent External Requests Source: https://context7.com/shopify/graphql-batch/llms.txt Fans out HTTP requests concurrently using `concurrent-ruby` and `connection_pool`. Satisfies the synchronous promise interface. ```ruby module Loaders class HTTPLoader < GraphQL::Batch::Loader def initialize(host:, size: 4, timeout: 4) super() @host = host @size = size @timeout = timeout end def perform(operations) # Fan out — start all requests concurrently futures = operations.map do |op| Concurrent::Promises.future { pool.with { |conn| op.call(conn) } } end # Converge — block on each future and fulfill its promise operations.each_with_index { |op, i| fulfill(op, futures[i].value) } end private def pool @pool ||= ConnectionPool.new(size: @size, timeout: @timeout) { HTTP.persistent(@host) } end end end ``` ```ruby # Field resolver: def weather(lat:, lng:, lang:) key = Rails.application.credentials.darksky_key path = "/forecast/#{key}/#{lat},#{lng}?lang=#{lang}" Loaders::HTTPLoader.for(host: 'https://api.darksky.net') .load(->(conn) { conn.get(path).flush }) .then { |resp| resp.status.ok? ? JSON.parse(resp.body).dig('currently', 'summary') : nil } end # GraphQL query batches both city lookups into one perform call — 2 concurrent HTTP requests: # { montreal: weather(lat: 45.5017, lng: -73.5673, lang: "fr") # waterloo: weather(lat: 43.4643, lng: -80.5204, lang: "en") } ``` -------------------------------- ### Load Multiple Records with `Loader#load_many` Source: https://context7.com/shopify/graphql-batch/llms.txt Use `load_many` to efficiently fetch multiple records by their IDs. This method ensures that a single database query is executed for all requested IDs, preserving the order of the results. ```ruby class Types::QueryType < GraphQL::Schema::Object field :products, [Types::ProductType, { null: true }], null: false do argument :ids, [ID], required: true end def products(ids:) RecordLoader.for(Product).load_many(ids) # GraphQL query: { products(ids: ["1","2","3"]) { id title } } # Fires exactly ONE query: SELECT * FROM products WHERE id IN (1, 2, 3) # Returns Promise<[Product, nil, Product]> preserving order end end ``` -------------------------------- ### Chain dependent queries with .then Source: https://github.com/shopify/graphql-batch/blob/main/README.md Chain multiple asynchronous operations where one depends on the result of another. The query block can return another query, which will be executed sequentially. ```ruby def product_image(id:) RecordLoader.for(Product).load(id).then do |product| RecordLoader.for(Image).load(product.image_id) end end ``` -------------------------------- ### Handle exceptions with .then's second argument Source: https://github.com/shopify/graphql-batch/blob/main/README.md Provide a fallback mechanism for exceptions during promise resolution by passing a second lambda to .then. This lambda handles exceptions and can execute alternative logic. ```ruby def product(id:) # Try the cache first ... CacheLoader.for(Product).load(id).then(nil, lambda do |exc| # But if there's a connection error, go to the underlying database raise exc unless exc.is_a?(Redis::BaseConnectionError) logger.warn err.message RecordLoader.for(Product).load(id) end) end ``` -------------------------------- ### Custom Cache Key for AssociationLoader Source: https://context7.com/shopify/graphql-batch/llms.txt Override `cache_key` to control how loaded keys are deduplicated. Use `object_id` to ensure records with the same database ID are loaded independently. ```ruby class AssociationLoader < GraphQL::Batch::Loader def initialize(model, association_name) super() @model = model @association_name = association_name validate end # Use object_id so two records with the same DB id are each loaded separately def cache_key(record) record.object_id 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 def perform(records) ::ActiveRecord::Associations::Preloader.new(records: records, associations: @association_name).call records.each { |record| fulfill(record, read_association(record)) } end private def validate raise ArgumentError, "No association #{@association_name} on #{@model}" unless @model.reflect_on_association(@association_name) end def read_association(record) = record.public_send(@association_name) def association_loaded?(record) = record.association(@association_name).loaded? end ``` ```ruby # Usage: def variants AssociationLoader.for(Product, :variants).load(object) end ``` -------------------------------- ### Transform query results with .then Source: https://github.com/shopify/graphql-batch/blob/main/README.md Use the .then method on a promise returned by a loader to transform the resolved data. This allows for chaining operations on the loaded records. ```ruby def product_title(id:) RecordLoader.for(Product).load(id).then do |product| product.title end end ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.