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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/README.md +4 -4
  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 +14 -27
  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 +15 -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 +1 -1
  20. data/test/fetch_by_uniq_key_test.rb +18 -14
  21. data/test/finder_methods_test.rb +39 -0
  22. data/test/has_one_association_test.rb +20 -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 +4 -1
  27. data/test/model/contribution.rb +1 -1
  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 +11 -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 +2 -5
  41. 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
@@ -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.3"
4
+ VERSION = "2.6.4"
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
@@ -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 test_compare_record_attributes_with_where_values
21
- book = Book.new(title: "foobar")
22
- assert Book.send(:compare_record_attributes_with_where_values, book, title: :foobar)
23
- book.discount_percentage = 60.00
24
- assert Book.send(:compare_record_attributes_with_where_values, book, discount_percentage: "60")
25
- book.publish_date = Time.current.to_date
26
- assert Book.send(:compare_record_attributes_with_where_values, book, publish_date: Time.current.to_date.to_s)
27
- book.title = nil
28
- assert Book.send(:compare_record_attributes_with_where_values, book, title: nil)
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
@@ -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
- # 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
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
@@ -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
@@ -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 < ActiveRecord::Base
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
@@ -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 < ActiveRecord::Base
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? }
@@ -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
@@ -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
- class Post < ActiveRecord::Base
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