second_level_cache 2.5.0 → 2.6.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +1 -1
- data/lib/second_level_cache.rb +1 -0
- data/lib/second_level_cache/active_record.rb +8 -2
- data/lib/second_level_cache/active_record/base.rb +1 -5
- data/lib/second_level_cache/active_record/belongs_to_association.rb +6 -4
- data/lib/second_level_cache/active_record/fetch_by_uniq_key.rb +24 -17
- data/lib/second_level_cache/active_record/finder_methods.rb +44 -42
- data/lib/second_level_cache/active_record/has_one_association.rb +14 -20
- data/lib/second_level_cache/active_record/persistence.rb +6 -2
- 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 -23
- data/test/active_record_test_case_helper.rb +11 -1
- data/test/fetch_by_uniq_key_test.rb +47 -4
- data/test/finder_methods_test.rb +39 -0
- data/test/has_one_association_test.rb +16 -4
- data/test/model/account.rb +2 -2
- data/test/model/animal.rb +1 -1
- data/test/model/application_record.rb +3 -0
- data/test/model/book.rb +6 -1
- data/test/model/contribution.rb +14 -0
- data/test/model/image.rb +1 -1
- data/test/model/order.rb +1 -1
- data/test/model/order_item.rb +1 -1
- data/test/model/paranoid.rb +10 -0
- data/test/model/post.rb +1 -1
- data/test/model/topic.rb +1 -1
- data/test/model/user.rb +7 -7
- data/test/paranoid_test.rb +16 -0
- data/test/{preloader_test.rb → preloader_belongs_to_test.rb} +17 -15
- data/test/preloader_has_many_test.rb +13 -0
- data/test/preloader_has_one_test.rb +69 -0
- data/test/record_marshal_test.rb +1 -1
- data/test/test_helper.rb +3 -4
- metadata +28 -28
@@ -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,27 +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
|
40
|
-
gem.add_development_dependency 'rubocop', "~> 0.52.0"
|
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"
|
41
41
|
end
|
@@ -21,7 +21,7 @@ module ActiveRecordTestCaseHelper
|
|
21
21
|
yield
|
22
22
|
|
23
23
|
stream_io.rewind
|
24
|
-
|
24
|
+
captured_stream.read
|
25
25
|
ensure
|
26
26
|
captured_stream.close
|
27
27
|
captured_stream.unlink
|
@@ -76,6 +76,16 @@ module ActiveRecordTestCaseHelper
|
|
76
76
|
model.column_names.include?(column_name.to_s)
|
77
77
|
end
|
78
78
|
|
79
|
+
def savepoint
|
80
|
+
if ActiveRecord::Base.connection.supports_savepoints?
|
81
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
82
|
+
yield
|
83
|
+
ActiveRecord::Base.connection.rollback_transaction
|
84
|
+
else
|
85
|
+
yield
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
79
89
|
class SQLCounter
|
80
90
|
class << self
|
81
91
|
attr_accessor :ignored_sql, :log, :log_all
|
@@ -6,14 +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_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}"
|
19
|
+
end
|
20
|
+
|
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
|
17
33
|
end
|
18
34
|
|
19
35
|
def test_should_query_from_db_using_primary_key
|
@@ -51,4 +67,31 @@ class FetchByUinqKeyTest < ActiveSupport::TestCase
|
|
51
67
|
user = User.fetch_by_uniq_key(@user.name, :name)
|
52
68
|
assert_equal user, @user
|
53
69
|
end
|
70
|
+
|
71
|
+
def test_should_return_correct_when_destroy_old_record_and_create_same_new_record
|
72
|
+
savepoint do
|
73
|
+
uniq_key = { email: "#{Time.now.to_i}@foobar.com" }
|
74
|
+
old_user = User.create(uniq_key)
|
75
|
+
new_user = old_user.deep_dup
|
76
|
+
assert_equal old_user, User.fetch_by_uniq_keys(uniq_key)
|
77
|
+
old_user.destroy
|
78
|
+
|
79
|
+
# Dirty id cache should be removed
|
80
|
+
assert_queries(2) { assert_nil User.fetch_by_uniq_keys(uniq_key) }
|
81
|
+
assert_queries(1) { assert_nil User.fetch_by_uniq_keys(uniq_key) }
|
82
|
+
|
83
|
+
new_user.save
|
84
|
+
assert_equal new_user, User.fetch_by_uniq_keys(uniq_key)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_should_return_correct_when_old_record_modify_uniq_key_and_new_record_use_same_uniq_key
|
89
|
+
savepoint do
|
90
|
+
uniq_key = { email: @user.email }
|
91
|
+
assert_equal @user, User.fetch_by_uniq_keys(uniq_key)
|
92
|
+
@user.update_attribute(:email, "#{Time.now.to_i}@foobar.com")
|
93
|
+
new_user = User.create(uniq_key)
|
94
|
+
assert_equal new_user, User.fetch_by_uniq_keys(uniq_key)
|
95
|
+
end
|
96
|
+
end
|
54
97
|
end
|
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,16 @@ 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
|
27
33
|
end
|
28
34
|
|
29
35
|
def test_has_one_with_conditions
|
@@ -49,4 +55,10 @@ class HasOneAssociationTest < ActiveSupport::TestCase
|
|
49
55
|
@account.update(user_id: @user.id + 1)
|
50
56
|
assert_nil @user.reload.account
|
51
57
|
end
|
58
|
+
|
59
|
+
def test_should_one_query_when_has_one_target_is_null
|
60
|
+
Namespace.destroy_all
|
61
|
+
@user.reload
|
62
|
+
assert_queries(1) { @user.namespace }
|
63
|
+
end
|
52
64
|
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
@@ -4,12 +4,17 @@ ActiveRecord::Base.connection.create_table(:books, force: true) do |t|
|
|
4
4
|
t.string :title
|
5
5
|
t.string :body
|
6
6
|
t.integer :user_id
|
7
|
+
t.decimal :discount_percentage, precision: 5, scale: 2
|
7
8
|
t.integer :images_count, default: 0
|
9
|
+
t.date :publish_date
|
10
|
+
t.boolean :normal, default: true, nil: false
|
8
11
|
end
|
9
12
|
|
10
|
-
class Book <
|
13
|
+
class Book < ApplicationRecord
|
11
14
|
second_level_cache
|
12
15
|
|
16
|
+
default_scope -> { where(normal: true) }
|
17
|
+
|
13
18
|
belongs_to :user, counter_cache: true
|
14
19
|
has_many :images, as: :imagable
|
15
20
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveRecord::Base.connection.create_table(:contributions, force: true) do |t|
|
4
|
+
t.integer :user_id
|
5
|
+
t.text :data
|
6
|
+
t.date :date
|
7
|
+
end
|
8
|
+
|
9
|
+
class Contribution < ApplicationRecord
|
10
|
+
second_level_cache
|
11
|
+
|
12
|
+
validates_uniqueness_of :user_id, scope: :date, if: -> { user_id_changed? || date_changed? }
|
13
|
+
belongs_to :user
|
14
|
+
end
|
data/test/model/image.rb
CHANGED