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
@@ -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
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SecondLevelCache
|
4
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
# preload.second_level_cache
|
6
|
+
def preload(event)
|
7
|
+
prefix = color("SecondLevelCache", CYAN)
|
8
|
+
miss_ids = (event.payload[:miss] || []).join(",")
|
9
|
+
hit_ids = (event.payload[:hit] || []).join(",")
|
10
|
+
debug " #{prefix} preload #{event.payload[:key]} miss [#{miss_ids}], hit [#{hit_ids}]"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
SecondLevelCache::LogSubscriber.attach_to :second_level_cache
|
@@ -71,23 +71,11 @@ 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
|
-
expire_changed_association_uniq_keys
|
80
77
|
SecondLevelCache.cache_store.write(second_level_cache_key, marshal, expires_in: expires_in)
|
81
78
|
end
|
82
|
-
|
83
79
|
alias update_second_level_cache write_second_level_cache
|
84
|
-
|
85
|
-
def expire_changed_association_uniq_keys
|
86
|
-
reflections = klass.reflections.select { |_, reflection| reflection.belongs_to? }
|
87
|
-
changed_keys = reflections.map { |_, reflection| reflection.foreign_key } & previous_changes.keys
|
88
|
-
changed_keys.each do |key|
|
89
|
-
SecondLevelCache.cache_store.delete(klass.send(:cache_uniq_key, key => previous_changes[key][0]))
|
90
|
-
end
|
91
|
-
end
|
92
80
|
end
|
93
81
|
end
|
@@ -2,54 +2,21 @@
|
|
2
2
|
|
3
3
|
module RecordMarshal
|
4
4
|
class << self
|
5
|
-
# dump ActiveRecord
|
6
|
-
# ["User",
|
7
|
-
# {"id"=>30,
|
8
|
-
# "email"=>"dddssddd@gmail.com",
|
9
|
-
# "created_at"=>2012-07-25 18:25:57 UTC
|
10
|
-
# }
|
11
|
-
# ]
|
12
|
-
|
5
|
+
# dump ActiveRecord instance with only attributes before type cast.
|
13
6
|
def dump(record)
|
14
|
-
[record.class.name, record.
|
7
|
+
[record.class.name, record.attributes_before_type_cast]
|
15
8
|
end
|
16
9
|
|
17
10
|
# load a cached record
|
18
|
-
def load(serialized)
|
11
|
+
def load(serialized, &block)
|
19
12
|
return unless serialized
|
20
|
-
# fix issues 19
|
21
|
-
# fix 2.1.2 object.changed? ActiveRecord::SerializationTypeMismatch: Attribute was supposed to be a Hash, but was a String. -- "{:a=>\"t\", :b=>\"x\"}"
|
22
|
-
# fix 2.1.4 object.changed? is true
|
23
|
-
# fix Rails 4.2 is deprecating `serialized_attributes` without replacement to Rails 5 is deprecating `serialized_attributes` without replacement
|
24
|
-
klass = serialized[0].constantize
|
25
|
-
attributes = serialized[1]
|
26
|
-
|
27
|
-
# for ActiveRecord 5.0.0
|
28
|
-
klass.columns.each do |c|
|
29
|
-
name = c.name
|
30
|
-
cast_type = klass.attribute_types[name]
|
31
|
-
next unless cast_type.is_a?(::ActiveRecord::Type::Serialized)
|
32
|
-
coder = cast_type.coder
|
33
|
-
next if attributes[name].nil? || attributes[name].is_a?(String)
|
34
|
-
if coder.is_a?(::ActiveRecord::Coders::YAMLColumn)
|
35
|
-
attributes[name] = coder.dump(attributes[name]) if attributes[name].is_a?(coder.object_class)
|
36
|
-
elsif coder.is_a?(::ActiveRecord::Store::IndifferentCoder)
|
37
|
-
# https://github.com/rails/rails/blob/5b14129/activerecord/lib/active_record/store.rb#L179
|
38
|
-
attributes[name] = coder.dump(attributes[name])
|
39
|
-
elsif coder == ::ActiveRecord::Coders::JSON
|
40
|
-
attributes[name] = attributes[name].to_json
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
klass.defined_enums.each do |key, value|
|
45
|
-
attributes[key] = value[attributes[key]] if attributes[key].is_a?(String)
|
46
|
-
end
|
47
13
|
|
48
|
-
|
14
|
+
serialized[0].constantize.instantiate(serialized[1], &block)
|
49
15
|
end
|
50
16
|
|
51
|
-
|
52
|
-
|
17
|
+
# load multi cached records
|
18
|
+
def load_multi(serializeds, &block)
|
19
|
+
serializeds.map { |serialized| load(serialized, &block) }
|
53
20
|
end
|
54
21
|
end
|
55
22
|
end
|
data/second_level_cache.gemspec
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("../lib/second_level_cache/version", __FILE__)
|
4
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
6
|
|
6
7
|
Gem::Specification.new do |gem|
|
7
|
-
gem.authors = [
|
8
|
-
gem.email = [
|
9
|
-
gem.description =
|
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."
|
10
11
|
gem.summary = <<-SUMMARY
|
11
12
|
SecondLevelCache is a write-through and read-through caching library inspired by Cache Money and cache_fu, support only Rails3 and ActiveRecord.
|
12
13
|
|
@@ -15,26 +16,26 @@ Gem::Specification.new do |gem|
|
|
15
16
|
Write-Through: As objects are created, updated, and deleted, all of the caches are automatically kept up-to-date and coherent.
|
16
17
|
SUMMARY
|
17
18
|
|
18
|
-
gem.homepage =
|
19
|
+
gem.homepage = "https://github.com/hooopo/second_level_cache"
|
19
20
|
|
20
|
-
gem.files = Dir.glob(
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
gem.files = Dir.glob("lib/**/*.rb") + [
|
22
|
+
"README.md",
|
23
|
+
"Rakefile",
|
24
|
+
"Gemfile",
|
25
|
+
"CHANGELOG.md",
|
26
|
+
"second_level_cache.gemspec"
|
26
27
|
]
|
27
|
-
gem.test_files = Dir.glob(
|
28
|
+
gem.test_files = Dir.glob("test/**/*.rb")
|
28
29
|
gem.executables = gem.files.grep(%r{^bin/})
|
29
|
-
gem.name =
|
30
|
-
gem.require_paths = [
|
30
|
+
gem.name = "second_level_cache"
|
31
|
+
gem.require_paths = ["lib"]
|
31
32
|
gem.version = SecondLevelCache::VERSION
|
32
33
|
|
33
|
-
gem.add_runtime_dependency
|
34
|
-
gem.add_runtime_dependency
|
34
|
+
gem.add_runtime_dependency "activerecord", [">= 5.2", "< 7"]
|
35
|
+
gem.add_runtime_dependency "activesupport", [">= 5.2", "< 7"]
|
35
36
|
|
36
|
-
gem.add_development_dependency
|
37
|
-
gem.add_development_dependency
|
38
|
-
gem.add_development_dependency
|
39
|
-
gem.add_development_dependency
|
37
|
+
gem.add_development_dependency "database_cleaner"
|
38
|
+
gem.add_development_dependency "rake"
|
39
|
+
gem.add_development_dependency "rubocop"
|
40
|
+
gem.add_development_dependency "sqlite3", "> 1.4"
|
40
41
|
end
|
@@ -6,26 +6,30 @@ 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
|
-
def
|
21
|
-
book = Book.new
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
def test_record_attributes_equal_where_values
|
22
|
+
book = Book.new
|
23
|
+
assert_no_queries do
|
24
|
+
book.title = "foobar"
|
25
|
+
assert Book.send(:record_attributes_equal_where_values?, book, title: :foobar)
|
26
|
+
book.discount_percentage = 60.00
|
27
|
+
assert Book.send(:record_attributes_equal_where_values?, book, discount_percentage: "60")
|
28
|
+
book.publish_date = Time.current.to_date
|
29
|
+
assert Book.send(:record_attributes_equal_where_values?, book, publish_date: Time.current.to_date.to_s)
|
30
|
+
book.title = nil
|
31
|
+
assert Book.send(:record_attributes_equal_where_values?, book, title: nil)
|
32
|
+
end
|
29
33
|
end
|
30
34
|
|
31
35
|
def test_should_query_from_db_using_primary_key
|
data/test/finder_methods_test.rb
CHANGED
@@ -59,6 +59,45 @@ class FinderMethodsTest < ActiveSupport::TestCase
|
|
59
59
|
refute_equal @user.name, @from_db.name
|
60
60
|
end
|
61
61
|
|
62
|
+
def test_should_fetch_from_db_if_where_use_string
|
63
|
+
@user.write_second_level_cache
|
64
|
+
assert_queries(:any) do
|
65
|
+
assert_nil User.unscoped.where(id: @user.id).where("name = 'nonexistent'").first
|
66
|
+
end
|
67
|
+
assert_queries(:any) do
|
68
|
+
assert_raises ActiveRecord::RecordNotFound do
|
69
|
+
User.where("name = 'nonexistent'").find(@user.id)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_should_fetch_from_db_when_use_eager_load
|
75
|
+
@user.write_second_level_cache
|
76
|
+
assert_queries(:any) do
|
77
|
+
assert_sql(/LEFT\sOUTER\sJOIN\s\"books\"/m) do
|
78
|
+
User.eager_load(:books).find(@user.id)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_should_fetch_from_db_when_use_includes
|
84
|
+
@user.write_second_level_cache
|
85
|
+
assert_queries(:any) do
|
86
|
+
assert_sql(/SELECT\s\"books\"\.\*\sFROM\s\"books\"/m) do
|
87
|
+
User.includes(:books).find(@user.id)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_should_fetch_from_db_when_use_preload
|
93
|
+
@user.write_second_level_cache
|
94
|
+
assert_queries(:any) do
|
95
|
+
assert_sql(/SELECT\s\"books\"\.\*\sFROM\s\"books\"/m) do
|
96
|
+
User.preload(:books).find(@user.id)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
62
101
|
def test_where_and_first_should_with_cache
|
63
102
|
@user.write_second_level_cache
|
64
103
|
assert_no_queries do
|
@@ -20,10 +20,20 @@ class HasOneAssociationTest < ActiveSupport::TestCase
|
|
20
20
|
clean_user = user.reload
|
21
21
|
assert_equal User, clean_user.forked_from_user.class
|
22
22
|
assert_equal @user.id, user.forked_from_user.id
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
# If ForkedUserLink second_level_cache_enabled is true
|
24
|
+
user.reload
|
25
|
+
assert_no_queries do
|
26
|
+
user.forked_from_user
|
27
|
+
end
|
28
|
+
# IF ForkedUserLink second_level_cache_enabled is false
|
29
|
+
user.reload
|
30
|
+
ForkedUserLink.without_second_level_cache do
|
31
|
+
assert_queries(1) { user.forked_from_user }
|
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
|
27
37
|
end
|
28
38
|
|
29
39
|
def test_has_one_with_conditions
|
@@ -49,4 +59,10 @@ class HasOneAssociationTest < ActiveSupport::TestCase
|
|
49
59
|
@account.update(user_id: @user.id + 1)
|
50
60
|
assert_nil @user.reload.account
|
51
61
|
end
|
62
|
+
|
63
|
+
def test_should_one_query_when_has_one_target_is_null
|
64
|
+
Namespace.destroy_all
|
65
|
+
@user.reload
|
66
|
+
assert_queries(1) { @user.namespace }
|
67
|
+
end
|
52
68
|
end
|
data/test/model/account.rb
CHANGED
@@ -7,7 +7,7 @@ ActiveRecord::Base.connection.create_table(:accounts, force: true) do |t|
|
|
7
7
|
t.timestamps null: false
|
8
8
|
end
|
9
9
|
|
10
|
-
class Account <
|
10
|
+
class Account < ApplicationRecord
|
11
11
|
second_level_cache expires_in: 3.days
|
12
|
-
belongs_to :user
|
12
|
+
belongs_to :user, foreign_key: :user_id, inverse_of: :account
|
13
13
|
end
|
data/test/model/animal.rb
CHANGED
data/test/model/book.rb
CHANGED
@@ -7,11 +7,14 @@ ActiveRecord::Base.connection.create_table(:books, force: true) do |t|
|
|
7
7
|
t.decimal :discount_percentage, precision: 5, scale: 2
|
8
8
|
t.integer :images_count, default: 0
|
9
9
|
t.date :publish_date
|
10
|
+
t.boolean :normal, default: true, nil: false
|
10
11
|
end
|
11
12
|
|
12
|
-
class Book <
|
13
|
+
class Book < ApplicationRecord
|
13
14
|
second_level_cache
|
14
15
|
|
16
|
+
default_scope -> { where(normal: true) }
|
17
|
+
|
15
18
|
belongs_to :user, counter_cache: true
|
16
19
|
has_many :images, as: :imagable
|
17
20
|
end
|
data/test/model/contribution.rb
CHANGED
@@ -6,7 +6,7 @@ ActiveRecord::Base.connection.create_table(:contributions, force: true) do |t|
|
|
6
6
|
t.date :date
|
7
7
|
end
|
8
8
|
|
9
|
-
class Contribution <
|
9
|
+
class Contribution < ApplicationRecord
|
10
10
|
second_level_cache
|
11
11
|
|
12
12
|
validates_uniqueness_of :user_id, scope: :date, if: -> { user_id_changed? || date_changed? }
|
data/test/model/image.rb
CHANGED
data/test/model/order.rb
CHANGED
data/test/model/order_item.rb
CHANGED
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
|
-
|
9
|
+
ActiveRecord::Base.connection.create_table(:hotspots, force: true) do |t|
|
10
|
+
t.integer :post_id
|
11
|
+
t.string :summary
|
12
|
+
end
|
13
|
+
|
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
|