Last active
December 10, 2024 21:57
-
-
Save rmosolgo/accca5dd4b4ae7b0b9713054bbf6c1af to your computer and use it in GitHub Desktop.
Caching top-level lists with GraphQL-Ruby when new items are created
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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