second_level_cache 2.5.0 → 2.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -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/persistence.rb +6 -2
  12. data/lib/second_level_cache/active_record/preloader.rb +34 -28
  13. data/lib/second_level_cache/adapter/paranoia.rb +24 -0
  14. data/lib/second_level_cache/config.rb +1 -0
  15. data/lib/second_level_cache/log_subscriber.rb +15 -0
  16. data/lib/second_level_cache/mixin.rb +0 -12
  17. data/lib/second_level_cache/record_marshal.rb +7 -40
  18. data/lib/second_level_cache/version.rb +1 -1
  19. data/second_level_cache.gemspec +23 -23
  20. data/test/active_record_test_case_helper.rb +11 -1
  21. data/test/fetch_by_uniq_key_test.rb +47 -4
  22. data/test/finder_methods_test.rb +39 -0
  23. data/test/has_one_association_test.rb +16 -4
  24. data/test/model/account.rb +2 -2
  25. data/test/model/animal.rb +1 -1
  26. data/test/model/application_record.rb +3 -0
  27. data/test/model/book.rb +6 -1
  28. data/test/model/contribution.rb +14 -0
  29. data/test/model/image.rb +1 -1
  30. data/test/model/order.rb +1 -1
  31. data/test/model/order_item.rb +1 -1
  32. data/test/model/paranoid.rb +10 -0
  33. data/test/model/post.rb +1 -1
  34. data/test/model/topic.rb +1 -1
  35. data/test/model/user.rb +7 -7
  36. data/test/paranoid_test.rb +16 -0
  37. data/test/{preloader_test.rb → preloader_belongs_to_test.rb} +17 -15
  38. data/test/preloader_has_many_test.rb +13 -0
  39. data/test/preloader_has_one_test.rb +69 -0
  40. data/test/record_marshal_test.rb +1 -1
  41. data/test/test_helper.rb +3 -4
  42. metadata +28 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3d60f7f52e3d34b0d49447bab88db697f929bf270435765ac8fed80d8d2da1c
4
- data.tar.gz: 8f4cde5b59a3b37112d0471f2e0a5ccf1218745b7e63b07d125401f15203f693
3
+ metadata.gz: 84fecc39005beffa957b1602ff86ace6ca5419a1215d35011034ac6c78b5e0bf
4
+ data.tar.gz: f0a09a62d06ef9f10a1ee4559a4aac1b8e39eaa8a86ee81ce90d67212c9c1811
5
5
  SHA512:
6
- metadata.gz: cddaaae90ae703a7b9ec9febdc3471b9839417a63d75c55b88d622e6d016358467ff8cc19e0a292adacc3c4391c3b7d679e56c544369bfac877d6a2f85a12dfb
7
- data.tar.gz: c5968feec18ad66efa1cee8f379330695276703b65d8a36f0f4943957cdcb107cb855fa2ae660a4e91d90f64abd39556e510b108cad728085fd191f08e502bc7
6
+ metadata.gz: 40c406b9b4181c5e1085127bbab26b71c5a47842d685521b9fa1ec43e58e529d8591c24ae675760ed2a3dcc9644dca7fcb6db12a5e651b67670af413c16addcc
7
+ data.tar.gz: fc5f6ee2184884f4580123550e3d3edbb113114de4f7dacad340ef8c37711e9388335fdf029a06971fefe6a666ae0ca615f2852101384f986750333c3f85b0de
@@ -1,3 +1,37 @@
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
+
25
+ 2.5.2
26
+ -------
27
+
28
+ - Fix methods argument as keyword warning in Ruby 2.7. (#94)
29
+
30
+ 2.5.1
31
+ -------
32
+
33
+ - Fix cache expire issue for SoftDelete cases.
34
+
1
35
  2.5.0
2
36
  -------
3
37
 
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.gem("paranoia") rescue false)
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
@@ -4,17 +4,21 @@ module SecondLevelCache
4
4
  module ActiveRecord
5
5
  module Persistence
6
6
  # update_column will call update_columns
7
+ # https://github.com/rails/rails/blob/5-0-stable/activerecord/lib/active_record/persistence.rb#L315
7
8
  def update_columns(attributes)
8
9
  super(attributes).tap { update_second_level_cache }
9
10
  end
10
11
 
12
+ # https://github.com/rails/rails/blob/5-0-stable/activerecord/lib/active_record/persistence.rb#L441
11
13
  def reload(options = nil)
12
14
  expire_second_level_cache
13
15
  super(options)
14
16
  end
15
17
 
16
- def touch(*names)
17
- super(*names).tap { update_second_level_cache }
18
+ # https://github.com/rails/rails/blob/5-0-stable/activerecord/lib/active_record/persistence.rb#L490
19
+ def touch(*names, **opts)
20
+ # super: touch(*names, time: nil)
21
+ super(*names, **opts).tap { update_second_level_cache }
18
22
  end
19
23
  end
20
24
  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