graphiti icon indicating copy to clipboard operation
graphiti copied to clipboard

Add support for caching in Graphiti

Open jkeen opened this issue 2 years ago • 1 comments

Graphiti doesn't have much of a caching story right now. For a while I used @richmolj's suggestion of ActionCaching (#160) often I ended up rolling my own logic which sometimes in the case of resources with sideloads required a bunch of duplicated effort outside of graphiti (parsing params, determining sideloads, etc) that I knew graphiti was already kinda doing.

This PR exposes some things that make caching the most time-intensive part of the request cycle (the rendering) with Graphiti a snap. Most of this leverages built-in Rails caching methods, but we have to do some extra work to make sure we respect sideloads.

Changes:

  • expose cache_key method to resource instance this generates a combined stable cache key based on resource identifiers specified by the resource class, the specified sideloads, and any specified extra_fields or fields, pages, or links which will affect the response.

  • expose cache_key_with_version method to resource instance same as above, but with the last modified dates added in. If any included resource's updated_at changes, this key will change.

  • expose updated_at method to resource instance returns the max updated_at date of the resource and any specified sideloads

  • expose etag method to resource instance generate a Weak Etag based on the cache_key_with_version response. With etag and updated_at methods on a resource instance, using stale?(@resource) will respect them.

  • for cached resources, rendering logic in Graphiti is wrapped in a cache block Rails.cache.fetch(@resource.cache_key, version: @resource.updated_at) { [expensive rendering] }.

    (Using cache_key and version together by default instead of using cache_key_with_version as the key better ensures we won’t flood a cache store with dead keys)

For an end user to have easy cached resources that behave just like Rails, they’d set up their cache store and do the following:

class EmployeesController < ApplicationResource
  cache_resource
end
class EmployeesController < ApplicationController
  def index
   @employees = Employees.all(params)
   respond_with @employees if stale?(@employees)
  end
end

jkeen avatar Jun 28 '22 13:06 jkeen

@richmolj This concludes my blast of PRs for a while 😝 #422 and #423 were just leading to this in order to keep things topical.

There might be a few things still to add to this, but thought this was a good enough starting point to talk about it. This has made a huuuugge difference in my app, and I'm super stoked about this addition.

json-api resources has a kinda-similar caching strategy, but with more configuration. I liked how they defined it on the Resource definition. I initially had it as a :cache argument being passed into the .find or .all method and this ended up feeling better.

jsonapi-renderer which we're leveraging for rendering has a caching strategy that I'm specifically working around in this PR, because I couldn't get it to work at all. The idea of individually caching each resource response fragment sounds good in theory, but cutting it off at this point made sense until a reason to do otherwise presented itself

jkeen avatar Jun 28 '22 14:06 jkeen

:tada: This PR is included in version 1.7.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

github-actions[bot] avatar Mar 27 '24 23:03 github-actions[bot]