second_level_cache 2.6.1 → 2.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile +1 -2
- data/LICENSE +21 -0
- data/README.md +22 -17
- data/lib/second_level_cache/active_record/base.rb +1 -5
- data/lib/second_level_cache/active_record/belongs_to_association.rb +3 -0
- data/lib/second_level_cache/active_record/fetch_by_uniq_key.rb +16 -15
- data/lib/second_level_cache/active_record/has_one_association.rb +2 -2
- data/lib/second_level_cache/active_record/preloader/association.rb +61 -0
- data/lib/second_level_cache/active_record/preloader/legacy.rb +57 -0
- data/lib/second_level_cache/active_record.rb +20 -5
- data/lib/second_level_cache/adapter/paranoia.rb +25 -0
- data/lib/second_level_cache/mixin.rb +2 -3
- data/lib/second_level_cache/version.rb +1 -1
- data/second_level_cache.gemspec +13 -12
- data/test/belongs_to_association_test.rb +1 -1
- data/test/fetch_by_uniq_key_test.rb +6 -5
- data/test/has_one_association_test.rb +4 -0
- data/test/model/paranoid.rb +10 -0
- data/test/model/post.rb +10 -0
- data/test/model/user.rb +0 -2
- data/test/paranoid_test.rb +18 -0
- data/test/test_helper.rb +2 -2
- metadata +39 -44
- data/lib/second_level_cache/active_record/preloader.rb +0 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9161291526eed87047708e5128c64e2907ddf61f017dbf073831c1281b6b504d
|
4
|
+
data.tar.gz: 192fffc74fe056dd8bc2d8e6486f4f5de380698bfc728d65bf02853c110a9d66
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 84e57628cd9eb58d52d7a4d5c23844d8e470ee599d865845287ac9fee9b0659fe7a0b563ab5f2e6d7fd3c59ffce6de543ec0ae91a36e77fd8d2c628f05cd53a1
|
7
|
+
data.tar.gz: 60aa90c2dfa0be0920ee121772a426c3ce209423a4c5a2c28bf61efad3ad5da14738317f40df8704e6628db9fc7a2e439619ca2bdb1d7cb2d22073d1ccded656
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
|
2
|
+
New version releases please visit https://github.com/hooopo/second_level_cache/releases
|
3
|
+
|
4
|
+
2.6.4
|
5
|
+
-------
|
6
|
+
|
7
|
+
- Fix `undefined method klass` error for has_one through. (#123)
|
8
|
+
|
9
|
+
2.6.3
|
10
|
+
-------
|
11
|
+
|
12
|
+
- Fix paranoia load error.
|
13
|
+
|
14
|
+
2.6.2
|
15
|
+
-------
|
16
|
+
|
17
|
+
- Fix activerecord association cache. (#109)
|
18
|
+
- Fix fetch_by_uniq_key cache key with prefix. (#120)
|
19
|
+
|
1
20
|
2.6.1
|
2
21
|
-------
|
3
22
|
|
data/Gemfile
CHANGED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Hackershare
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,24 +1,29 @@
|
|
1
1
|
# SecondLevelCache
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/second_level_cache.svg)](http://badge.fury.io/rb/second_level_cache)
|
4
|
-
[![
|
4
|
+
[![build](https://github.com/hooopo/second_level_cache/actions/workflows/build.yml/badge.svg)](https://github.com/hooopo/second_level_cache/actions/workflows/build.yml)
|
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
|
|
11
11
|
Write-Through: As objects are created, updated, and deleted, all of the caches are automatically kept up-to-date and coherent.
|
12
12
|
|
13
|
-
|
14
13
|
## Install
|
15
14
|
|
16
15
|
In your gem file:
|
17
16
|
|
18
|
-
ActiveRecord
|
17
|
+
ActiveRecord 7
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'second_level_cache', '~> 2.7'
|
21
|
+
```
|
22
|
+
|
23
|
+
ActiveRecord 5.2 and 6.0:
|
19
24
|
|
20
25
|
```ruby
|
21
|
-
gem 'second_level_cache', '~> 2.
|
26
|
+
gem 'second_level_cache', '~> 2.6.3'
|
22
27
|
```
|
23
28
|
|
24
29
|
ActiveRecord 5.0.x, 5.1.x:
|
@@ -97,9 +102,9 @@ User.select("id, name").find(1)
|
|
97
102
|
|
98
103
|
## Notice
|
99
104
|
|
100
|
-
|
101
|
-
|
102
|
-
|
105
|
+
- SecondLevelCache cache by model name and id, so only find_one query will work.
|
106
|
+
- Only equal conditions query WILL get cache; and SQL string query like `User.where("name = 'Hooopo'").find(1)` WILL NOT work.
|
107
|
+
- SecondLevelCache sync cache after transaction commit:
|
103
108
|
|
104
109
|
```ruby
|
105
110
|
# user and account's write_second_level_cache operation will invoke after the logger.
|
@@ -117,7 +122,7 @@ end # <- Cache write
|
|
117
122
|
Rails.logger.info "info"
|
118
123
|
```
|
119
124
|
|
120
|
-
|
125
|
+
- If you are using SecondLevelCache with database_cleaner, you should set cleaning strategy to `:truncation`:
|
121
126
|
|
122
127
|
```ruby
|
123
128
|
DatabaseCleaner.strategy = :truncation
|
@@ -133,15 +138,15 @@ config.cache_store = [:dalli_store, APP_CONFIG["memcached_host"], { namespace: "
|
|
133
138
|
|
134
139
|
## Tips:
|
135
140
|
|
136
|
-
|
137
|
-
you can only change the `cache_key_prefix
|
141
|
+
- When you want to clear only second level cache apart from other cache for example fragment cache in cache store,
|
142
|
+
you can only change the `cache_key_prefix` (default: `slc`):
|
138
143
|
|
139
144
|
```ruby
|
140
145
|
SecondLevelCache.configure.cache_key_prefix = "slc1"
|
141
146
|
```
|
142
147
|
|
143
|
-
|
144
|
-
|
148
|
+
- SecondLevelCache was added model schema digest as cache version, this means when you add/remove/change columns, the caches of this Model will expires.
|
149
|
+
- When your want change the model cache version by manualy, just add the `version` option like this:
|
145
150
|
|
146
151
|
```ruby
|
147
152
|
class User < ActiveRecord::Base
|
@@ -149,7 +154,7 @@ class User < ActiveRecord::Base
|
|
149
154
|
end
|
150
155
|
```
|
151
156
|
|
152
|
-
|
157
|
+
- It provides a great feature, not hits db when fetching record via unique key (not primary key).
|
153
158
|
|
154
159
|
```ruby
|
155
160
|
# this will fetch from cache
|
@@ -160,7 +165,7 @@ post = Post.fetch_by_uniq_keys(user_id: 2, slug: "foo")
|
|
160
165
|
user = User.fetch_by_uniq_keys!(nick_name: "hooopo") # this will raise `ActiveRecord::RecordNotFound` Exception when nick name not exists.
|
161
166
|
```
|
162
167
|
|
163
|
-
|
168
|
+
- You can use Rails's [Eager Loading](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) feature as normal. Even better, second_level_cache will transform the `IN` query into a Rails.cache.multi_read operation. For example:
|
164
169
|
|
165
170
|
```ruby
|
166
171
|
Answer.includes(:question).limit(10).order("id DESC").each{|answer| answer.question.title}
|
@@ -171,8 +176,8 @@ Answer Load (0.2ms) SELECT `answers`.* FROM `answers` ORDER BY id DESC LIMIT 10
|
|
171
176
|
|
172
177
|
## Original design by:
|
173
178
|
|
174
|
-
|
175
|
-
|
179
|
+
- [chloerei](https://github.com/chloerei)
|
180
|
+
- [hooopo](https://github.com/hooopo)
|
176
181
|
|
177
182
|
## Contributors
|
178
183
|
|
@@ -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) }
|
@@ -9,10 +9,10 @@ module SecondLevelCache
|
|
9
9
|
|
10
10
|
if obj_id
|
11
11
|
record = begin
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
find(obj_id)
|
13
|
+
rescue
|
14
|
+
nil
|
15
|
+
end
|
16
16
|
end
|
17
17
|
return record if record_attributes_equal_where_values?(record, where_values)
|
18
18
|
record = where(where_values).first
|
@@ -42,20 +42,21 @@ module SecondLevelCache
|
|
42
42
|
end
|
43
43
|
|
44
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
|
50
45
|
|
51
|
-
|
52
|
-
|
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("_")
|
53
50
|
end
|
54
51
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
52
|
+
ext_key = keys.join(",")
|
53
|
+
"#{SecondLevelCache.configure.cache_key_prefix}/uniq_key_#{name}_#{ext_key}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def record_attributes_equal_where_values?(record, where_values)
|
57
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema/ClassMethods.html#method-i-type_for_attribute
|
58
|
+
where_values.all? { |k, v| record&.read_attribute(k) == type_for_attribute(k).cast(v) }
|
59
|
+
end
|
59
60
|
end
|
60
61
|
end
|
61
62
|
end
|
@@ -6,12 +6,12 @@ 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.scope
|
9
|
+
return super if klass.default_scopes.present? || reflection.scope
|
10
10
|
# TODO: implement cache with has_one scope
|
11
11
|
|
12
12
|
through = reflection.options[:through]
|
13
13
|
record = if through
|
14
|
-
return super unless
|
14
|
+
return super unless owner.class.reflections[through.to_s].klass.second_level_cache_enabled?
|
15
15
|
begin
|
16
16
|
reflection.klass.find(owner.send(through).read_attribute(reflection.foreign_key))
|
17
17
|
rescue StandardError
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecondLevelCache
|
4
|
+
module ActiveRecord
|
5
|
+
module Associations
|
6
|
+
module Preloader
|
7
|
+
module Association
|
8
|
+
# Override load_query method for add Association instance in arguments to LoaderQuery
|
9
|
+
# https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/associations/preloader/association.rb#L148
|
10
|
+
def loader_query
|
11
|
+
::ActiveRecord::Associations::Preloader::Association::LoaderQuery.new(self, scope, association_key_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Override load_records_for_keys for use SecondLevelCache before preload association
|
15
|
+
# https://github.com/rails/rails/blob/8f5b35b6107c28125b571b9842e248b13f804e5c/activerecord/lib/active_record/associations/preloader/association.rb#L7
|
16
|
+
module LoaderQuery
|
17
|
+
attr_reader :association
|
18
|
+
|
19
|
+
delegate :klass, to: :association
|
20
|
+
|
21
|
+
def initialize(association, scope, association_key_name)
|
22
|
+
@association = association
|
23
|
+
@scope = scope
|
24
|
+
@association_key_name = association_key_name
|
25
|
+
end
|
26
|
+
|
27
|
+
def reflection
|
28
|
+
association.send(:reflection)
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_records_for_keys(keys, &block)
|
32
|
+
ids = keys.to_a
|
33
|
+
|
34
|
+
return super unless klass.second_level_cache_enabled?
|
35
|
+
return super unless reflection.is_a?(::ActiveRecord::Reflection::BelongsToReflection)
|
36
|
+
return super if klass.default_scopes.present? || reflection.scope
|
37
|
+
return super if association_key_name.to_s != klass.primary_key
|
38
|
+
|
39
|
+
map_cache_keys = ids.map { |id| klass.second_level_cache_key(id) }
|
40
|
+
records_from_cache = ::SecondLevelCache.cache_store.read_multi(*map_cache_keys)
|
41
|
+
record_marshals = RecordMarshal.load_multi(records_from_cache.values, &block)
|
42
|
+
|
43
|
+
# NOTICE
|
44
|
+
# Rails.cache.read_multi return hash that has keys only hitted.
|
45
|
+
# eg. Rails.cache.read_multi(1,2,3) => {2 => hit_value, 3 => hit_value}
|
46
|
+
hitted_ids = record_marshals.map { |record| record.read_attribute(association_key_name).to_s }
|
47
|
+
missed_ids = ids.map(&:to_s) - hitted_ids
|
48
|
+
ActiveSupport::Notifications.instrument("preload.second_level_cache", key: association_key_name, hit: hitted_ids, miss: missed_ids)
|
49
|
+
return SecondLevelCache::RecordRelation.new(record_marshals) if missed_ids.empty?
|
50
|
+
|
51
|
+
records_from_db = super(missed_ids.to_set, &block)
|
52
|
+
records_from_db.map { |r| r.write_second_level_cache }
|
53
|
+
|
54
|
+
SecondLevelCache::RecordRelation.new(records_from_db + record_marshals)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecondLevelCache
|
4
|
+
module ActiveRecord
|
5
|
+
module Associations
|
6
|
+
module Preloader
|
7
|
+
module Association
|
8
|
+
# For < Rails 7
|
9
|
+
module Legacy
|
10
|
+
RAILS6 = ::ActiveRecord.version >= ::Gem::Version.new("6")
|
11
|
+
|
12
|
+
def records_for(ids, &block)
|
13
|
+
return super unless klass.second_level_cache_enabled?
|
14
|
+
return super unless reflection.is_a?(::ActiveRecord::Reflection::BelongsToReflection)
|
15
|
+
return super if klass.default_scopes.present? || reflection.scope
|
16
|
+
return super if association_key_name.to_s != klass.primary_key
|
17
|
+
|
18
|
+
map_cache_keys = ids.map { |id| klass.second_level_cache_key(id) }
|
19
|
+
records_from_cache = ::SecondLevelCache.cache_store.read_multi(*map_cache_keys)
|
20
|
+
|
21
|
+
record_marshals = if RAILS6
|
22
|
+
RecordMarshal.load_multi(records_from_cache.values) do |record|
|
23
|
+
# This block is copy from:
|
24
|
+
# https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/associations/preloader/association.rb#L101
|
25
|
+
owner = owners_by_key[convert_key(record[association_key_name])].first
|
26
|
+
association = owner.association(reflection.name)
|
27
|
+
association.set_inverse_instance(record)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
RecordMarshal.load_multi(records_from_cache.values, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# NOTICE
|
34
|
+
# Rails.cache.read_multi return hash that has keys only hitted.
|
35
|
+
# eg. Rails.cache.read_multi(1,2,3) => {2 => hit_value, 3 => hit_value}
|
36
|
+
hitted_ids = record_marshals.map { |record| record.read_attribute(association_key_name).to_s }
|
37
|
+
missed_ids = ids.map(&:to_s) - hitted_ids
|
38
|
+
ActiveSupport::Notifications.instrument("preload.second_level_cache", key: association_key_name, hit: hitted_ids, miss: missed_ids)
|
39
|
+
return SecondLevelCache::RecordRelation.new(record_marshals) if missed_ids.empty?
|
40
|
+
|
41
|
+
records_from_db = super(missed_ids, &block)
|
42
|
+
records_from_db.map { |r| write_cache(r) }
|
43
|
+
|
44
|
+
SecondLevelCache::RecordRelation.new(records_from_db + record_marshals)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def write_cache(record)
|
50
|
+
record.write_second_level_cache
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -8,11 +8,18 @@ require "second_level_cache/active_record/finder_methods"
|
|
8
8
|
require "second_level_cache/active_record/persistence"
|
9
9
|
require "second_level_cache/active_record/belongs_to_association"
|
10
10
|
require "second_level_cache/active_record/has_one_association"
|
11
|
-
require "second_level_cache/active_record/preloader"
|
11
|
+
require "second_level_cache/active_record/preloader/association"
|
12
|
+
require "second_level_cache/active_record/preloader/legacy"
|
12
13
|
|
13
14
|
# http://api.rubyonrails.org/classes/ActiveSupport/LazyLoadHooks.html
|
14
15
|
# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
|
15
|
-
ActiveSupport.on_load(:active_record) do
|
16
|
+
ActiveSupport.on_load(:active_record, run_once: true) do
|
17
|
+
if Bundler.definition.dependencies.find { |x| x.name == "paranoia" }
|
18
|
+
require "second_level_cache/adapter/paranoia"
|
19
|
+
include SecondLevelCache::Adapter::Paranoia::ActiveRecord
|
20
|
+
SecondLevelCache::Mixin.send(:prepend, SecondLevelCache::Adapter::Paranoia::Mixin)
|
21
|
+
end
|
22
|
+
|
16
23
|
include SecondLevelCache::Mixin
|
17
24
|
prepend SecondLevelCache::ActiveRecord::Base
|
18
25
|
extend SecondLevelCache::ActiveRecord::FetchByUniqKey
|
@@ -21,7 +28,15 @@ ActiveSupport.on_load(:active_record) do
|
|
21
28
|
ActiveRecord::Associations::BelongsToAssociation.send(:prepend, SecondLevelCache::ActiveRecord::Associations::BelongsToAssociation)
|
22
29
|
ActiveRecord::Associations::HasOneAssociation.send(:prepend, SecondLevelCache::ActiveRecord::Associations::HasOneAssociation)
|
23
30
|
ActiveRecord::Relation.send(:prepend, SecondLevelCache::ActiveRecord::FinderMethods)
|
24
|
-
|
25
|
-
# https://github.com/rails/rails/
|
26
|
-
ActiveRecord::
|
31
|
+
|
32
|
+
# https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/associations/preloader/association.rb#L117
|
33
|
+
if ::ActiveRecord.version < ::Gem::Version.new("7")
|
34
|
+
ActiveRecord::Associations::Preloader::Association.send(:prepend, SecondLevelCache::ActiveRecord::Associations::Preloader::Association::Legacy)
|
35
|
+
end
|
36
|
+
|
37
|
+
if ::ActiveRecord.version >= ::Gem::Version.new("7")
|
38
|
+
# https://github.com/rails/rails/blob/7-0-stable/activerecord/lib/active_record/associations/preloader/association.rb#L25
|
39
|
+
ActiveRecord::Associations::Preloader::Association.send(:prepend, SecondLevelCache::ActiveRecord::Associations::Preloader::Association)
|
40
|
+
ActiveRecord::Associations::Preloader::Association::LoaderQuery.send(:prepend, SecondLevelCache::ActiveRecord::Associations::Preloader::Association::LoaderQuery)
|
41
|
+
end
|
27
42
|
end
|
@@ -0,0 +1,25 @@
|
|
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
|
+
|
21
|
+
alias_method :update_second_level_cache, :write_second_level_cache
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -71,13 +71,12 @@ module SecondLevelCache
|
|
71
71
|
|
72
72
|
def write_second_level_cache
|
73
73
|
return unless klass.second_level_cache_enabled?
|
74
|
-
# Avoid rewrite cache again, when record has been soft deleted
|
75
|
-
return if respond_to?(:deleted?) && send(:deleted?)
|
76
74
|
|
77
75
|
marshal = RecordMarshal.dump(self)
|
78
76
|
expires_in = klass.second_level_cache_options[:expires_in]
|
79
77
|
SecondLevelCache.cache_store.write(second_level_cache_key, marshal, expires_in: expires_in)
|
80
78
|
end
|
81
|
-
|
79
|
+
|
80
|
+
alias_method :update_second_level_cache, :write_second_level_cache
|
82
81
|
end
|
83
82
|
end
|
data/second_level_cache.gemspec
CHANGED
@@ -5,10 +5,10 @@ lib = File.expand_path("../lib", __FILE__)
|
|
5
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
6
|
|
7
7
|
Gem::Specification.new do |gem|
|
8
|
-
gem.authors
|
9
|
-
gem.email
|
10
|
-
gem.description
|
11
|
-
gem.summary
|
8
|
+
gem.authors = ["Hooopo"]
|
9
|
+
gem.email = ["hoooopo@gmail.com"]
|
10
|
+
gem.description = "Write Through and Read Through caching library inspired by CacheMoney and cache_fu, support ActiveRecord 4."
|
11
|
+
gem.summary = <<-SUMMARY
|
12
12
|
SecondLevelCache is a write-through and read-through caching library inspired by Cache Money and cache_fu, support only Rails3 and ActiveRecord.
|
13
13
|
|
14
14
|
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.
|
@@ -16,23 +16,24 @@ Gem::Specification.new do |gem|
|
|
16
16
|
Write-Through: As objects are created, updated, and deleted, all of the caches are automatically kept up-to-date and coherent.
|
17
17
|
SUMMARY
|
18
18
|
|
19
|
-
gem.homepage
|
19
|
+
gem.homepage = "https://github.com/hooopo/second_level_cache"
|
20
20
|
|
21
|
-
gem.files
|
21
|
+
gem.files = Dir.glob("lib/**/*.rb") + [
|
22
22
|
"README.md",
|
23
|
+
"LICENSE",
|
23
24
|
"Rakefile",
|
24
25
|
"Gemfile",
|
25
26
|
"CHANGELOG.md",
|
26
27
|
"second_level_cache.gemspec"
|
27
28
|
]
|
28
|
-
gem.test_files
|
29
|
-
gem.executables
|
30
|
-
gem.name
|
29
|
+
gem.test_files = Dir.glob("test/**/*.rb")
|
30
|
+
gem.executables = gem.files.grep(%r{^bin/})
|
31
|
+
gem.name = "second_level_cache"
|
31
32
|
gem.require_paths = ["lib"]
|
32
|
-
gem.version
|
33
|
+
gem.version = SecondLevelCache::VERSION
|
33
34
|
|
34
|
-
gem.add_runtime_dependency "activerecord",
|
35
|
-
gem.add_runtime_dependency "activesupport",
|
35
|
+
gem.add_runtime_dependency "activerecord", ">= 6.0"
|
36
|
+
gem.add_runtime_dependency "activesupport", ">= 6.0"
|
36
37
|
|
37
38
|
gem.add_development_dependency "database_cleaner"
|
38
39
|
gem.add_development_dependency "rake"
|
@@ -6,15 +6,16 @@ class FetchByUinqKeyTest < ActiveSupport::TestCase
|
|
6
6
|
def setup
|
7
7
|
@user = User.create name: "hooopo", email: "hoooopo@gmail.com"
|
8
8
|
@post = Post.create slug: "foobar", topic_id: 2
|
9
|
+
@cache_prefix = SecondLevelCache.configure.cache_key_prefix
|
9
10
|
end
|
10
11
|
|
11
12
|
def test_cache_uniq_key
|
12
|
-
assert_equal User.send(:cache_uniq_key, name: "hooopo"), "uniq_key_User_name_hooopo"
|
13
|
-
assert_equal User.send(:cache_uniq_key, foo: 1, bar: 2), "uniq_key_User_foo_1,bar_2"
|
14
|
-
assert_equal User.send(:cache_uniq_key, foo: 1, bar: nil), "uniq_key_User_foo_1,bar_"
|
13
|
+
assert_equal User.send(:cache_uniq_key, name: "hooopo"), "#{@cache_prefix}/uniq_key_User_name_hooopo"
|
14
|
+
assert_equal User.send(:cache_uniq_key, foo: 1, bar: 2), "#{@cache_prefix}/uniq_key_User_foo_1,bar_2"
|
15
|
+
assert_equal User.send(:cache_uniq_key, foo: 1, bar: nil), "#{@cache_prefix}/uniq_key_User_foo_1,bar_"
|
15
16
|
long_val = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
16
|
-
assert_equal User.send(:cache_uniq_key, foo: 1, bar: long_val), "uniq_key_User_foo_1,bar_#{Digest::MD5.hexdigest(long_val)}"
|
17
|
-
assert Contribution.send(:cache_uniq_key, user_id: 1, date: Time.current.to_date), "uniq_key_Contribution_user_id_1,date_#{Time.current.to_date}"
|
17
|
+
assert_equal User.send(:cache_uniq_key, foo: 1, bar: long_val), "#{@cache_prefix}/uniq_key_User_foo_1,bar_#{Digest::MD5.hexdigest(long_val)}"
|
18
|
+
assert Contribution.send(:cache_uniq_key, user_id: 1, date: Time.current.to_date), "#{@cache_prefix}/uniq_key_Contribution_user_id_1,date_#{Time.current.to_date}"
|
18
19
|
end
|
19
20
|
|
20
21
|
def test_record_attributes_equal_where_values
|
@@ -30,6 +30,10 @@ class HasOneAssociationTest < ActiveSupport::TestCase
|
|
30
30
|
ForkedUserLink.without_second_level_cache do
|
31
31
|
assert_queries(1) { user.forked_from_user }
|
32
32
|
end
|
33
|
+
# NoMethodError: undefined method `klass' for nil:NilClass active_record/has_one_association.rb:14:in `find_target'
|
34
|
+
hotspot = Hotspot.create(summary: "summary")
|
35
|
+
assert_equal hotspot.persisted?, true
|
36
|
+
assert_nil hotspot.topic
|
33
37
|
end
|
34
38
|
|
35
39
|
def test_has_one_with_conditions
|
data/test/model/post.rb
CHANGED
@@ -6,7 +6,17 @@ ActiveRecord::Base.connection.create_table(:posts, force: true) do |t|
|
|
6
6
|
t.integer :topic_id
|
7
7
|
end
|
8
8
|
|
9
|
+
ActiveRecord::Base.connection.create_table(:hotspots, force: true) do |t|
|
10
|
+
t.integer :post_id
|
11
|
+
t.string :summary
|
12
|
+
end
|
13
|
+
|
9
14
|
class Post < ApplicationRecord
|
10
15
|
second_level_cache
|
11
16
|
belongs_to :topic, touch: true
|
12
17
|
end
|
18
|
+
|
19
|
+
class Hotspot < ApplicationRecord
|
20
|
+
belongs_to :post, required: false
|
21
|
+
has_one :topic, through: :post
|
22
|
+
end
|
data/test/model/user.rb
CHANGED
@@ -9,7 +9,6 @@ ActiveRecord::Base.connection.create_table(:users, force: true) do |t|
|
|
9
9
|
t.integer :status, default: 0
|
10
10
|
t.integer :books_count, default: 0
|
11
11
|
t.integer :images_count, default: 0
|
12
|
-
t.datetime :deleted_at
|
13
12
|
t.timestamps null: false, precision: 6
|
14
13
|
end
|
15
14
|
|
@@ -29,7 +28,6 @@ end
|
|
29
28
|
class User < ApplicationRecord
|
30
29
|
CACHE_VERSION = 3
|
31
30
|
second_level_cache(version: CACHE_VERSION, expires_in: 3.days)
|
32
|
-
acts_as_paranoid
|
33
31
|
|
34
32
|
serialize :options, Array
|
35
33
|
serialize :json_options, JSON if ::ActiveRecord::VERSION::STRING >= "4.1.0"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class ParanoidTest < ActiveSupport::TestCase
|
6
|
+
def setup
|
7
|
+
skip unless defined?(Paranoi)
|
8
|
+
@paranoid = Paranoid.create
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_should_expire_cache_when_destroy
|
12
|
+
skip unless defined(Paranoi)
|
13
|
+
@paranoid.destroy
|
14
|
+
assert_nil Paranoid.find_by(id: @paranoid.id)
|
15
|
+
assert_nil SecondLevelCache.cache_store.read(@paranoid.second_level_cache_key)
|
16
|
+
assert_nil User.read_second_level_cache(@paranoid.id)
|
17
|
+
end
|
18
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -2,11 +2,10 @@
|
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
4
|
require "minitest/autorun"
|
5
|
-
require "active_support/
|
5
|
+
require "active_support/all"
|
6
6
|
require "active_record_test_case_helper"
|
7
7
|
require "database_cleaner"
|
8
8
|
require "active_record"
|
9
|
-
require "paranoia"
|
10
9
|
require "pry"
|
11
10
|
ActiveSupport.test_order = :sorted if ActiveSupport.respond_to?(:test_order=)
|
12
11
|
# Force hook :active_record on_load event to make sure loader can work.
|
@@ -27,6 +26,7 @@ require "model/order_item"
|
|
27
26
|
require "model/account"
|
28
27
|
require "model/animal"
|
29
28
|
require "model/contribution"
|
29
|
+
require "model/paranoid" if defined?(Paranoid)
|
30
30
|
|
31
31
|
DatabaseCleaner[:active_record].strategy = :truncation
|
32
32
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: second_level_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hooopo
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,40 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '7'
|
19
|
+
version: '6.0'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '7'
|
26
|
+
version: '6.0'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: activesupport
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
36
30
|
requirements:
|
37
31
|
- - ">="
|
38
32
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
40
|
-
- - "<"
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
version: '7'
|
33
|
+
version: '6.0'
|
43
34
|
type: :runtime
|
44
35
|
prerelease: false
|
45
36
|
version_requirements: !ruby/object:Gem::Requirement
|
46
37
|
requirements:
|
47
38
|
- - ">="
|
48
39
|
- !ruby/object:Gem::Version
|
49
|
-
version: '
|
50
|
-
- - "<"
|
51
|
-
- !ruby/object:Gem::Version
|
52
|
-
version: '7'
|
40
|
+
version: '6.0'
|
53
41
|
- !ruby/object:Gem::Dependency
|
54
42
|
name: database_cleaner
|
55
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -116,6 +104,7 @@ extra_rdoc_files: []
|
|
116
104
|
files:
|
117
105
|
- CHANGELOG.md
|
118
106
|
- Gemfile
|
107
|
+
- LICENSE
|
119
108
|
- README.md
|
120
109
|
- Rakefile
|
121
110
|
- lib/second_level_cache.rb
|
@@ -127,7 +116,9 @@ files:
|
|
127
116
|
- lib/second_level_cache/active_record/finder_methods.rb
|
128
117
|
- lib/second_level_cache/active_record/has_one_association.rb
|
129
118
|
- lib/second_level_cache/active_record/persistence.rb
|
130
|
-
- lib/second_level_cache/active_record/preloader.rb
|
119
|
+
- lib/second_level_cache/active_record/preloader/association.rb
|
120
|
+
- lib/second_level_cache/active_record/preloader/legacy.rb
|
121
|
+
- lib/second_level_cache/adapter/paranoia.rb
|
131
122
|
- lib/second_level_cache/config.rb
|
132
123
|
- lib/second_level_cache/log_subscriber.rb
|
133
124
|
- lib/second_level_cache/mixin.rb
|
@@ -150,9 +141,11 @@ files:
|
|
150
141
|
- test/model/image.rb
|
151
142
|
- test/model/order.rb
|
152
143
|
- test/model/order_item.rb
|
144
|
+
- test/model/paranoid.rb
|
153
145
|
- test/model/post.rb
|
154
146
|
- test/model/topic.rb
|
155
147
|
- test/model/user.rb
|
148
|
+
- test/paranoid_test.rb
|
156
149
|
- test/persistence_test.rb
|
157
150
|
- test/polymorphic_association_test.rb
|
158
151
|
- test/preloader_belongs_to_test.rb
|
@@ -167,7 +160,7 @@ files:
|
|
167
160
|
homepage: https://github.com/hooopo/second_level_cache
|
168
161
|
licenses: []
|
169
162
|
metadata: {}
|
170
|
-
post_install_message:
|
163
|
+
post_install_message:
|
171
164
|
rdoc_options: []
|
172
165
|
require_paths:
|
173
166
|
- lib
|
@@ -182,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
175
|
- !ruby/object:Gem::Version
|
183
176
|
version: '0'
|
184
177
|
requirements: []
|
185
|
-
rubygems_version: 3.
|
186
|
-
signing_key:
|
178
|
+
rubygems_version: 3.2.3
|
179
|
+
signing_key:
|
187
180
|
specification_version: 4
|
188
181
|
summary: 'SecondLevelCache is a write-through and read-through caching library inspired
|
189
182
|
by Cache Money and cache_fu, support only Rails3 and ActiveRecord. Read-Through:
|
@@ -192,32 +185,34 @@ summary: 'SecondLevelCache is a write-through and read-through caching library i
|
|
192
185
|
is a cache miss, it will populate the cache. Write-Through: As objects are created,
|
193
186
|
updated, and deleted, all of the caches are automatically kept up-to-date and coherent.'
|
194
187
|
test_files:
|
195
|
-
- test/
|
196
|
-
- test/
|
197
|
-
- test/require_test.rb
|
198
|
-
- test/finder_methods_test.rb
|
199
|
-
- test/preloader_has_one_test.rb
|
200
|
-
- test/preloader_non_integer_test.rb
|
188
|
+
- test/active_record_test_case_helper.rb
|
189
|
+
- test/base_test.rb
|
201
190
|
- test/belongs_to_association_test.rb
|
202
|
-
- test/second_level_cache_test.rb
|
203
191
|
- test/enum_attr_test.rb
|
204
|
-
- test/
|
205
|
-
- test/
|
206
|
-
- test/
|
207
|
-
- test/single_table_inheritance_test.rb
|
208
|
-
- test/model/image.rb
|
192
|
+
- test/fetch_by_uniq_key_test.rb
|
193
|
+
- test/finder_methods_test.rb
|
194
|
+
- test/has_one_association_test.rb
|
209
195
|
- test/model/account.rb
|
210
|
-
- test/model/order_item.rb
|
211
|
-
- test/model/contribution.rb
|
212
|
-
- test/model/book.rb
|
213
|
-
- test/model/topic.rb
|
214
196
|
- test/model/animal.rb
|
215
|
-
- test/model/order.rb
|
216
197
|
- test/model/application_record.rb
|
198
|
+
- test/model/book.rb
|
199
|
+
- test/model/contribution.rb
|
200
|
+
- test/model/image.rb
|
201
|
+
- test/model/order.rb
|
202
|
+
- test/model/order_item.rb
|
203
|
+
- test/model/paranoid.rb
|
217
204
|
- test/model/post.rb
|
205
|
+
- test/model/topic.rb
|
218
206
|
- test/model/user.rb
|
219
|
-
- test/
|
207
|
+
- test/paranoid_test.rb
|
208
|
+
- test/persistence_test.rb
|
209
|
+
- test/polymorphic_association_test.rb
|
210
|
+
- test/preloader_belongs_to_test.rb
|
211
|
+
- test/preloader_has_many_test.rb
|
212
|
+
- test/preloader_has_one_test.rb
|
213
|
+
- test/preloader_non_integer_test.rb
|
214
|
+
- test/record_marshal_test.rb
|
215
|
+
- test/require_test.rb
|
216
|
+
- test/second_level_cache_test.rb
|
217
|
+
- test/single_table_inheritance_test.rb
|
220
218
|
- test/test_helper.rb
|
221
|
-
- test/has_one_association_test.rb
|
222
|
-
- test/base_test.rb
|
223
|
-
- test/active_record_test_case_helper.rb
|
@@ -1,50 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module SecondLevelCache
|
4
|
-
module ActiveRecord
|
5
|
-
module Associations
|
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?
|
13
|
-
|
14
|
-
map_cache_keys = ids.map { |id| klass.second_level_cache_key(id) }
|
15
|
-
records_from_cache = ::SecondLevelCache.cache_store.read_multi(*map_cache_keys)
|
16
|
-
|
17
|
-
record_marshals = if RAILS6
|
18
|
-
RecordMarshal.load_multi(records_from_cache.values) do |record|
|
19
|
-
# This block is copy from:
|
20
|
-
# https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/associations/preloader/association.rb#L101
|
21
|
-
owner = owners_by_key[convert_key(record[association_key_name])].first
|
22
|
-
association = owner.association(reflection.name)
|
23
|
-
association.set_inverse_instance(record)
|
24
|
-
end
|
25
|
-
else
|
26
|
-
RecordMarshal.load_multi(records_from_cache.values, &block)
|
27
|
-
end
|
28
|
-
|
29
|
-
# NOTICE
|
30
|
-
# Rails.cache.read_multi return hash that has keys only hitted.
|
31
|
-
# eg. Rails.cache.read_multi(1,2,3) => {2 => hit_value, 3 => hit_value}
|
32
|
-
hitted_ids = record_marshals.map { |record| record.read_attribute(association_key_name).to_s }
|
33
|
-
missed_ids = ids.map(&:to_s) - hitted_ids
|
34
|
-
ActiveSupport::Notifications.instrument("preload.second_level_cache", key: association_key_name, hit: hitted_ids, miss: missed_ids)
|
35
|
-
return SecondLevelCache::RecordRelation.new(record_marshals) if missed_ids.empty?
|
36
|
-
|
37
|
-
records_from_db = super(missed_ids, &block)
|
38
|
-
records_from_db.map { |r| write_cache(r) }
|
39
|
-
|
40
|
-
SecondLevelCache::RecordRelation.new(records_from_db + record_marshals)
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
def write_cache(record)
|
45
|
-
record.write_second_level_cache
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|