Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for preloading related data for missing collection entries #43

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.7.2
3.1.0
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,17 @@ end

do_caching true

preloads do |cereals, options|
ActiveRecord::Associations::Preloader.new.preload(cereals, :maker)
end

cache_key_addons { |options| options[:be_trendy] }
dependency_key 'V3'

serialize :uid, from: :id, to: String
serialize :name, :company
serialize :copyright, through: :legal_info
serialize :maker_name, through: :maker
serialize :spiel do |cereal, _options|
'Made with whole grains!' if cereal.ingredients[:whole_grains] > 0.000001
end
Expand Down Expand Up @@ -136,6 +141,7 @@ CerealSerializer.new(Cereal.first, be_trendy: true, include: :ingredients).as_js
"name": "Eyeholes",
"company": "Needful Things",
"copyright": "© Need Things 2019",
"maker_name": "Cereal Inc",
"spiel": "Made with whole grains!",
"tagline": "Part of a balanced breakfast",
"small_print": "This doesn't mean jack-squat",
Expand Down Expand Up @@ -257,6 +263,15 @@ def fine_print
end
```

### Preload data to avoid n+1 issues when rendering a collection of items
```ruby
preloads do |cereals, options|
ActiveRecord::Associations::Preloader.new.preload(cereals, :maker)
end
```

When a serializer is cached, the `cereals` will contain only those entries for which no cache was found.

### Include other data
```ruby
class CerealSerializer < CacheCrispies::Base
Expand Down
9 changes: 9 additions & 0 deletions lib/cache_crispies/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ def self.file_hashes
).uniq.sort
end

def self.preloads(collection = [], options = {}, &block)
if block_given?
@preloads = block
nil
else
@preloads&.call(collection, options)
end
end

private

def self.file_hash
Expand Down
16 changes: 12 additions & 4 deletions lib/cache_crispies/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def as_json
attr_reader :collection, :serializer, :options

def uncached_json
@serializer.preloads(collection, options)
collection.map do |model|
serializer.new(model, options).as_json
end
Expand All @@ -46,11 +47,18 @@ def cached_json
hash[plan.cache_key] = model
end

CacheCrispies.cache.fetch_multi(*models_by_cache_key.keys) do |cache_key|
model = models_by_cache_key[cache_key]
already_cached = CacheCrispies.cache.read_multi(*models_by_cache_key.keys)

serializer.new(model, options).as_json
end.values
missing_keys = models_by_cache_key.keys - already_cached.keys
missing_values = models_by_cache_key.fetch_values(*missing_keys)
@serializer.preloads(missing_values, options)

new_entries = missing_keys.each_with_object({}) do |key, hash|
hash[key] = serializer.new(models_by_cache_key[key], options).as_json
end

CacheCrispies.cache.write_multi(new_entries)
new_entries.values + already_cached.values
end
end
end
45 changes: 37 additions & 8 deletions spec/cache_crispies/collection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,26 +58,55 @@ def models.cache_key() end

it 'fetches the cache for each object in the collection' do
expect(CacheCrispies::Plan).to receive(:new).with(
serializer, model1, options
serializer, model1, **options
).and_return double('plan-dbl-1', cache_key: 'cereal-key-1')

expect(CacheCrispies::Plan).to receive(:new).with(
serializer, model2, options
serializer, model2, **options
).and_return double('plan-dbl-2', cache_key: 'cereal-key-2')

expect(CacheCrispies).to receive_message_chain(:cache, :read_multi).
with('cereal-key-1', 'cereal-key-2').and_return({})

expect(CacheCrispies).to receive_message_chain(
:cache, :fetch_multi
:cache, :write_multi
).with(
'cereal-key-1', 'cereal-key-2'
).and_yield('cereal-key-1').and_return(
name: name1
).and_yield('cereal-key-2').and_return(
name: name2
{ 'cereal-key-1' => { name: name1 }, 'cereal-key-2' => { name: name2 } }
)

subject.as_json
end
end

context 'when the collection cache key does not miss' do
before do
allow(CacheCrispies).to receive_message_chain(
:cache, :fetch
).with('cacheable-collection-key').and_yield
end

it 'fetches the cache for each object in the collection' do
expect(CacheCrispies::Plan).to receive(:new).with(
serializer, model1, **options
).and_return double('plan-dbl-1', cache_key: 'cereal-key-1')

expect(CacheCrispies::Plan).to receive(:new).with(
serializer, model2, **options
).and_return double('plan-dbl-2', cache_key: 'cereal-key-2')

expect(CacheCrispies).to receive_message_chain(:cache, :read_multi).
with('cereal-key-1', 'cereal-key-2').
and_return(
{ 'cereal-key-1' => { name: name1 }, 'cereal-key-2' => { name: name2 } }
)

expect(CacheCrispies).to receive_message_chain(
:cache, :write_multi
).with({})

subject.as_json
end
end
end
end
end