Skip to content

Instantly share code, notes, and snippets.

@arturopuente
Last active February 29, 2024 19:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arturopuente/0dbfcf6b309b9ca8d76d8e8ce98e5712 to your computer and use it in GitHub Desktop.
Save arturopuente/0dbfcf6b309b9ca8d76d8e8ce98e5712 to your computer and use it in GitHub Desktop.
JSON-API Preloadable concern
module Preloadable
extend ActiveSupport::Concern
# A very common issue with APIs is that we want to preload relationships to
# avoid N+1 queries, something like this: /api/projects?include=users
# and this works well for attributes like the user name or their email, but
# what happens when you try to access their avatar? Yup. N+1.
# JSON:API, being a more generic standard, doesn't really know or give you
# tools to deal with Rails relationships and preloads easily. While this makes
# sense from their (and the library authors) perspective, we can do better.
# What about self.records, you ask? Ah, good question, yes, self.records
# allows us to specify the relationships we want, but by default it works on
# _every_ request, we want to have the ability to customize what each resource
# included relationship looks like for every request.
# The goal of this concern is to allow us to ask the resource for a specific
# set of plain or nested relationships to be included via a URL param.
# We'll work with two elements, a k/v object named PRELOAD_ATTRIBUTES that
# holds the relationships we are able of preloading, and a list of default
# preloads named DEFAULT_PRELOAD_ATTRIBUTES (ideally this should be empty/not
# exist for new resources, but it's not because we were overriding the
# self.records methods on previous resources). It looks like this:
# USER_PRELOAD_ATTRIBUTES = [
# { avatar_attachment: :blob }
# ]
#
# PRELOAD_ATTRIBUTES = {
# centers: [:centers],
# institutions: [:institutions],
# organizations: [:centers, :institutions],
# memberships: [
# :project_memberships,
# :project_external_memberships,
# :external_user_members,
# members: USER_PRELOAD_ATTRIBUTES
# ],
# users: [
# :external_user_members,
# members: USER_PRELOAD_ATTRIBUTES
# ],
# }
#
# DEFAULT_PRELOAD_ATTRIBUTES = [
# :organizations,
# :memberships,
# ]
# Notice a couple of important things here:
# The value of the preload k/v pairs must always be an array.
# You can define auxiliary lists of properties/objects to deal with nested
# relationships in multiple places, e.g.: USER_PRELOAD_ATTRIBUTES.
# This allows you to nest attributes as much as neeed to avoid N+1 on
# associated resources, e.g.: retrieve the ActiveStorage blob of the avatar
# attachment of a list of users
# How does this work on the request level? Instead of using the include URL
# param we'll use the preload param:
# /api/projects?include=users,centers => /api/projects?preload=users,centers
# Our api/application_controller will parse it and include it in the context
# attribute, and this makes it available in all the resources methods that
# receive an options hash from the library (self.records, custom_links).
# Don't forget you still have to declare the relationships you want on the k/v
# hash on the resource!
class_methods do
def records(options = {})
default_attrs = :DEFAULT_PRELOAD_ATTRIBUTES
attrs = const_defined?(default_attrs) ? const_get(default_attrs) : {}
if options[:context][:preload].present?
attrs = options[:context][:preload].split(",").map(&:to_sym)
end
return super if attrs.empty?
preload_attrs = const_get(:PRELOAD_ATTRIBUTES)
includable = preload_attrs.keys.flat_map do |key|
preload_attrs[key] if attrs.include?(key)
end.compact
super.includes(includable)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment