ar_cache 1.1.0 → 2.0.0

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.
data/ar_cache.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.require_paths = ['lib']
29
29
 
30
30
  spec.add_runtime_dependency 'activerecord', '>= 6.1', '< 7'
31
+ spec.add_runtime_dependency 'oj', '>= 3', '< 4'
31
32
 
32
33
  # For more information and examples about making a new gem, checkout our
33
34
  # guide at: https://bundler.io/guides/creating_gem.html
data/lib/ar_cache.rb CHANGED
@@ -2,11 +2,10 @@
2
2
 
3
3
  require 'active_support/cache'
4
4
  require 'active_record'
5
+ require 'oj'
5
6
 
6
7
  require 'ar_cache/version'
7
8
  require 'ar_cache/configuration'
8
- require 'ar_cache/record'
9
- require 'ar_cache/store'
10
9
  require 'ar_cache/marshal'
11
10
  require 'ar_cache/table'
12
11
  require 'ar_cache/mock_table'
@@ -18,31 +17,59 @@ require 'ar_cache/active_record'
18
17
  require_relative './generators/ar_cache/install_generator' if defined?(Rails)
19
18
 
20
19
  module ArCache
21
- PRELOADER = ::ActiveRecord::Associations::Preloader.new
20
+ PLACEHOLDER = ''
21
+
22
+ @cache_reflection = {}
22
23
 
23
24
  class << self
24
- delegate :configure, to: Configuration
25
+ delegate :configure, :memcached?, :redis?, :cache_store, to: ArCache::Configuration
26
+ delegate :read, :read_multi, :write, :write_multi, :delete, :delete_multi, :exist?, :clear, to: :cache_store
25
27
 
26
28
  def skip_cache?
27
29
  Thread.current[:ar_cache_skip_cache]
28
30
  end
29
31
 
30
32
  def skip_cache
31
- Thread.current[:ar_cache_skip_cache] = true
32
- yield
33
- ensure
34
- Thread.current[:ar_cache_skip_cache] = false
33
+ return yield if skip_cache?
34
+
35
+ begin
36
+ Thread.current[:ar_cache_skip_cache] = true
37
+ yield
38
+ ensure
39
+ Thread.current[:ar_cache_skip_cache] = false
40
+ end
41
+ end
42
+
43
+ def skip_expire?
44
+ Thread.current[:ar_cache_skip_expire]
45
+ end
46
+
47
+ def skip_expire
48
+ return yield if skip_expire?
49
+
50
+ begin
51
+ Thread.current[:ar_cache_skip_expire] = true
52
+ yield
53
+ ensure
54
+ Thread.current[:ar_cache_skip_expire] = false
55
+ end
56
+ end
57
+
58
+ def cache_reflection?(reflection)
59
+ @cache_reflection.fetch(reflection) do
60
+ Thread.current[:ar_cache_reflection] = true
61
+ @cache_reflection[reflection] = yield
62
+ ensure
63
+ Thread.current[:ar_cache_reflection] = false
64
+ end
35
65
  end
36
66
 
37
- def pre_expire?
38
- Thread.current[:ar_cache_pre_expire]
67
+ def dump_attributes(attributes)
68
+ memcached? || redis? ? Oj.dump(attributes) : attributes
39
69
  end
40
70
 
41
- def pre_expire
42
- Thread.current[:ar_cache_pre_expire] = true
43
- yield
44
- ensure
45
- Thread.current[:ar_cache_pre_expire] = false
71
+ def load_attributes(attributes)
72
+ memcached? || redis? ? Oj.load(attributes) : attributes
46
73
  end
47
74
  end
48
75
  end
@@ -4,18 +4,21 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module Associations
6
6
  module HasOneThroughAssociation
7
+ PRELOADER = ::ActiveRecord::Associations::Preloader.new
8
+
7
9
  private def find_target
8
- return super if reflection.klass.ar_cache_table.disabled?
9
- return super if reflection.through_reflection.klass.ar_cache_table.disabled?
10
+ return super if ArCache.skip_cache?
11
+ return super unless ArCache.cache_reflection?(reflection) do
12
+ ArCache::Query.new(owner.association(through_reflection.name).scope).exec_queries_cacheable? &&
13
+ ArCache::Query.new(source_reflection.active_record.new.association(source_reflection.name).scope).exec_queries_cacheable? # rubocop:disable Layout/LineLength
14
+ end
10
15
 
11
16
  if (owner.strict_loading? || reflection.strict_loading?) && owner.validation_context.nil?
12
- Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
17
+ ::ActiveRecord::Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
13
18
  end
14
19
 
15
- ArCache::PRELOADER.preload(owner, reflection.name)
16
- owner.send(reflection.name)
17
- rescue StandardError
18
- super
20
+ PRELOADER.preload(owner, reflection.name)
21
+ target
19
22
  end
20
23
  end
21
24
  end
@@ -5,10 +5,8 @@ module ArCache
5
5
  module Associations
6
6
  module SingularAssociation
7
7
  private def skip_statement_cache?(...)
8
- # Polymorphic associations do not support computing the class, so can't judge ArCache status.
9
- # But SingularAssociation query usually can hit the unique index, so here return true directly.
10
- return true if is_a?(::ActiveRecord::Associations::BelongsToPolymorphicAssociation)
11
- return true if reflection.klass.ar_cache_table.enabled?
8
+ return super if ArCache.skip_cache?
9
+ return true if ArCache.cache_reflection?(reflection) { ArCache::Query.new(scope).exec_queries_cacheable? }
12
10
 
13
11
  super
14
12
  end
@@ -4,18 +4,23 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module ConnectionAdapters
6
6
  module DatabaseStatements
7
+ def select_all(arel, ...)
8
+ result = super
9
+ klass, select_values = arel.try(:klass_and_select_values)
10
+ return result if klass.nil?
11
+
12
+ klass.ar_cache_table.write(result.to_a)
13
+
14
+ if select_values
15
+ result.to_a.each { |r| r.slice!(*select_values) }
16
+ elsif klass.ignored_columns.any?
17
+ result.to_a.each { |r| r.except!(*klass.ignored_columns) }
18
+ end
19
+
20
+ result
21
+ end
22
+
7
23
  # def insert(arel, ...)
8
- # super.tap do
9
- # if arel.is_a?(String)
10
- # sql = arel.downcase
11
- # ArCache::Table.all.each do |table|
12
- # current_transaction.add_changed_table(table.name) if sql.include?(table.name)
13
- # end
14
- # else # is Arel::InsertManager
15
- # klass = arel.ast.relation.instance_variable_get(:@klass)
16
- # current_transaction.add_changed_table(klass.table_name)
17
- # end
18
- # end
19
24
  # end
20
25
  # alias create insert
21
26
 
@@ -46,24 +51,24 @@ module ArCache
46
51
  end
47
52
 
48
53
  private def update_ar_cache_by_arel(arel)
49
- return if ArCache.pre_expire?
54
+ return if ArCache.skip_expire?
50
55
 
51
56
  arel_table = arel.ast.relation.is_a?(Arel::Table) ? arel.ast.relation : arel.ast.relation.left
52
57
  klass = arel_table.instance_variable_get(:@klass)
53
- current_transaction.update_ar_cache_table(klass.ar_cache_table) if klass.ar_cache_table.enabled?
58
+ current_transaction.update_ar_cache_table(klass.ar_cache_table)
54
59
  end
55
60
 
56
61
  private def update_ar_cache_by_sql(sql)
57
62
  sql = sql.downcase
58
63
 
59
64
  ArCache::Table.all.each do |table|
60
- current_transaction.update_ar_cache_table(table) if table.enabled? && sql.include?(table.name)
65
+ current_transaction.update_ar_cache_table(table) if sql.include?(table.name)
61
66
  end
62
67
  end
63
68
 
64
69
  private def update_ar_cache_by_table(table_name)
65
70
  ArCache::Table.all.each do |table|
66
- current_transaction.update_ar_cache_table(table) if table.enabled? && table_name.casecmp?(table.name)
71
+ break current_transaction.update_ar_cache_table(table) if table_name == table.name
67
72
  end
68
73
  end
69
74
  end
@@ -4,75 +4,80 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module ConnectionAdapters
6
6
  module NullTransaction
7
- def delete_ar_cache_keys(keys, delay: false) # rubocop:disable Lint/UnusedMethodArgument
8
- ArCache::Store.delete_multi(keys)
7
+ def delete_ar_cache_primary_keys(keys, table)
8
+ handle_ar_cache_primary_keys(keys) unless table.disabled?
9
9
  end
10
10
 
11
- def update_ar_cache_table(table, delay: false) # rubocop:disable Lint/UnusedMethodArgument
12
- table.update_version
11
+ def update_ar_cache_table(table)
12
+ table.update_cache
13
13
  end
14
14
 
15
- def add_changed_table(...); end
15
+ def handle_ar_cache_primary_keys(keys)
16
+ if ArCache::Configuration.cache_lock?
17
+ keys.each { |k| ArCache.write(k, ArCache::PLACEHOLDER, raw: true, expires_in: 1.day) }
18
+ else
19
+ ArCache.delete_multi(keys)
20
+ end
21
+ end
16
22
  end
17
23
 
18
24
  module Transaction
19
25
  include NullTransaction
20
26
 
27
+ attr_reader :ar_cache_primary_keys, :ar_cache_tables
28
+
21
29
  def initialize(...)
22
30
  super
23
- @ar_cache_keys = []
31
+ @ar_cache_primary_keys = []
24
32
  @ar_cache_tables = []
25
33
  end
26
34
 
27
- def delete_ar_cache_keys(keys, delay: false)
28
- super if !delay && read_uncommitted?
29
- @ar_cache_keys.push(*keys)
30
- end
31
-
32
- def update_ar_cache_table(table, delay: false)
33
- add_changed_table(table.name) unless delay
35
+ def delete_ar_cache_primary_keys(keys, table)
36
+ connection.transaction_manager.add_transaction_table(table.name)
37
+ return if table.disabled?
34
38
 
35
- super if !delay && read_uncommitted?
36
- @ar_cache_tables.push(table)
39
+ super if read_uncommitted?
40
+ ar_cache_primary_keys.push(*keys)
37
41
  end
38
42
 
39
- def add_changed_table(table_name)
40
- connection.transaction_manager.add_changed_table(table_name)
43
+ def update_ar_cache_table(table)
44
+ connection.transaction_manager.add_transaction_table(table.name)
45
+ return if table.disabled?
46
+
47
+ super if read_uncommitted?
48
+ ar_cache_tables.push(table)
41
49
  end
42
50
 
43
- # FIXME: Cache update and transaction commit may cause dirty reads during this period!
51
+ # FIXME: The cache is removed after transaction commited, so dirty read may occur.
44
52
  def commit
45
53
  super
46
54
  ensure
47
55
  if @run_commit_callbacks
48
- @ar_cache_tables.uniq(&:name).each(&:update_version) if @ar_cache_tables.any?
49
- ArCache::Store.delete_multi(@ar_cache_keys.uniq) if @ar_cache_keys.any?
56
+ handle_ar_cache_primary_keys(ar_cache_primary_keys.uniq) if ar_cache_primary_keys.any?
57
+ ar_cache_tables.uniq(&:name).each(&:update_cache) if ar_cache_tables.any?
50
58
  else
51
- transaction = connection.current_transaction
52
- @ar_cache_tables.each { |table| transaction.update_ar_cache_table(table, delay: true) }
53
- transaction.delete_ar_cache_keys(@ar_cache_keys, delay: true)
59
+ connection.current_transaction.ar_cache_tables.push(*ar_cache_tables)
60
+ connection.current_transaction.ar_cache_primary_keys.push(*ar_cache_primary_keys)
54
61
  end
55
62
  end
56
63
 
57
64
  def read_uncommitted?
58
- ArCache::Configuration.read_uncommitted ||
59
- isolation_level == :read_uncommitted ||
60
- !connection.transaction_manager.fully_joinable?
65
+ isolation_level == :read_uncommitted || !connection.transaction_manager.fully_joinable?
61
66
  end
62
67
  end
63
68
 
64
69
  module TransactionManager
65
70
  def initialize(...)
66
71
  super
67
- @changed_tables = {}
72
+ @transaction_tables = {}
68
73
  end
69
74
 
70
- def add_changed_table(table_name)
71
- @changed_tables[table_name] = true if fully_joinable?
75
+ def add_transaction_table(table_name)
76
+ @transaction_tables[table_name] = true if fully_joinable?
72
77
  end
73
78
 
74
- def changed_table?(table_name)
75
- @changed_tables.key?(table_name)
79
+ def transaction_table?(table_name)
80
+ @transaction_tables.key?(table_name)
76
81
  end
77
82
 
78
83
  def fully_joinable?
@@ -82,7 +87,7 @@ module ArCache
82
87
  def within_new_transaction(...)
83
88
  super
84
89
  ensure
85
- @changed_tables = {} if @stack.count(&:joinable?).zero?
90
+ @transaction_tables.clear if @stack.count(&:joinable?).zero?
86
91
  end
87
92
  end
88
93
  end
@@ -4,16 +4,15 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module Core
6
6
  module ClassMethods
7
- delegate :skip_ar_cache, to: :all
7
+ # The #find and #find_by use ActiveRecord::StatementCache to execute querying first.
8
+ # For ArCache, we need force skip ActiveRecord::StatementCache.
8
9
 
9
- # The #find use statement cache execute querying first, so need force skip.
10
10
  def find(...)
11
- ar_cache_table.enabled? ? all.find(...) : super
11
+ ArCache.skip_cache? || ar_cache_table.disabled? ? super : all.find(...)
12
12
  end
13
13
 
14
- # The #find_by use statement cache execute querying first, so need force skip.
15
14
  def find_by(...)
16
- ar_cache_table.enabled? ? all.find_by(...) : super
15
+ ArCache.skip_cache? || ar_cache_table.disabled? ? super : all.find_by(...)
17
16
  end
18
17
  end
19
18
  end
@@ -5,10 +5,7 @@ module ArCache
5
5
  module InsertAll
6
6
  def execute
7
7
  super.tap do
8
- if on_duplicate == :update
9
- connection.current_transaction.update_ar_cache_table(model.ar_cache_table)
10
- connection.current_transaction.add_changed_table(model.table_name)
11
- end
8
+ connection.current_transaction.update_ar_cache_table(model.ar_cache_table) if on_duplicate == :update
12
9
  end
13
10
  end
14
11
  end
@@ -9,13 +9,7 @@ module ArCache
9
9
  end
10
10
 
11
11
  def ar_cache_table
12
- @ar_cache_table ||= begin
13
- if abstract_class? || self == ArCache::Record
14
- ArCache::MockTable
15
- else
16
- ArCache::Table.new(table_name)
17
- end
18
- end
12
+ @ar_cache_table ||= abstract_class? ? ArCache::MockTable : ArCache::Table.new(table_name)
19
13
  end
20
14
  end
21
15
 
@@ -5,14 +5,14 @@ module ArCache
5
5
  module Persistence
6
6
  module ClassMethods
7
7
  def _update_record(_, constraints)
8
- ArCache.pre_expire do
8
+ ArCache.skip_expire do
9
9
  delete_ar_cache_key(constraints[@primary_key])
10
10
  super
11
11
  end
12
12
  end
13
13
 
14
14
  def _delete_record(constraints)
15
- ArCache.pre_expire do
15
+ ArCache.skip_expire do
16
16
  delete_ar_cache_key(constraints[@primary_key])
17
17
  super
18
18
  end
@@ -20,8 +20,7 @@ module ArCache
20
20
 
21
21
  private def delete_ar_cache_key(id)
22
22
  key = ar_cache_table.primary_cache_key(id)
23
- connection.current_transaction.delete_ar_cache_keys([key])
24
- connection.current_transaction.add_changed_table(table_name)
23
+ connection.current_transaction.delete_ar_cache_primary_keys([key], ar_cache_table)
25
24
  end
26
25
  end
27
26
 
@@ -4,28 +4,22 @@ module ArCache
4
4
  module ActiveRecord
5
5
  module Relation
6
6
  def reload
7
- @skip_ar_cache = true if loaded?
8
- super
9
- end
10
-
11
- def skip_ar_cache
12
- tap { @skip_ar_cache = true }
7
+ loaded? ? ArCache.skip_cache { super } : super
13
8
  end
14
9
 
15
10
  def explain
16
- @skip_ar_cache = true
17
- super
11
+ ArCache.skip_cache { super }
18
12
  end
19
13
 
20
14
  def update_all(...)
21
- ArCache.pre_expire { delete_ar_cache_keys ? super : 0 }
15
+ ArCache.skip_expire { delete_ar_cache_primary_keys ? super : 0 }
22
16
  end
23
17
 
24
18
  def delete_all
25
- ArCache.pre_expire { delete_ar_cache_keys ? super : 0 }
19
+ ArCache.skip_expire { delete_ar_cache_primary_keys ? super : 0 }
26
20
  end
27
21
 
28
- private def delete_ar_cache_keys
22
+ private def delete_ar_cache_primary_keys
29
23
  return true if klass.ar_cache_table.disabled?
30
24
 
31
25
  where_clause = ArCache::WhereClause.new(klass, arel.constraints)
@@ -37,13 +31,12 @@ module ArCache
37
31
 
38
32
  return false if keys.empty?
39
33
 
40
- @klass.connection.current_transaction.delete_ar_cache_keys(keys)
41
- @klass.connection.current_transaction.add_changed_table(@klass.table_name)
34
+ @klass.connection.current_transaction.delete_ar_cache_primary_keys(keys, @klass.ar_cache_table)
42
35
  true
43
36
  end
44
37
 
45
38
  private def exec_queries(&block)
46
- @skip_ar_cache ? super : ArCache::Query.new(self).exec_queries(&block).freeze
39
+ ArCache.skip_cache? ? super : ArCache::Query.new(self).exec_queries(&block).freeze
47
40
  end
48
41
  end
49
42
  end