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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -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/persistence.rb +6 -2
  12. data/lib/second_level_cache/active_record/preloader.rb +34 -28
  13. data/lib/second_level_cache/adapter/paranoia.rb +24 -0
  14. data/lib/second_level_cache/config.rb +1 -0
  15. data/lib/second_level_cache/log_subscriber.rb +15 -0
  16. data/lib/second_level_cache/mixin.rb +0 -12
  17. data/lib/second_level_cache/record_marshal.rb +7 -40
  18. data/lib/second_level_cache/version.rb +1 -1
  19. data/second_level_cache.gemspec +23 -23
  20. data/test/active_record_test_case_helper.rb +11 -1
  21. data/test/fetch_by_uniq_key_test.rb +47 -4
  22. data/test/finder_methods_test.rb +39 -0
  23. data/test/has_one_association_test.rb +16 -4
  24. data/test/model/account.rb +2 -2
  25. data/test/model/animal.rb +1 -1
  26. data/test/model/application_record.rb +3 -0
  27. data/test/model/book.rb +6 -1
  28. data/test/model/contribution.rb +14 -0
  29. data/test/model/image.rb +1 -1
  30. data/test/model/order.rb +1 -1
  31. data/test/model/order_item.rb +1 -1
  32. data/test/model/paranoid.rb +10 -0
  33. data/test/model/post.rb +1 -1
  34. data/test/model/topic.rb +1 -1
  35. data/test/model/user.rb +7 -7
  36. data/test/paranoid_test.rb +16 -0
  37. data/test/{preloader_test.rb → preloader_belongs_to_test.rb} +17 -15
  38. data/test/preloader_has_many_test.rb +13 -0
  39. data/test/preloader_has_one_test.rb +69 -0
  40. data/test/record_marshal_test.rb +1 -1
  41. data/test/test_helper.rb +3 -4
  42. 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
@@ -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.0"
4
+ VERSION = "2.6.2"
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,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 = '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 'pry'
39
- gem.add_development_dependency 'database_cleaner'
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
- 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