second_level_cache 2.6.1 → 2.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0056aa765b7668e44d115f964c4fb11f1632ecf99206145c89aed0fb58f2b4d0
4
- data.tar.gz: a4fb19d6ff3859c728c7d8a43f28f64bb092c89226772de31b5b22f1e57e2cf6
3
+ metadata.gz: 9161291526eed87047708e5128c64e2907ddf61f017dbf073831c1281b6b504d
4
+ data.tar.gz: 192fffc74fe056dd8bc2d8e6486f4f5de380698bfc728d65bf02853c110a9d66
5
5
  SHA512:
6
- metadata.gz: 32f1cb2460325a2291d2732c2f3ec906c7c16c2c5838564df7bfa56f362c0049d37c1ef2658db6c057faf31183869bbd8d6fc4702f4056f0a8bc4d143933cc68
7
- data.tar.gz: 46e242047cd4e0c1c3651dc3e7ff03028d044a96b6c4c915d994114226c5ed30c8668248b14caac8165b51a283ba6ab9455badc5c27c6f7a31468753086216c4
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
@@ -3,6 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
- gem "paranoia", "~> 2.4.2"
7
6
  gem "pry"
8
- gem "rails", "~> 6"
7
+ gem "rails", "~> 7"
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
- [![Build Status](https://travis-ci.org/hooopo/second_level_cache.svg?branch=master)](https://travis-ci.org/hooopo/second_level_cache)
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 5.2:
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.4.0'
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
- * SecondLevelCache cache by model name and id, so only find_one query will work.
101
- * Only equal conditions query WILL get cache; and SQL string query like `User.where("name = 'Hooopo'").find(1)` WILL NOT work.
102
- * SecondLevelCache sync cache after transaction commit:
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
- * If you are using SecondLevelCache with database_cleaner, you should set cleaning strategy to `:truncation`:
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
- * 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`:
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
- * SecondLevelCache was added model schema digest as cache version, this means when you add/remove/change columns, the caches of this Model will expires.
144
- * When your want change the model cache version by manualy, just add the `version` option like this:
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
- * It provides a great feature, not hits db when fetching record via unique key (not primary key).
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
- * 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:
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
- * [chloerei](https://github.com/chloerei)
175
- * [hooopo](https://github.com/hooopo)
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
- 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) }
@@ -9,10 +9,10 @@ module SecondLevelCache
9
9
 
10
10
  if obj_id
11
11
  record = begin
12
- find(obj_id)
13
- rescue StandardError
14
- nil
15
- end
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
- ext_key = keys.join(",")
52
- "uniq_key_#{name}_#{ext_key}"
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
- 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
+ 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 klass.reflections[through.to_s].klass.second_level_cache_enabled?
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
- # Rails 5.2 has removed ActiveRecord::Associations::Preloader::BelongsTo
25
- # https://github.com/rails/rails/pull/31079
26
- ActiveRecord::Associations::Preloader::Association.send(:prepend, SecondLevelCache::ActiveRecord::Associations::Preloader)
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
- alias update_second_level_cache write_second_level_cache
79
+
80
+ alias_method :update_second_level_cache, :write_second_level_cache
82
81
  end
83
82
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecondLevelCache
4
- VERSION = "2.6.1"
4
+ VERSION = "2.7.0"
5
5
  end
@@ -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 = ["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
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 = "https://github.com/hooopo/second_level_cache"
19
+ gem.homepage = "https://github.com/hooopo/second_level_cache"
20
20
 
21
- gem.files = Dir.glob("lib/**/*.rb") + [
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 = Dir.glob("test/**/*.rb")
29
- gem.executables = gem.files.grep(%r{^bin/})
30
- gem.name = "second_level_cache"
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 = SecondLevelCache::VERSION
33
+ gem.version = SecondLevelCache::VERSION
33
34
 
34
- gem.add_runtime_dependency "activerecord", [">= 5.2", "< 7"]
35
- gem.add_runtime_dependency "activesupport", [">= 5.2", "< 7"]
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"
@@ -11,7 +11,7 @@ class BelongsToAssociationTest < ActiveSupport::TestCase
11
11
  book = @user.books.create
12
12
 
13
13
  @user.write_second_level_cache
14
- book.send(:clear_association_cache)
14
+
15
15
  assert_no_queries do
16
16
  assert_equal @user, book.user
17
17
  end
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Base.connection.create_table(:paranoids, force: true) do |t|
4
+ t.datetime :deleted_at
5
+ end
6
+
7
+ class Paranoid < ApplicationRecord
8
+ second_level_cache
9
+ acts_as_paranoid
10
+ end
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/test_case"
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.6.1
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: 2020-05-22 00:00:00.000000000 Z
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: '5.2'
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: '5.2'
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: '5.2'
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: '5.2'
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.0.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/preloader_belongs_to_test.rb
196
- - test/polymorphic_association_test.rb
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/record_marshal_test.rb
205
- - test/preloader_has_many_test.rb
206
- - test/persistence_test.rb
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/fetch_by_uniq_key_test.rb
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