second_level_cache 2.5.3 → 2.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +4 -4
  4. data/lib/second_level_cache.rb +1 -0
  5. data/lib/second_level_cache/active_record.rb +8 -2
  6. data/lib/second_level_cache/active_record/base.rb +1 -5
  7. data/lib/second_level_cache/active_record/belongs_to_association.rb +6 -4
  8. data/lib/second_level_cache/active_record/fetch_by_uniq_key.rb +14 -27
  9. data/lib/second_level_cache/active_record/finder_methods.rb +44 -42
  10. data/lib/second_level_cache/active_record/has_one_association.rb +15 -20
  11. data/lib/second_level_cache/active_record/preloader.rb +34 -28
  12. data/lib/second_level_cache/adapter/paranoia.rb +24 -0
  13. data/lib/second_level_cache/config.rb +1 -0
  14. data/lib/second_level_cache/log_subscriber.rb +15 -0
  15. data/lib/second_level_cache/mixin.rb +0 -12
  16. data/lib/second_level_cache/record_marshal.rb +7 -40
  17. data/lib/second_level_cache/version.rb +1 -1
  18. data/second_level_cache.gemspec +23 -22
  19. data/test/active_record_test_case_helper.rb +1 -1
  20. data/test/fetch_by_uniq_key_test.rb +18 -14
  21. data/test/finder_methods_test.rb +39 -0
  22. data/test/has_one_association_test.rb +20 -4
  23. data/test/model/account.rb +2 -2
  24. data/test/model/animal.rb +1 -1
  25. data/test/model/application_record.rb +3 -0
  26. data/test/model/book.rb +4 -1
  27. data/test/model/contribution.rb +1 -1
  28. data/test/model/image.rb +1 -1
  29. data/test/model/order.rb +1 -1
  30. data/test/model/order_item.rb +1 -1
  31. data/test/model/paranoid.rb +10 -0
  32. data/test/model/post.rb +11 -1
  33. data/test/model/topic.rb +1 -1
  34. data/test/model/user.rb +7 -7
  35. data/test/paranoid_test.rb +18 -0
  36. data/test/{preloader_test.rb → preloader_belongs_to_test.rb} +17 -15
  37. data/test/preloader_has_many_test.rb +13 -0
  38. data/test/preloader_has_one_test.rb +69 -0
  39. data/test/record_marshal_test.rb +1 -1
  40. data/test/test_helper.rb +2 -5
  41. metadata +33 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57109a059ebed4c9d448fa28e474f19eef4ebf7b67e4ebbd728f2be852b61480
4
- data.tar.gz: '049303d75a982adf35dfea344af63577dd9df1c09a28613bb4dec4332ef95de4'
3
+ metadata.gz: 9401e1e025a3e955471ae743711371608400fe8c7f451f74f8b4f73bbb7c9dd6
4
+ data.tar.gz: 47c05c11d57d26dfe5c990bf111a173d0864956d788c5c5ace70ca812ef3d629
5
5
  SHA512:
6
- metadata.gz: 553a34ea48e7b3100ae819282c354ee0eb51e2d609b7c090bf477035b975a0e497158ca6f4b282add76c882fa7e94277fb39486aaf4e45c35c9e98088a2bbb34
7
- data.tar.gz: 28a7f7ea7f32467e92097824559fbaebaf3d9dd0fe49443d7779e666cce3270f3378c8e77dae91bffbc5248c8fadd10f00a08ebbbe390641fa9e42007b917ba5
6
+ metadata.gz: 516d74db202dbd5d12f866b36a16b30e763a75fa88001c4ac5ab04ce1c73a2a2e412472ceda957401f9da303094d83c5a944b9c062d69e56116abdae76359256
7
+ data.tar.gz: b0f3440ffd9333d49ac957cc1db3c988f41276d57f18f874fa84fc664134a14ba0acfcc8b9f5001bab99243ef51c2492ca2ea8fe2768ac461b398a1b06504e65
@@ -1,3 +1,32 @@
1
+ 2.6.4
2
+ -------
3
+
4
+ - Fix `undefined method klass` error for has_one through. (#123)
5
+
6
+ 2.6.3
7
+ -------
8
+
9
+ - Fix paranoia load error.
10
+
11
+ 2.6.2
12
+ -------
13
+
14
+ - Fix activerecord association cache. (#109)
15
+ - Fix fetch_by_uniq_key cache key with prefix. (#120)
16
+
17
+ 2.6.1
18
+ -------
19
+
20
+ - Improve proload debug log output, and deprecated logger method. (#106)
21
+
22
+ 2.6.0
23
+ -------
24
+
25
+ - Add has_one through cache support. (#98)
26
+ - Fix string query, eager_load, includes/preload for fetch from db. ( #103, #102, #101)
27
+ - Fix preloader if exists default scope. (#104)
28
+ - Change cache hit log as `DEBUG` level. (#105)
29
+
1
30
  2.5.3
2
31
  -------
3
32
 
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Build Status](https://travis-ci.org/hooopo/second_level_cache.svg?branch=master)](https://travis-ci.org/hooopo/second_level_cache)
5
5
  [![Code Climate](https://codeclimate.com/github/hooopo/second_level_cache.svg)](https://codeclimate.com/github/hooopo/second_level_cache)
6
6
 
7
- SecondLevelCache is a write-through and read-through caching library inspired by Cache Money and cache_fu, support ActiveRecord 4.
7
+ SecondLevelCache is a write-through and read-through caching library inspired by Cache Money and cache_fu, support ActiveRecord 4, ActiveRecord 5 and ActiveRecord 6.
8
8
 
9
9
  Read-Through: Queries by ID, like `current_user.articles.find(params[:id])`, will first look in cache store and then look in the database for the results of that query. If there is a cache miss, it will populate the cache.
10
10
 
@@ -15,10 +15,10 @@ Write-Through: As objects are created, updated, and deleted, all of the caches a
15
15
 
16
16
  In your gem file:
17
17
 
18
- ActiveRecord 5.2:
18
+ ActiveRecord 5.2 and 6.0:
19
19
 
20
20
  ```ruby
21
- gem 'second_level_cache', '~> 2.4.0'
21
+ gem 'second_level_cache', '~> 2.6.3'
22
22
  ```
23
23
 
24
24
  ActiveRecord 5.0.x, 5.1.x:
@@ -134,7 +134,7 @@ config.cache_store = [:dalli_store, APP_CONFIG["memcached_host"], { namespace: "
134
134
  ## Tips:
135
135
 
136
136
  * When you want to clear only second level cache apart from other cache for example fragment cache in cache store,
137
- you can only change the `cache_key_prefix`:
137
+ you can only change the `cache_key_prefix` (default: `slc`):
138
138
 
139
139
  ```ruby
140
140
  SecondLevelCache.configure.cache_key_prefix = "slc1"
@@ -5,6 +5,7 @@ require "second_level_cache/config"
5
5
  require "second_level_cache/record_marshal"
6
6
  require "second_level_cache/record_relation"
7
7
  require "second_level_cache/active_record"
8
+ require "second_level_cache/log_subscriber"
8
9
 
9
10
  module SecondLevelCache
10
11
  def self.configure
@@ -12,7 +12,13 @@ require "second_level_cache/active_record/preloader"
12
12
 
13
13
  # http://api.rubyonrails.org/classes/ActiveSupport/LazyLoadHooks.html
14
14
  # ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
15
- ActiveSupport.on_load(:active_record) do
15
+ ActiveSupport.on_load(:active_record, run_once: true) do
16
+ if Bundler.definition.dependencies.find { |x| x.name == "paranoia" }
17
+ require "second_level_cache/adapter/paranoia"
18
+ include SecondLevelCache::Adapter::Paranoia::ActiveRecord
19
+ SecondLevelCache::Mixin.send(:prepend, SecondLevelCache::Adapter::Paranoia::Mixin)
20
+ end
21
+
16
22
  include SecondLevelCache::Mixin
17
23
  prepend SecondLevelCache::ActiveRecord::Base
18
24
  extend SecondLevelCache::ActiveRecord::FetchByUniqKey
@@ -23,5 +29,5 @@ ActiveSupport.on_load(:active_record) do
23
29
  ActiveRecord::Relation.send(:prepend, SecondLevelCache::ActiveRecord::FinderMethods)
24
30
  # Rails 5.2 has removed ActiveRecord::Associations::Preloader::BelongsTo
25
31
  # https://github.com/rails/rails/pull/31079
26
- ActiveRecord::Associations::Preloader::Association.send(:prepend, SecondLevelCache::ActiveRecord::Associations::Preloader::BelongsTo)
32
+ ActiveRecord::Associations::Preloader::Association.send(:prepend, SecondLevelCache::ActiveRecord::Associations::Preloader)
27
33
  end
@@ -6,11 +6,7 @@ module SecondLevelCache
6
6
  def self.prepended(base)
7
7
  base.after_commit :update_second_level_cache, on: :update
8
8
  base.after_commit :write_second_level_cache, on: :create
9
- if defined?(::Paranoia)
10
- base.after_destroy :expire_second_level_cache
11
- else
12
- base.after_commit :expire_second_level_cache, on: :destroy
13
- end
9
+ base.after_commit :expire_second_level_cache, on: :destroy
14
10
 
15
11
  class << base
16
12
  prepend ClassMethods
@@ -6,6 +6,9 @@ module SecondLevelCache
6
6
  module BelongsToAssociation
7
7
  def find_target
8
8
  return super unless klass.second_level_cache_enabled?
9
+ return super if klass.default_scopes.present? || reflection.scope
10
+ return super if reflection.active_record_primary_key.to_s != klass.primary_key
11
+
9
12
  cache_record = klass.read_second_level_cache(second_level_cache_key)
10
13
  if cache_record
11
14
  return cache_record.tap { |record| set_inverse_instance(record) }
@@ -21,10 +24,9 @@ module SecondLevelCache
21
24
  end
22
25
 
23
26
  private
24
-
25
- def second_level_cache_key
26
- owner[reflection.foreign_key]
27
- end
27
+ def second_level_cache_key
28
+ owner[reflection.foreign_key]
29
+ end
28
30
  end
29
31
  end
30
32
  end
@@ -14,14 +14,14 @@ module SecondLevelCache
14
14
  nil
15
15
  end
16
16
  end
17
- return record if compare_record_attributes_with_where_values(record, where_values)
17
+ return record if record_attributes_equal_where_values?(record, where_values)
18
18
  record = where(where_values).first
19
19
  if record
20
20
  SecondLevelCache.cache_store.write(cache_key, record.id)
21
- return record
21
+ record
22
22
  else
23
23
  SecondLevelCache.cache_store.delete(cache_key)
24
- return nil
24
+ nil
25
25
  end
26
26
  end
27
27
 
@@ -42,33 +42,20 @@ module SecondLevelCache
42
42
  end
43
43
 
44
44
  private
45
-
46
- def cache_uniq_key(where_values)
47
- keys = where_values.collect do |k, v|
48
- v = Digest::MD5.hexdigest(v) if v.respond_to?(:size) && v.size >= 32
49
- [k, v].join("_")
45
+ def cache_uniq_key(where_values)
46
+ keys = where_values.collect do |k, v|
47
+ v = Digest::MD5.hexdigest(v) if v.respond_to?(:size) && v.size >= 32
48
+ [k, v].join("_")
49
+ end
50
+
51
+ ext_key = keys.join(",")
52
+ "#{SecondLevelCache.configure.cache_key_prefix}/uniq_key_#{name}_#{ext_key}"
50
53
  end
51
54
 
52
- ext_key = keys.join(",")
53
- "uniq_key_#{name}_#{ext_key}"
54
- end
55
-
56
- def compare_record_attributes_with_where_values(record, where_values)
57
- return false unless record
58
- where_values.all? do |k, v|
59
- attribute_value = record.read_attribute(k)
60
- attribute_value == case attribute_value
61
- when String
62
- v.to_s
63
- when Numeric
64
- v.to_f
65
- when Date
66
- v.to_date
67
- else # Maybe NilClass/?
68
- v
69
- end
55
+ def record_attributes_equal_where_values?(record, where_values)
56
+ # https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-type_for_attribute
57
+ where_values.all? { |k, v| record&.read_attribute(k) == type_for_attribute(k).cast(v) }
70
58
  end
71
- end
72
59
  end
73
60
  end
74
61
  end
@@ -17,20 +17,13 @@ module SecondLevelCache
17
17
  # Article.where("articles.user_id = 1").find(prams[:id])
18
18
  # Article.where("user_id = 1 AND ...").find(params[:id])
19
19
  def find_one(id)
20
- return super(id) unless second_level_cache_enabled?
21
- return super(id) unless select_all_column?
20
+ return super unless cachable?
22
21
 
23
22
  id = id.id if ActiveRecord::Base == id
24
- if cachable?
25
- record = @klass.read_second_level_cache(id)
26
- if record
27
- if where_values_hash.blank? || where_values_match_cache?(record)
28
- return record
29
- end
30
- end
31
- end
23
+ record = @klass.read_second_level_cache(id)
24
+ return record if record && where_values_match_cache?(record)
32
25
 
33
- record = super(id)
26
+ record = super
34
27
  record.write_second_level_cache
35
28
  record
36
29
  end
@@ -48,53 +41,62 @@ module SecondLevelCache
48
41
  # User.where(age: 18).first
49
42
  #
50
43
  def first(limit = nil)
51
- return super(limit) if limit.to_i > 1
44
+ return super if limit.to_i > 1
45
+ return super unless cachable?
52
46
  # only have primary_key condition in where
53
47
  if where_values_hash.length == 1 && where_values_hash.key?(primary_key)
54
48
  record = @klass.read_second_level_cache(where_values_hash[primary_key])
55
49
  return record if record
56
50
  end
57
51
 
58
- record = super(limit)
59
- record&.write_second_level_cache if select_all_column?
52
+ record = super
53
+ record&.write_second_level_cache
60
54
  record
61
55
  end
62
56
 
63
57
  private
58
+ # readonly_value - active_record/relation/query_methods.rb Rails 5.1 true/false
59
+ def cachable?
60
+ second_level_cache_enabled? &&
61
+ limit_one? &&
62
+ # !eager_loading? &&
63
+ includes_values.blank? &&
64
+ preload_values.blank? &&
65
+ eager_load_values.blank? &&
66
+ select_values.blank? &&
67
+ order_values_can_cache? &&
68
+ readonly_value.blank? &&
69
+ joins_values.blank? &&
70
+ !@klass.locking_enabled? &&
71
+ where_clause_predicates_all_equality?
72
+ end
64
73
 
65
- # readonly_value - active_record/relation/query_methods.rb Rails 5.1 true/false
66
- def cachable?
67
- limit_one? &&
68
- order_values.blank? &&
69
- includes_values.blank? &&
70
- preload_values.blank? &&
71
- readonly_value.blank? &&
72
- joins_values.blank? &&
73
- !@klass.locking_enabled? &&
74
- where_clause_match_equality?
75
- end
74
+ def order_values_can_cache?
75
+ return true if order_values.empty?
76
+ return false unless order_values.one?
77
+ return true if order_values.first == klass.primary_key
78
+ return false unless order_values.first.is_a?(::Arel::Nodes::Ordering)
79
+ return true if order_values.first.expr == klass.primary_key
80
+ order_values.first.expr.try(:name) == klass.primary_key
81
+ end
76
82
 
77
- def where_clause_match_equality?
78
- where_values_hash.all?
79
- end
83
+ def where_clause_predicates_all_equality?
84
+ where_clause.send(:predicates).size == where_values_hash.size
85
+ end
80
86
 
81
- def where_values_match_cache?(record)
82
- where_values_hash.all? do |key, value|
83
- if value.is_a?(Array)
84
- value.include?(record.read_attribute(key))
85
- else
86
- record.read_attribute(key) == value
87
+ def where_values_match_cache?(record)
88
+ where_values_hash.all? do |key, value|
89
+ if value.is_a?(Array)
90
+ value.include?(record.read_attribute(key))
91
+ else
92
+ record.read_attribute(key) == value
93
+ end
87
94
  end
88
95
  end
89
- end
90
96
 
91
- def limit_one?
92
- limit_value.blank? || limit_value == 1
93
- end
94
-
95
- def select_all_column?
96
- select_values.blank?
97
- end
97
+ def limit_one?
98
+ limit_value.blank? || limit_value == 1
99
+ end
98
100
  end
99
101
  end
100
102
  end
@@ -6,31 +6,26 @@ module SecondLevelCache
6
6
  module HasOneAssociation
7
7
  def find_target
8
8
  return super unless klass.second_level_cache_enabled?
9
- return super if reflection.options[:through] || reflection.scope
10
- # TODO: implement cache with has_one through, scope
9
+ return super if klass.default_scopes.present? || reflection.scope
10
+ # TODO: implement cache with has_one scope
11
11
 
12
- owner_primary_key = owner[reflection.active_record_primary_key]
13
- if reflection.options[:as]
14
- keys = {
15
- reflection.foreign_key => owner_primary_key,
16
- reflection.type => owner.class.base_class.name
17
- }
18
- cache_record = klass.fetch_by_uniq_keys(keys)
12
+ through = reflection.options[:through]
13
+ record = if through
14
+ return super if klass.reflections[through.to_s].nil?
15
+ return super unless klass.reflections[through.to_s].klass.second_level_cache_enabled?
16
+ begin
17
+ reflection.klass.find(owner.send(through).read_attribute(reflection.foreign_key))
18
+ rescue StandardError
19
+ nil
20
+ end
19
21
  else
20
- cache_record = klass.fetch_by_uniq_key(owner_primary_key, reflection.foreign_key)
22
+ uniq_keys = { reflection.foreign_key => owner[reflection.active_record_primary_key] }
23
+ uniq_keys[reflection.type] = owner.class.base_class.name if reflection.options[:as]
24
+ klass.fetch_by_uniq_keys(uniq_keys)
21
25
  end
22
26
 
23
- if cache_record
24
- return cache_record.tap { |record| set_inverse_instance(record) }
25
- end
26
-
27
- record = super
28
27
  return nil unless record
29
-
30
- record.tap do |r|
31
- set_inverse_instance(r)
32
- r.write_second_level_cache
33
- end
28
+ record.tap { |r| set_inverse_instance(r) }
34
29
  end
35
30
  end
36
31
  end
@@ -3,42 +3,48 @@
3
3
  module SecondLevelCache
4
4
  module ActiveRecord
5
5
  module Associations
6
- class Preloader
7
- module BelongsTo
8
- def records_for(ids, &block)
9
- return super(ids, &block) unless reflection.is_a?(::ActiveRecord::Reflection::BelongsToReflection)
10
- return super(ids, &block) unless klass.second_level_cache_enabled?
11
-
12
- map_cache_keys = ids.map { |id| klass.second_level_cache_key(id) }
13
- records_from_cache = ::SecondLevelCache.cache_store.read_multi(*map_cache_keys)
14
- # NOTICE
15
- # Rails.cache.read_multi return hash that has keys only hitted.
16
- # eg. Rails.cache.read_multi(1,2,3) => {2 => hit_value, 3 => hit_value}
17
- hitted_ids = records_from_cache.map { |key, _| key.split("/")[2] }
18
- missed_ids = ids.map(&:to_s) - hitted_ids
19
-
20
- ::SecondLevelCache.logger.info "missed ids -> #{missed_ids.join(',')} | hitted ids -> #{hitted_ids.join(',')}"
21
-
22
- record_marshals = RecordMarshal.load_multi(records_from_cache.values)
23
-
24
- if missed_ids.empty?
25
- return SecondLevelCache::RecordRelation.new(record_marshals)
6
+ module Preloader
7
+ RAILS6 = ::ActiveRecord.version >= ::Gem::Version.new("6")
8
+
9
+ def records_for(ids, &block)
10
+ return super unless klass.second_level_cache_enabled?
11
+ return super unless reflection.is_a?(::ActiveRecord::Reflection::BelongsToReflection)
12
+ return super if klass.default_scopes.present? || reflection.scope
13
+ return super if association_key_name.to_s != klass.primary_key
14
+
15
+ map_cache_keys = ids.map { |id| klass.second_level_cache_key(id) }
16
+ records_from_cache = ::SecondLevelCache.cache_store.read_multi(*map_cache_keys)
17
+
18
+ record_marshals = if RAILS6
19
+ RecordMarshal.load_multi(records_from_cache.values) do |record|
20
+ # This block is copy from:
21
+ # https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/associations/preloader/association.rb#L101
22
+ owner = owners_by_key[convert_key(record[association_key_name])].first
23
+ association = owner.association(reflection.name)
24
+ association.set_inverse_instance(record)
26
25
  end
26
+ else
27
+ RecordMarshal.load_multi(records_from_cache.values, &block)
28
+ end
27
29
 
28
- records_from_db = super(missed_ids, &block)
29
- records_from_db.map do |r|
30
- write_cache(r)
31
- end
30
+ # NOTICE
31
+ # Rails.cache.read_multi return hash that has keys only hitted.
32
+ # eg. Rails.cache.read_multi(1,2,3) => {2 => hit_value, 3 => hit_value}
33
+ hitted_ids = record_marshals.map { |record| record.read_attribute(association_key_name).to_s }
34
+ missed_ids = ids.map(&:to_s) - hitted_ids
35
+ ActiveSupport::Notifications.instrument("preload.second_level_cache", key: association_key_name, hit: hitted_ids, miss: missed_ids)
36
+ return SecondLevelCache::RecordRelation.new(record_marshals) if missed_ids.empty?
32
37
 
33
- SecondLevelCache::RecordRelation.new(records_from_db + record_marshals)
34
- end
38
+ records_from_db = super(missed_ids, &block)
39
+ records_from_db.map { |r| write_cache(r) }
35
40
 
36
- private
41
+ SecondLevelCache::RecordRelation.new(records_from_db + record_marshals)
42
+ end
37
43
 
44
+ private
38
45
  def write_cache(record)
39
46
  record.write_second_level_cache
40
47
  end
41
- end
42
48
  end
43
49
  end
44
50
  end