second_level_cache 2.5.2 → 2.6.3

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 +24 -0
  3. data/README.md +1 -1
  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 +24 -17
  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 +14 -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 +11 -1
  20. data/test/fetch_by_uniq_key_test.rb +47 -4
  21. data/test/finder_methods_test.rb +39 -0
  22. data/test/has_one_association_test.rb +16 -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 +6 -1
  27. data/test/model/contribution.rb +14 -0
  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 +1 -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 +3 -5
  41. metadata +31 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb263a78785d298fbaccbcb46bb71250f021e66d69f0cf5c89f9d1cd97651c50
4
- data.tar.gz: f861c5fc32002720f2e83b30444ad3f2ac368427406a1c832ca4ba0f5620da89
3
+ metadata.gz: 1f5d055fea10f55860339d904a65bb84a51893f742d20758311e0b2c2b690727
4
+ data.tar.gz: eeba8b526efb808158e440e10bf752dbf3bc51e5f4d88878f859cf8f3b790647
5
5
  SHA512:
6
- metadata.gz: 9cacc9218aed1c33fd4470c8fa6a5bf21a7e89c22da09f522594e6ab5fb4830a168a7c930c0e3b4470086ac0193e1a906ca27f6543d0cba44fe1b2e9c6cc02f5
7
- data.tar.gz: df2b8f074ce143b141d8fb34d4f5799da505c1e72fdda03dbeaa7284cfd10d9074ca87b93481ba2cafe9df7d578ef40fb11e4afa2a172d351cfcdabc77e7efc4
6
+ metadata.gz: 63ad5288284a4a06735175fb6171962ecde65ad08ffe9cbc112ce060e98c236150acc0b29947536fe06891144929de2ba8e744099a1c31865dccab9c2f9492c9
7
+ data.tar.gz: 559d3cde7763d4bd2645a9f416f66bdb54e74e5335004529c88f56bdfc6a3c934a6f7d5ee86aa5377405c12ad09d813ba2cf4313d43704daacd4480e9d25c782
@@ -1,3 +1,27 @@
1
+ 2.6.2
2
+ -------
3
+
4
+ - Fix activerecord association cache. (#109)
5
+ - Fix fetch_by_uniq_key cache key with prefix. (#120)
6
+
7
+ 2.6.1
8
+ -------
9
+
10
+ - Improve proload debug log output, and deprecated logger method. (#106)
11
+
12
+ 2.6.0
13
+ -------
14
+
15
+ - Add has_one through cache support. (#98)
16
+ - Fix string query, eager_load, includes/preload for fetch from db. ( #103, #102, #101)
17
+ - Fix preloader if exists default scope. (#104)
18
+ - Change cache hit log as `DEBUG` level. (#105)
19
+
20
+ 2.5.3
21
+ -------
22
+
23
+ - Fix `fetch_by_uniq_keys` method that cache incorrect when A record modified uniq key and B reocrd used old uniq key of A record (#96)
24
+
1
25
  2.5.2
2
26
  -------
3
27
 
data/README.md CHANGED
@@ -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
@@ -6,19 +6,22 @@ module SecondLevelCache
6
6
  def fetch_by_uniq_keys(where_values)
7
7
  cache_key = cache_uniq_key(where_values)
8
8
  obj_id = SecondLevelCache.cache_store.read(cache_key)
9
+
9
10
  if obj_id
10
- begin
11
- return find(obj_id)
12
- rescue StandardError
13
- return nil
14
- end
11
+ record = begin
12
+ find(obj_id)
13
+ rescue StandardError
14
+ nil
15
+ end
15
16
  end
16
-
17
+ return record if record_attributes_equal_where_values?(record, where_values)
17
18
  record = where(where_values).first
18
- return nil unless record
19
-
20
- record.tap do |r|
21
- SecondLevelCache.cache_store.write(cache_key, r.id)
19
+ if record
20
+ SecondLevelCache.cache_store.write(cache_key, record.id)
21
+ record
22
+ else
23
+ SecondLevelCache.cache_store.delete(cache_key)
24
+ nil
22
25
  end
23
26
  end
24
27
 
@@ -39,16 +42,20 @@ module SecondLevelCache
39
42
  end
40
43
 
41
44
  private
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
42
50
 
43
- def cache_uniq_key(where_values)
44
- keys = where_values.collect do |k, v|
45
- v = Digest::MD5.hexdigest(v) if v && v.size >= 32
46
- [k, v].join("_")
51
+ ext_key = keys.join(",")
52
+ "#{SecondLevelCache.configure.cache_key_prefix}/uniq_key_#{name}_#{ext_key}"
47
53
  end
48
54
 
49
- ext_key = keys.join(",")
50
- "uniq_key_#{name}_#{ext_key}"
51
- 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) }
58
+ end
52
59
  end
53
60
  end
54
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,25 @@ 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 unless klass.reflections[through.to_s].klass.second_level_cache_enabled?
15
+ begin
16
+ reflection.klass.find(owner.send(through).read_attribute(reflection.foreign_key))
17
+ rescue StandardError
18
+ nil
19
+ end
19
20
  else
20
- cache_record = klass.fetch_by_uniq_key(owner_primary_key, reflection.foreign_key)
21
+ uniq_keys = { reflection.foreign_key => owner[reflection.active_record_primary_key] }
22
+ uniq_keys[reflection.type] = owner.class.base_class.name if reflection.options[:as]
23
+ klass.fetch_by_uniq_keys(uniq_keys)
21
24
  end
22
25
 
23
- if cache_record
24
- return cache_record.tap { |record| set_inverse_instance(record) }
25
- end
26
-
27
- record = super
28
26
  return nil unless record
29
-
30
- record.tap do |r|
31
- set_inverse_instance(r)
32
- r.write_second_level_cache
33
- end
27
+ record.tap { |r| set_inverse_instance(r) }
34
28
  end
35
29
  end
36
30
  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
@@ -0,0 +1,24 @@
1
+ module SecondLevelCache
2
+ module Adapter
3
+ module Paranoia
4
+ module ActiveRecord
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ after_destroy :expire_second_level_cache
9
+ end
10
+ end
11
+
12
+ module Mixin
13
+ extend ActiveSupport::Concern
14
+
15
+ def write_second_level_cache
16
+ # Avoid rewrite cache again, when record has been soft deleted
17
+ return if respond_to?(:deleted?) && send(:deleted?)
18
+ super
19
+ end
20
+ alias update_second_level_cache write_second_level_cache
21
+ end
22
+ end
23
+ end
24
+ end