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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +1 -1
- data/lib/second_level_cache.rb +1 -0
- data/lib/second_level_cache/active_record.rb +8 -2
- data/lib/second_level_cache/active_record/base.rb +1 -5
- data/lib/second_level_cache/active_record/belongs_to_association.rb +6 -4
- data/lib/second_level_cache/active_record/fetch_by_uniq_key.rb +24 -17
- data/lib/second_level_cache/active_record/finder_methods.rb +44 -42
- data/lib/second_level_cache/active_record/has_one_association.rb +14 -20
- data/lib/second_level_cache/active_record/preloader.rb +34 -28
- data/lib/second_level_cache/adapter/paranoia.rb +24 -0
- data/lib/second_level_cache/config.rb +1 -0
- data/lib/second_level_cache/log_subscriber.rb +15 -0
- data/lib/second_level_cache/mixin.rb +0 -12
- data/lib/second_level_cache/record_marshal.rb +7 -40
- data/lib/second_level_cache/version.rb +1 -1
- data/second_level_cache.gemspec +23 -22
- data/test/active_record_test_case_helper.rb +11 -1
- data/test/fetch_by_uniq_key_test.rb +47 -4
- data/test/finder_methods_test.rb +39 -0
- data/test/has_one_association_test.rb +16 -4
- data/test/model/account.rb +2 -2
- data/test/model/animal.rb +1 -1
- data/test/model/application_record.rb +3 -0
- data/test/model/book.rb +6 -1
- data/test/model/contribution.rb +14 -0
- data/test/model/image.rb +1 -1
- data/test/model/order.rb +1 -1
- data/test/model/order_item.rb +1 -1
- data/test/model/paranoid.rb +10 -0
- data/test/model/post.rb +1 -1
- data/test/model/topic.rb +1 -1
- data/test/model/user.rb +7 -7
- data/test/paranoid_test.rb +18 -0
- data/test/{preloader_test.rb → preloader_belongs_to_test.rb} +17 -15
- data/test/preloader_has_many_test.rb +13 -0
- data/test/preloader_has_one_test.rb +69 -0
- data/test/record_marshal_test.rb +1 -1
- data/test/test_helper.rb +3 -5
- metadata +31 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f5d055fea10f55860339d904a65bb84a51893f742d20758311e0b2c2b690727
|
4
|
+
data.tar.gz: eeba8b526efb808158e440e10bf752dbf3bc51e5f4d88878f859cf8f3b790647
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63ad5288284a4a06735175fb6171962ecde65ad08ffe9cbc112ce060e98c236150acc0b29947536fe06891144929de2ba8e744099a1c31865dccab9c2f9492c9
|
7
|
+
data.tar.gz: 559d3cde7763d4bd2645a9f416f66bdb54e74e5335004529c88f56bdfc6a3c934a6f7d5ee86aa5377405c12ad09d813ba2cf4313d43704daacd4480e9d25c782
|
data/CHANGELOG.md
CHANGED
@@ -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"
|
data/lib/second_level_cache.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
26
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
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
|
-
|
25
|
-
|
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
|
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
|
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
|
59
|
-
record&.write_second_level_cache
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
order_values.
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
83
|
+
def where_clause_predicates_all_equality?
|
84
|
+
where_clause.send(:predicates).size == where_values_hash.size
|
85
|
+
end
|
80
86
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
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
|
10
|
-
# TODO: implement cache with has_one
|
9
|
+
return super if klass.default_scopes.present? || reflection.scope
|
10
|
+
# TODO: implement cache with has_one scope
|
11
11
|
|
12
|
-
|
13
|
-
if
|
14
|
-
|
15
|
-
|
16
|
-
reflection.
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
34
|
-
|
38
|
+
records_from_db = super(missed_ids, &block)
|
39
|
+
records_from_db.map { |r| write_cache(r) }
|
35
40
|
|
36
|
-
|
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
|