The project is in a healthy, maintained state
Eager loading and other ActiveRecord helpers for Blueprinter
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 2.5
~> 5.0
~> 13.0
~> 1.4

Runtime

>= 6.0, < 7.2
 Project Readme

Discord

blueprinter-activerecord

blueprinter-activerecord is a blueprinter extension to help you easily preload the associations from your Blueprints, N levels deep. It also provides logging extensions so you can measure how effective the primary extension is/will be.

Installation

Add blueprinter-activerecord to your Gemfile and enable the extension using one of the configurations below.

Configurations

Automatic mode

In automatic mode, every query (ActiveRecord::Relation) passed to a Blueprint will automatically be preloaded during render.

Blueprinter.configure do |config|
  config.extensions << BlueprinterActiveRecord::Preloader.new(auto: true)
end

If you'd prefer to use includes rather than preload, pass use: :includes.

Dynamic mode

In dynamic mode, each query passed to a Blueprint is evaluated by the block. If it returns true, the query will be preloaded during render.

Blueprinter.configure do |config|
  config.extensions << BlueprinterActiveRecord::Preloader.new do |q, blueprint, view, options|
    # examine q, q.model, blueprint, view, or options and return true or false
  end
end

If you'd prefer to use includes rather than preload, pass use: :includes.

Manual mode

In manual mode, nothing happens automatically; you'll need to opt individual queries into preloading.

Blueprinter.configure do |config|
  config.extensions << BlueprinterActiveRecord::Preloader.new
end

The preload_blueprint method is used to opt queries in:

q = Widget.
  where(...).
  order(...).
  preload_blueprint

# preloading happens during "render"
json = WidgetBlueprint.render(q, view: :extended)

If you'd prefer to use includes or eager_load rather than preload, pass the use option:

  preload_blueprint(use: :includes)

Notes on use

Pass the query to render, not query results

If the query runs before being passed to render, no preloading can take place.

# Oops - the query already ran :(
widgets = Widget.where(...).to_a
WidgetBlueprint.render(widgets, view: :extended)

# Yay! :)
widgets = Widget.where(...)
WidgetBlueprint.render(widgets, view: :extended)

If you must run the query first, there is a way:

widgets = Widget.
  where(...).
  # preloading will happen HERE b/c we gave it all the info it needs
  preload_blueprint(WidgetBlueprint, :extended).
  to_a
do_something widgets
WidgetBlueprint.render(widgets, view: :extended)

Look out for hidden associations

blueprinter-activerecord may feel magical, but it's not magic. Some associations may be "hidden" and you'll need to preload them the old-fashioned way.

# Here's a Blueprint with one association and one field
class WidgetBlueprint < Blueprinter::Base
  association :category, blueprint: CategoryBlueprint
  field :parts_description
  ...
end

class Widget < ActiveRecord::Base
  belongs_to :category
  has_many :parts

  # The field is this instance method, and Blueprinter can't see inside it
  def parts_description
    # I'm calling the "parts" association but no one knows!
    parts.map(&:description).join(", ")
  end
end

q = Widget.where(...).order(...).
  # Since "category" is declared in the Blueprint, it will automatically be preloaded during "render".
  # But because "parts" is hidden inside of a method call, we must manually preload it.
  preload(:parts).
  # catch any other hidden associations
  strict_loading

WidgetBlueprint.render(q)

Rails 6.1 added support for strict_loading. Depending on your configuration, it will either raise exceptions or log warnings if a query triggers any lazy loading. Very useful for catching any associations Blueprinter can't see.

Logging

There are two different logging extensions. You can use them together or separately to measure how much the Preloder extension is, or can, help your application.

Missing Preloads Logger

This extension is useful for measuring how helpful BlueprinterActiveRecord::Preloader will be for your application. It can be used with or without Preloader. Any Blueprint-rendered queries not caught by the Preloader extension will be caught by this logger.

Blueprinter.configure do |config|
  # Preloader (optional) may be in in manual or dynamic mode
  config.extensions << BlueprinterActiveRecord::Preloader.new

  # Catches any Blueprint-rendered queries that aren't caught by Preloader
  config.extensions << BlueprinterActiveRecord::MissingPreloadsLogger.new do |info|
    Rails.logger.info({
      event: "missing_preloads",
      root_model: info.query.model.name,
      sql: info.query.to_sql,
      missing: info.found.map { |x| x.join " > " },
      percent_missing: info.percent_found,
      total: info.num_existing + info.found.size,
      visible: info.visible.size,
      trace: info.trace,
    }.to_json)
  end
end

Added Preloads Logger

This extension measures how many missing preloads are being found & fixed by the preloader. Any query caught by this extension won't end up in MissingPreloadsLogger.

Blueprinter.configure do |config|
  # Preloader (required) may be in any mode
  config.extensions << BlueprinterActiveRecord::Preloader.new

  # Catches any queries found by Preloader
  config.extensions << BlueprinterActiveRecord::AddedPreloadsLogger.new do |info|
    Rails.logger.info({
      event: "added_preloads",
      root_model: info.query.model.name,
      sql: info.query.to_sql,
      added: info.found.map { |x| x.join " > " },
      percent_added: info.percent_found,
      total: info.num_existing + info.found.size,
      visible: info.visible.size,
      trace: info.trace,
    }.to_json)
  end
end

Rake task

Curious what exactly preload_blueprint is going to preload? There's a rake task to pretty-print the whole tree. Pass it the Blueprint, view, and ActiveRecord model:

bundle exec rake blueprinter:activerecord:preloads[WidgetBlueprint,extended,Widget]
{
  :customer => {
    :contacts => {},
    :address => {},
  },
  :parts => {},
}

Testing

bundle install
bundle exec appraisal install
bundle exec appraisal rake test