second_level_cache 2.5.2 → 2.6.3
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 +24 -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/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 +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 +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 +3 -5
- metadata +31 -17
@@ -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
|
@@ -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
data/test/model/order.rb
CHANGED
data/test/model/order_item.rb
CHANGED