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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +1 -1
  4. data/lib/second_level_cache.rb +1 -0
  5. data/lib/second_level_cache/active_record.rb +8 -2
  6. data/lib/second_level_cache/active_record/base.rb +1 -5
  7. data/lib/second_level_cache/active_record/belongs_to_association.rb +6 -4
  8. data/lib/second_level_cache/active_record/fetch_by_uniq_key.rb +24 -17
  9. data/lib/second_level_cache/active_record/finder_methods.rb +44 -42
  10. data/lib/second_level_cache/active_record/has_one_association.rb +14 -20
  11. data/lib/second_level_cache/active_record/preloader.rb +34 -28
  12. data/lib/second_level_cache/adapter/paranoia.rb +24 -0
  13. data/lib/second_level_cache/config.rb +1 -0
  14. data/lib/second_level_cache/log_subscriber.rb +15 -0
  15. data/lib/second_level_cache/mixin.rb +0 -12
  16. data/lib/second_level_cache/record_marshal.rb +7 -40
  17. data/lib/second_level_cache/version.rb +1 -1
  18. data/second_level_cache.gemspec +23 -22
  19. data/test/active_record_test_case_helper.rb +11 -1
  20. data/test/fetch_by_uniq_key_test.rb +47 -4
  21. data/test/finder_methods_test.rb +39 -0
  22. data/test/has_one_association_test.rb +16 -4
  23. data/test/model/account.rb +2 -2
  24. data/test/model/animal.rb +1 -1
  25. data/test/model/application_record.rb +3 -0
  26. data/test/model/book.rb +6 -1
  27. data/test/model/contribution.rb +14 -0
  28. data/test/model/image.rb +1 -1
  29. data/test/model/order.rb +1 -1
  30. data/test/model/order_item.rb +1 -1
  31. data/test/model/paranoid.rb +10 -0
  32. data/test/model/post.rb +1 -1
  33. data/test/model/topic.rb +1 -1
  34. data/test/model/user.rb +7 -7
  35. data/test/paranoid_test.rb +18 -0
  36. data/test/{preloader_test.rb → preloader_belongs_to_test.rb} +17 -15
  37. data/test/preloader_has_many_test.rb +13 -0
  38. data/test/preloader_has_one_test.rb +69 -0
  39. data/test/record_marshal_test.rb +1 -1
  40. data/test/test_helper.rb +3 -5
  41. metadata +31 -17
@@ -11,6 +11,7 @@ module SecondLevelCache
11
11
  end
12
12
 
13
13
  def logger
14
+ ActiveSupport::Deprecation.warn("logger is deprecated and will be removed from SecondLevelCache 2.7.0")
14
15
  @logger ||= Rails.logger if defined?(Rails)
15
16
  @logger ||= Logger.new(STDOUT)
16
17
  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 instace with only attributes.
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.attributes]
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
- klass.instantiate(attributes)
14
+ serialized[0].constantize.instantiate(serialized[1], &block)
49
15
  end
50
16
 
51
- def load_multi(serializeds)
52
- serializeds.map { |serialized| load(serialized) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SecondLevelCache
4
- VERSION = "2.5.2"
4
+ VERSION = "2.6.3"
5
5
  end
@@ -1,12 +1,13 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/second_level_cache/version', __FILE__)
3
- lib = File.expand_path('../lib', __FILE__)
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 = ['Hooopo']
8
- gem.email = ['hoooopo@gmail.com']
9
- gem.description = 'Write Through and Read Through caching library inspired by CacheMoney and cache_fu, support ActiveRecord 4.'
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 = 'https://github.com/hooopo/second_level_cache'
19
+ gem.homepage = "https://github.com/hooopo/second_level_cache"
19
20
 
20
- gem.files = Dir.glob('lib/**/*.rb') + [
21
- 'README.md',
22
- 'Rakefile',
23
- 'Gemfile',
24
- 'CHANGELOG.md',
25
- 'second_level_cache.gemspec'
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('test/**/*.rb')
28
+ gem.test_files = Dir.glob("test/**/*.rb")
28
29
  gem.executables = gem.files.grep(%r{^bin/})
29
- gem.name = 'second_level_cache'
30
- gem.require_paths = ['lib']
30
+ gem.name = "second_level_cache"
31
+ gem.require_paths = ["lib"]
31
32
  gem.version = SecondLevelCache::VERSION
32
33
 
33
- gem.add_runtime_dependency 'activesupport', ['>= 5.2', '< 7']
34
- gem.add_runtime_dependency 'activerecord', ['>= 5.2', '< 7']
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 'sqlite3', '> 1.4'
37
- gem.add_development_dependency 'rake'
38
- gem.add_development_dependency 'database_cleaner'
39
- 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"
40
41
  end
@@ -21,7 +21,7 @@ module ActiveRecordTestCaseHelper
21
21
  yield
22
22
 
23
23
  stream_io.rewind
24
- return captured_stream.read
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
@@ -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
- # clean_user = user.reload
24
- # assert_no_queries do
25
- # clean_user.forked_from_user
26
- # end
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
@@ -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 < ActiveRecord::Base
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
@@ -6,7 +6,7 @@ ActiveRecord::Base.connection.create_table(:animals, force: true) do |t|
6
6
  t.timestamps null: false
7
7
  end
8
8
 
9
- class Animal < ActiveRecord::Base
9
+ class Animal < ApplicationRecord
10
10
  second_level_cache
11
11
  end
12
12
 
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -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 < ActiveRecord::Base
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
@@ -6,7 +6,7 @@ ActiveRecord::Base.connection.create_table(:images, force: true) do |t|
6
6
  t.integer :imagable_id
7
7
  end
8
8
 
9
- class Image < ActiveRecord::Base
9
+ class Image < ApplicationRecord
10
10
  second_level_cache
11
11
 
12
12
  belongs_to :imagable, polymorphic: true, counter_cache: true
@@ -7,7 +7,7 @@ ActiveRecord::Base.connection.create_table(:orders, force: true, id: :uuid) do |
7
7
  t.timestamps null: false
8
8
  end
9
9
 
10
- class Order < ActiveRecord::Base
10
+ class Order < ApplicationRecord
11
11
  second_level_cache
12
12
 
13
13
  has_many :order_items
@@ -6,7 +6,7 @@ ActiveRecord::Base.connection.create_table(:order_items, force: true, id: :uuid)
6
6
  t.string :order_id
7
7
  end
8
8
 
9
- class OrderItem < ActiveRecord::Base
9
+ class OrderItem < ApplicationRecord
10
10
  second_level_cache
11
11
  belongs_to :order, touch: true
12
12
  end
@@ -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