procore-oss / blueprinter

Simple, Fast, and Declarative Serialization Library for Ruby
MIT License
1.14k stars 109 forks source link

Support BatchLoader #433

Closed jesseduffield closed 4 months ago

jesseduffield commented 5 months ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe

I just stumbled across the same problem that this article stumbled across: https://ananthakumaran.in/2023/01/01/solving_n_plus_1_queries_on_rails.html

Describe the feature you'd like to see implemented

I'm using BatchLoader (https://github.com/exAspArk/batch-loader) to lazily load some associated data that I then want to render via a blueprint. BatchLoader makes use of LazyObject which is kind of like a promise in that it doesn't resolve until you call a method on it. Here's an example usage:

class AuthorBlueprint < ApplicationBlueprint
  identifier :id

  field :book_count
end

class Author < ApplicationRecord
  def book_count
    BatchLoader.for(id).batch do |ids, loader|
      Book.where(author_id: ids).group(:author_id).count.each do |author_id, count|
        loader.call(author_id, count)
      end
    end
  end
end

What I want to happen is for only a single query to be called in order to obtain the book counts for each author returned by my authors index endpoint. But, it appears that blueprinter calls some method on the book_count value right away, rather than waiting until it needs to convert the whole response to JSON. This breaks the batch loading approach.

Interestingly, if I wrap book_count in a hash, batch loading works.

Is this something that we could support in blueprinter? Or alternatively is there some kind of custom extractor I could use? Thanks!

Describe alternatives you've considered

None

Additional context

No response

lessthanjacob commented 4 months ago

Hey Jesse! Thanks for bringing this to our attention.

At the moment, the default mechanism for extracting field values will end up invoking forcing BatchLoader to evaluate for each object (as you and the article call out). This is primarily a side effect of:

  1. Determining if the value should be formatted into the expected DateTime format.
  2. A check to determine if a default value should be supplied instead of the current value.

We're planning some substantial overhauls to how extractors/formatters/transformers work, but in the short-term, this behavior may be difficult to unwind. As a temporary workaround, you should be able to specify the PublicSendExtractor for that field, which should prevent preemptive evaluation of the value:

class AuthorBlueprint < ApplicationBlueprint
  identifier :id

  field :book_count, extractor: ::Blueprinter::PublicSendExtractor.new 
end

Just note that since this is part of the private API, this may not be fully supported in future versions!