Grape::Entity::Preloader

Ruby Gem Version

Grape::Entity::Preloader allows preload associations and callbacks for avoiding N+1 operations in Grape::Entity.

Installation

bundle add grape-entity-preloader

If bundler is not being used to manage dependencies, install the gem by executing:

gem install grape-entity-preloader

Usage

Activation

Global Activation

You can enable the preloader globally. This is useful in environments where you want preloading to be the default behavior.

# config/initializers/grape_entity_preloader.rb
Grape::Entity::Preloader.enabled!

Local Activation and Deactivation

You can control preloading for specific represent calls. For a specific block of code, you can use with_enable or with_disable. This is useful in contexts like API endpoints or middlewares.

Grape::Entity::Preloader.with_enable do
  # Preloading is enabled for all represent calls inside this block
  MyAPI::Entities::User.represent(User.all)
end

Grape::Entity::Preloader.with_disable do
  # Preloading is disabled for all represent calls inside this block
  MyAPI::Entities::User.represent(User.all)
end

preload

Use preload to configure preloading for an exposure. It can be a Symbol to preload an ActiveRecord association, a Proc to run a custom preload callback, or an Array with an optional condition.

Association preloading

A Symbol value preloads the named association:

class UserEntity < Grape::Entity
  expose :id
  expose :name
  # This will preload the `books` association for all users being represented.
  expose :books, using: BookEntity, preload: :books
end

# In your API
users = User.limit(10)
# When UserEntity represents users, it will execute two queries:
# 1. SELECT * FROM users LIMIT 10
# 2. SELECT * FROM books WHERE books.user_id IN (...)
UserEntity.represent(users)

For nested preloading:

class BookEntity < Grape::Entity
  expose :id
  expose :title
  # This will preload tags for each book
  expose :tags, using: TagEntity, preload: :tags
end

class UserEntity < Grape::Entity
  expose :id
  expose :name
  expose :books, using: BookEntity, preload: :books
end

# It will generate 3 queries instead of 1 + 10 (for books) + N (for tags)
UserEntity.represent(User.limit(10))

Custom preloading callback

For more complex scenarios that association preloading doesn't cover (e.g., loading data from other services, custom caching logic), you can use a Proc that accepts two arguments:

  1. objects: An array of the parent objects being represented.
  2. options: The Grape::Entity::Options object for the current representation context.

This is the same objects / options signature used by conditional preloading (see below).

The Proc must return a Hash mapping each parent object to its preloaded value. The preloader stores this Hash in options and reads from it when rendering the exposure. If the cache is not available, the exposure fallback to its normal value method (delegation or block).

class UserStatsEntity < Grape::Entity
  expose :likes
  expose :followers
end

class UserEntity < Grape::Entity
  expose :id
  expose :name

  expose :stats, using: UserStatsEntity, preload: ->(users, _options) do
    # `users` is an array of User objects.
    # Here you can fetch stats for all users in one batch.
    stats_by_user_id = StatsService.batch_get_by_user_ids(users.map(&:id))

    # Return a Hash mapping each user to its stats object.
    users.to_h { |user| [user, stats_by_user_id[user.id]] }
  end do |user, _options|
    # Fallback when the preloader is disabled.
    StatsService.get(user.id)
  end
end

Callback deduplication

When several expose declarations need the same preloaded data, reference the same preload callback Proc for each of them. The preloader will run that callback once, store the resulting Hash in options, and reuse it for all of the associated exposures, avoiding duplicate work.

class UserEntity < Grape::Entity
  stats_callback = ->(users, _options) do
    stats_by_user_id = StatsService.batch_get(users.map(&:id))
    users.to_h { |user| [user, stats_by_user_id[user.id]] }
  end

  expose :public_stats, using: StatsEntity, preload: stats_callback do |user, _options|
    StatsService.get(user.id)
  end

  expose :private_stats, using: StatsEntity, preload: stats_callback do |user, _options|
    StatsService.get(user.id)
  end
end

Conditional preloading

When you need a condition, pass an Array where the first element is the preload value and the second element is a Proc that accepts one argument: options.

If the condition Proc returns a falsy value, preloading for that exposure will be skipped.

class UserEntity < Grape::Entity
  expose :id
  expose :name

  # The :audit_log association will only be preloaded if `include_audit_log` is true in the options.
  expose :audit_log,
         using: AuditLogEntity,
         preload: [:audit_log, ->(options) { options[:include_audit_log] }]
end

# Preloading for :audit_log is skipped
UserEntity.represent(user)

# Preloading for :audit_log is executed
UserEntity.represent(user, include_audit_log: true)

For a callback with a condition:

class UserEntity < Grape::Entity
  expose :stats,
         using: UserStatsEntity,
         preload: [
           ->(users, _options) { StatsService.batch_get(users) },
           ->(options) { options[:include_stats] }
         ]
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/OuYangJinTing/grape-entity-preloader. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Grape::Entity::Preloader project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.