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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +4 -4
- 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 +14 -27
- data/lib/second_level_cache/active_record/finder_methods.rb +44 -42
- data/lib/second_level_cache/active_record/has_one_association.rb +15 -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 +1 -1
- data/test/fetch_by_uniq_key_test.rb +18 -14
- data/test/finder_methods_test.rb +39 -0
- data/test/has_one_association_test.rb +20 -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 +4 -1
- data/test/model/contribution.rb +1 -1
- 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 +11 -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 +2 -5
- metadata +33 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9401e1e025a3e955471ae743711371608400fe8c7f451f74f8b4f73bbb7c9dd6
|
4
|
+
data.tar.gz: 47c05c11d57d26dfe5c990bf111a173d0864956d788c5c5ace70ca812ef3d629
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 516d74db202dbd5d12f866b36a16b30e763a75fa88001c4ac5ab04ce1c73a2a2e412472ceda957401f9da303094d83c5a944b9c062d69e56116abdae76359256
|
7
|
+
data.tar.gz: b0f3440ffd9333d49ac957cc1db3c988f41276d57f18f874fa84fc664134a14ba0acfcc8b9f5001bab99243ef51c2492ca2ea8fe2768ac461b398a1b06504e65
|
data/CHANGELOG.md
CHANGED
@@ -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
|
[](https://travis-ci.org/hooopo/second_level_cache)
|
5
5
|
[](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.
|
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"
|
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
|
@@ -14,14 +14,14 @@ module SecondLevelCache
|
|
14
14
|
nil
|
15
15
|
end
|
16
16
|
end
|
17
|
-
return record if
|
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
|
-
|
21
|
+
record
|
22
22
|
else
|
23
23
|
SecondLevelCache.cache_store.delete(cache_key)
|
24
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
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,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
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|