Skip to content

Instantly share code, notes, and snippets.

@rmosolgo
Last active December 10, 2024 21:57
Show Gist options
  • Save rmosolgo/accca5dd4b4ae7b0b9713054bbf6c1af to your computer and use it in GitHub Desktop.
Save rmosolgo/accca5dd4b4ae7b0b9713054bbf6c1af to your computer and use it in GitHub Desktop.
Caching top-level lists with GraphQL-Ruby when new items are created
require "bundler/inline"
gemfile do
gem "graphql", "2.4.7"
gem "graphql-enterprise", source: "https://gems.graphql.pro"
gem "activerecord"
gem "sqlite3"
end
# Set up the database for the example
require "active_record"
ActiveRecord::Base.establish_connection({ adapter: "sqlite3", database: ":memory:" })
ActiveRecord::Schema.define do
create_table :books, force: true do |t|
t.string :title
t.timestamps
end
end
class Book < ActiveRecord::Base; end
Book.create!(title: "That Distant Land")
Book.create!(title: "Historical Brewing Techniques")
Book.create!(title: "Ruby under a Microscope")
# This object exists to bust the cache. It behaves like a database record in that it:
#
# - has an `#id` which uniquely identifies it
# - can be "refetched" using `.find`
#
# However, it doesn't actually fetch records from the database. Instead,
# its value is based on _counting_ records from the database.
#
# This simple implementation could call `.all` on _any_ ActiveRecord model.
#
# To cache the result of a more sophisticated query, you could specialize it like the example class below.
class CachedList
def initialize(cached_list_class_name)
@cached_list_class_name = cached_list_class_name
end
def to_param
cache_key = Object.const_get(@cached_list_class_name).all.cache_key
"#{@cached_list_class_name}/#{cache_key}"
end
def id
@cached_list_class_name
end
def self.find(cached_list_class_name)
self.new(cached_list_class_name)
end
end
class FilteredBookList
# Make it work with `object_from_id`
def self.find(_id)
self.new
end
# Make it work with `id_from_object`
def id
self.class.name
end
# Make it work with ObjectCache
def to_param
books.cache_key
end
# Here's the filtered list of items
def books
# For example books whose titles begin with "T"
@books ||= Book.where("title LIKE 'T%'")
end
end
class MySchema < GraphQL::Schema
class BaseField < GraphQL::Schema::Field
include GraphQL::Enterprise::ObjectCache::FieldIntegration
cacheable(public: true)
end
class BaseObject < GraphQL::Schema::Object
field_class(BaseField)
include GraphQL::Enterprise::ObjectCache::ObjectIntegration
cacheable(public: true)
end
class Book < BaseObject
field :title, String
end
class Query < BaseObject
# For a simple implementation using `all`
field :books, [Book]
def books
Query.cacheable_object(CachedList.new("Book"), context)
::Book.all
end
# Or for a more complex implementation:
field :books_starting_with_t, [Book]
def books_starting_with_t
book_list = FilteredBookList.new
Query.cacheable_object(book_list, context)
book_list.books
end
end
query(Query)
use GraphQL::Enterprise::ObjectCache, memory: true
def self.private_context_fingerprint_for(ctx)
"" # not used in this example
end
def self.id_from_object(object, type, ctx)
"#{object.class.name}/#{object.id}"
end
def self.object_from_id(id, ctx)
obj_class, obj_id = id.split("/")
Object.const_get(obj_class).find(obj_id)
end
end
# Example 1, caching `.all`:
pp MySchema.execute("{ books { title } }").to_h
# {"data"=>{"books"=>[{"title"=>"That Distant Land"}, {"title"=>"Historical Brewing Techniques"}, {"title"=>"Ruby under a Microscope"}]}}
# Bust the cache:
Book.create!(title: "The Hobbit")
pp MySchema.execute("{ books { title } }").to_h
# {"data"=>{"books"=>[{"title"=>"That Distant Land"}, {"title"=>"Historical Brewing Techniques"}, {"title"=>"Ruby under a Microscope"}, {"title"=>"The Hobbit"}]}}
# Example 2, caching a custom query:
pp MySchema.execute("{ booksStartingWithT { title } }").to_h
# {"data"=>{"booksStartingWithT"=>[{"title"=>"That Distant Land"}, {"title"=>"The Hobbit"}]}}
# Doesn't bust the cache:
Book.create!(title: "Dog Training for Dummies")
pp MySchema.execute("{ booksStartingWithT { title } }").context[:object_cache][:hit]
# true
# Bust the cache:
Book.create!(title: "Treasure Island")
pp MySchema.execute("{ booksStartingWithT { title } }").to_h
# {"data"=>{"booksStartingWithT"=>[{"title"=>"That Distant Land"}, {"title"=>"The Hobbit"}, {"title"=>"Treasure Island"}]}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment