second_level_cache 2.5.3 → 2.6.4

Sign up to get free protection for your applications and to get access to all the features.
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