active_model_cachers 2.1.3 → 2.1.8

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +59 -0
  3. data/.gitignore +9 -9
  4. data/CHANGELOG.md +71 -50
  5. data/CODE_OF_CONDUCT.md +48 -48
  6. data/LICENSE.txt +21 -21
  7. data/README.md +399 -353
  8. data/Rakefile +10 -10
  9. data/active_model_cachers.gemspec +45 -38
  10. data/bin/console +14 -14
  11. data/bin/setup +8 -8
  12. data/gemfiles/3.2.gemfile +11 -11
  13. data/gemfiles/4.2.gemfile +11 -11
  14. data/gemfiles/5.0.gemfile +11 -11
  15. data/gemfiles/5.1.gemfile +11 -11
  16. data/gemfiles/5.2.gemfile +11 -11
  17. data/gemfiles/6.0.gemfile +11 -0
  18. data/lib/active_model_cachers.rb +11 -0
  19. data/lib/active_model_cachers/active_record/attr_model.rb +124 -103
  20. data/lib/active_model_cachers/active_record/cacher.rb +97 -96
  21. data/lib/active_model_cachers/active_record/extension.rb +119 -106
  22. data/lib/active_model_cachers/active_record/global_callbacks.rb +67 -59
  23. data/lib/active_model_cachers/cache_service.rb +151 -134
  24. data/lib/active_model_cachers/cache_service_factory.rb +55 -55
  25. data/lib/active_model_cachers/column_value_cache.rb +47 -46
  26. data/lib/active_model_cachers/config.rb +6 -6
  27. data/lib/active_model_cachers/false_object.rb +5 -5
  28. data/lib/active_model_cachers/hook/associations.rb +43 -43
  29. data/lib/active_model_cachers/hook/dependencies.rb +38 -38
  30. data/lib/active_model_cachers/hook/on_model_delete.rb +29 -29
  31. data/lib/active_model_cachers/nil_object.rb +5 -5
  32. data/lib/active_model_cachers/{active_record → patches}/patch_rails_3.rb +49 -49
  33. data/lib/active_model_cachers/patches/uninitialized_attribute.rb +9 -0
  34. data/lib/active_model_cachers/version.rb +4 -4
  35. metadata +31 -13
  36. data/.travis.yml +0 -26
@@ -1,134 +1,151 @@
1
- # frozen_string_literal: true
2
- require 'active_model_cachers/nil_object'
3
- require 'active_model_cachers/false_object'
4
- require 'active_model_cachers/column_value_cache'
5
-
6
- module ActiveModelCachers
7
- class CacheService
8
- class << self
9
- attr_accessor :cache_key
10
- attr_accessor :query
11
-
12
- def instance(id)
13
- hash = (RequestStore.store[self] ||= {})
14
- return hash[id] ||= new(id)
15
- end
16
-
17
- def clean_at(id)
18
- instance(id).clean_cache
19
- end
20
-
21
- @@column_value_cache = ActiveModelCachers::ColumnValueCache.new
22
- def define_callback_for_cleaning_cache(class_name, column, foreign_key, with_id, on: nil)
23
- return if @callbacks_defined
24
- @callbacks_defined = true
25
-
26
- clean = ->(id){ clean_at(with_id ? id : nil) }
27
- clean_ids = []
28
- fire_on = Array(on) if on
29
-
30
- ActiveRecord::Extension.global_callbacks.instance_exec do
31
- on_nullify(class_name) do |nullified_column, get_ids|
32
- get_ids.call.each{|s| clean.call(s) } if nullified_column == column
33
- end
34
-
35
- after_touch(class_name) do
36
- clean.call(send(foreign_key))
37
- end
38
-
39
- after_commit(class_name) do # TODO: on
40
- next if fire_on and not transaction_include_any_action?(fire_on)
41
- changed = column ? previous_changes.key?(column) : previous_changes.present?
42
- clean.call(send(foreign_key)) if changed || destroyed?
43
- end
44
-
45
- pre_before_delete(class_name) do |id, model|
46
- clean_ids << @@column_value_cache.add(self, class_name, id, foreign_key, model)
47
- end
48
-
49
- before_delete(class_name) do |_, model|
50
- clean_ids.each{|s| clean.call(s.call) }
51
- clean_ids = []
52
- end
53
-
54
- after_delete(class_name) do
55
- @@column_value_cache.clean_cache
56
- end
57
- end
58
- end
59
- end
60
-
61
- # ----------------------------------------------------------------
62
- # ● instance methods
63
- # ----------------------------------------------------------------
64
- def initialize(id)
65
- @id = id
66
- end
67
-
68
- def get(binding: nil)
69
- @cached_data ||= fetch_from_cache(binding: binding)
70
- return cache_to_raw_data(@cached_data)
71
- end
72
-
73
- def peek(binding: nil)
74
- @cached_data ||= get_from_cache
75
- return cache_to_raw_data(@cached_data)
76
- end
77
-
78
- def clean_cache(binding: nil)
79
- @cached_data = nil
80
- Rails.cache.delete(cache_key)
81
- return nil
82
- end
83
-
84
- private
85
-
86
- def cache_key
87
- key = self.class.cache_key
88
- return @id ? "#{key}_#{@id}" : key
89
- end
90
-
91
- def get_without_cache(binding)
92
- query = self.class.query
93
- return binding ? binding.instance_exec(@id, &query) : query.call(@id) if @id and query.parameters.size == 1
94
- return binding ? binding.instance_exec(&query) : query.call
95
- end
96
-
97
- def raw_to_cache_data(raw)
98
- return NilObject if raw == nil
99
- return FalseObject if raw == false
100
- clean_ar_cache(raw.is_a?(Array) ? raw : [raw])
101
- return raw_without_singleton_methods(raw)
102
- end
103
-
104
- def cache_to_raw_data(cached_data)
105
- return nil if cached_data == NilObject
106
- return false if cached_data == FalseObject
107
- return cached_data
108
- end
109
-
110
- def get_from_cache
111
- ActiveModelCachers.config.store.read(cache_key)
112
- end
113
-
114
- def fetch_from_cache(binding: nil)
115
- ActiveModelCachers.config.store.fetch(cache_key, expires_in: 30.minutes) do
116
- raw_to_cache_data(get_without_cache(binding))
117
- end
118
- end
119
-
120
- def clean_ar_cache(models)
121
- return if not models.first.is_a?(::ActiveRecord::Base)
122
- models.each_with_index do |model, index|
123
- model.send(:clear_aggregation_cache)
124
- model.send(:clear_association_cache)
125
- end
126
- end
127
-
128
- def raw_without_singleton_methods(raw)
129
- return raw if raw.singleton_methods.empty?
130
- return raw.class.find_by(id: raw.id) if raw.is_a?(::ActiveRecord::Base) # cannot marshal singleton, so load a new record instead.
131
- return raw # not sure what to do with other cases
132
- end
133
- end
134
- end
1
+ # frozen_string_literal: true
2
+ require 'active_model_cachers/nil_object'
3
+ require 'active_model_cachers/false_object'
4
+ require 'active_model_cachers/column_value_cache'
5
+
6
+ module ActiveModelCachers
7
+ class CacheService
8
+ class << self
9
+ attr_accessor :cache_key
10
+ attr_accessor :query_mapping
11
+
12
+ def instance(id)
13
+ hash = (RequestStore.store[self] ||= {})
14
+ return hash[id] ||= new(id)
15
+ end
16
+
17
+ def clean_at(id)
18
+ instance(id).clean_cache
19
+ end
20
+
21
+ @@column_value_cache = ActiveModelCachers::ColumnValueCache.new
22
+ def define_callback_for_cleaning_cache(class_name, column, foreign_key, with_id, on: nil)
23
+ return if @callbacks_defined
24
+ @callbacks_defined = true
25
+
26
+ clean = ->(id){ clean_at(with_id ? id : nil) }
27
+ clean_ids = []
28
+ fire_on = Array(on) if on
29
+
30
+ ActiveRecord::Extension.global_callbacks.instance_exec do
31
+ on_nullify(class_name) do |nullified_column, get_ids|
32
+ get_ids.call.each{|s| clean.call(s) } if nullified_column == column
33
+ end
34
+
35
+ after_touch1(class_name) do
36
+ clean.call(@@column_value_cache.add(self.class, class_name, id, foreign_key, self).call)
37
+ end
38
+
39
+ after_touch2(class_name) do
40
+ @@column_value_cache.clean_cache
41
+ end
42
+
43
+ after_commit1(class_name) do
44
+ next if fire_on and not transaction_include_any_action?(fire_on)
45
+ changed = column ? previous_changes.key?(column) : previous_changes.present?
46
+ if changed || destroyed?
47
+ clean.call(@@column_value_cache.add(self.class, class_name, id, foreign_key, self).call)
48
+ end
49
+ end
50
+
51
+ after_commit2(class_name) do
52
+ @@column_value_cache.clean_cache
53
+ end
54
+
55
+ before_delete1(class_name) do |id, model|
56
+ clean_ids << @@column_value_cache.add(self, class_name, id, foreign_key, model)
57
+ end
58
+
59
+ before_delete2(class_name) do |_, model|
60
+ clean_ids.each{|s| clean.call(s.call) }
61
+ clean_ids = []
62
+ end
63
+
64
+ after_delete(class_name) do
65
+ @@column_value_cache.clean_cache
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # ----------------------------------------------------------------
72
+ # ● instance methods
73
+ # ----------------------------------------------------------------
74
+ def initialize(id)
75
+ @id = id
76
+ end
77
+
78
+ def get(binding: nil, reflect: nil)
79
+ @cached_data ||= fetch_from_cache(binding: binding, reflect: reflect)
80
+ return cache_to_raw_data(@cached_data)
81
+ end
82
+
83
+ def peek(binding: nil, reflect: nil)
84
+ @cached_data ||= get_from_cache
85
+ return cache_to_raw_data(@cached_data)
86
+ end
87
+
88
+ def clean_cache(binding: nil, reflect: nil)
89
+ @cached_data = nil
90
+ Rails.cache.delete(cache_key)
91
+ return nil
92
+ end
93
+
94
+ private
95
+
96
+ def cache_key
97
+ key = self.class.cache_key
98
+ return @id ? "#{key}_#{@id}" : key
99
+ end
100
+
101
+ def get_query(binding, reflect)
102
+ self.class.query_mapping[reflect] || begin
103
+ puts "Warning: cannot find query. possible reflects: #{self.class.query_mapping.keys}, reflect: #{reflect}"
104
+ self.class.query_mapping.values.first
105
+ end
106
+ end
107
+
108
+ def get_without_cache(binding, attr)
109
+ query = get_query(binding, attr)
110
+ return binding ? binding.instance_exec(@id, &query) : query.call(@id) if @id and query.parameters.size == 1
111
+ return binding ? binding.instance_exec(&query) : query.call
112
+ end
113
+
114
+ def raw_to_cache_data(raw)
115
+ return NilObject if raw == nil
116
+ return FalseObject if raw == false
117
+ clean_ar_cache(raw.is_a?(Array) ? raw : [raw])
118
+ return raw_without_singleton_methods(raw)
119
+ end
120
+
121
+ def cache_to_raw_data(cached_data)
122
+ return nil if cached_data == NilObject
123
+ return false if cached_data == FalseObject
124
+ return cached_data
125
+ end
126
+
127
+ def get_from_cache
128
+ ActiveModelCachers.config.store.read(cache_key)
129
+ end
130
+
131
+ def fetch_from_cache(binding: nil, reflect: nil)
132
+ ActiveModelCachers.config.store.fetch(cache_key, expires_in: 30.minutes) do
133
+ raw_to_cache_data(get_without_cache(binding, reflect))
134
+ end
135
+ end
136
+
137
+ def clean_ar_cache(models)
138
+ return if not models.first.is_a?(::ActiveRecord::Base)
139
+ models.each do |model|
140
+ model.send(:clear_aggregation_cache) if model.respond_to?(:clear_aggregation_cache, true)
141
+ model.send(:clear_association_cache)
142
+ end
143
+ end
144
+
145
+ def raw_without_singleton_methods(raw)
146
+ return raw if raw.singleton_methods.empty?
147
+ return raw.class.find_by(id: raw.id) if raw.is_a?(::ActiveRecord::Base) # cannot marshal singleton, so load a new record instead.
148
+ return raw # not sure what to do with other cases
149
+ end
150
+ end
151
+ end
@@ -1,55 +1,55 @@
1
- # frozen_string_literal: true
2
- require 'request_store'
3
- require 'active_model_cachers/cache_service'
4
-
5
- module ActiveModelCachers
6
- class CacheServiceFactory
7
- @key_class_mapping = {}
8
- @cache_key_klass_mapping = {}
9
-
10
- class << self
11
- def has_cacher?(attr)
12
- return (@key_class_mapping[get_cache_key(attr)] != nil)
13
- end
14
-
15
- def create_for_active_model(attr, query)
16
- create(get_cache_key(attr), query)
17
- end
18
-
19
- def create(cache_key, query)
20
- @key_class_mapping[cache_key] ||= ->{
21
- klass = Class.new(CacheService)
22
- klass.cache_key = cache_key
23
- klass.query = query
24
- klass.instance_variable_set(:@callbacks_defined, false) # to remove warning: instance variable @callbacks_defined not initialized
25
- next klass
26
- }[]
27
- end
28
-
29
- def set_klass_to_mapping(attr, current_klass)
30
- cache_key = get_cache_key(attr)
31
- reflect = attr.klass.reflect_on_association(:posts)
32
- changed = clean_klass_cache_if_reloaded!(cache_key, current_klass, attr)
33
- @cache_key_klass_mapping[cache_key] = current_klass
34
- return changed
35
- end
36
-
37
- private
38
-
39
- def get_cache_key(attr)
40
- class_name, column = attr.extract_class_and_column
41
- return "active_model_cachers_#{class_name}_at_#{column}" if column
42
- foreign_key = attr.foreign_key(reverse: true)
43
- return "active_model_cachers_#{class_name}_by_#{foreign_key}" if foreign_key and foreign_key.to_s != 'id'
44
- return "active_model_cachers_#{class_name}"
45
- end
46
-
47
- def clean_klass_cache_if_reloaded!(cache_key, current_klass, attr)
48
- origin_klass, @cache_key_klass_mapping[cache_key] = @cache_key_klass_mapping[cache_key], current_klass
49
- return false if origin_klass == nil or origin_klass == current_klass # when code reloaded in development.
50
- @key_class_mapping[cache_key] = nil
51
- return true
52
- end
53
- end
54
- end
55
- end
1
+ # frozen_string_literal: true
2
+ require 'request_store'
3
+ require 'active_model_cachers/cache_service'
4
+
5
+ module ActiveModelCachers
6
+ class CacheServiceFactory
7
+ @key_class_mapping = {}
8
+ @cache_key_klass_mapping = {}
9
+
10
+ class << self
11
+ def has_cacher?(attr)
12
+ return (@key_class_mapping[get_cache_key(attr)] != nil)
13
+ end
14
+
15
+ def create_for_active_model(attr, query)
16
+ cache_key = get_cache_key(attr)
17
+
18
+ klass = @key_class_mapping[cache_key] ||= ->{
19
+ klass = Class.new(CacheService)
20
+ klass.cache_key = cache_key
21
+ klass.query_mapping = {}
22
+ klass.instance_variable_set(:@callbacks_defined, false) # to remove warning: instance variable @callbacks_defined not initialized
23
+ next klass
24
+ }[]
25
+
26
+ klass.query_mapping[attr.reflect] = query
27
+ return klass
28
+ end
29
+
30
+ def set_klass_to_mapping(attr, current_klass)
31
+ cache_key = get_cache_key(attr)
32
+ changed = clean_klass_cache_if_reloaded!(cache_key, current_klass, attr)
33
+ @cache_key_klass_mapping[cache_key] = current_klass
34
+ return changed
35
+ end
36
+
37
+ private
38
+
39
+ def get_cache_key(attr)
40
+ class_name, column = attr.extract_class_and_column
41
+ return "active_model_cachers_#{class_name}_at_#{column}" if column
42
+ foreign_key = attr.foreign_key(reverse: true)
43
+ return "active_model_cachers_#{class_name}_by_#{foreign_key}" if foreign_key and foreign_key.to_s != 'id'
44
+ return "active_model_cachers_#{class_name}"
45
+ end
46
+
47
+ def clean_klass_cache_if_reloaded!(cache_key, current_klass, attr)
48
+ origin_klass, @cache_key_klass_mapping[cache_key] = @cache_key_klass_mapping[cache_key], current_klass
49
+ return false if origin_klass == nil or origin_klass == current_klass # when code reloaded in development.
50
+ @key_class_mapping[cache_key] = nil
51
+ return true
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,46 +1,47 @@
1
- # frozen_string_literal: true
2
- class ActiveModelCachers::ColumnValueCache
3
- def initialize
4
- @cache1 = Hash.new{|h, k| h[k] = {} }
5
- @cache2 = Hash.new{|h, k| h[k] = {} }
6
- end
7
-
8
- def add(object, class_name, id, foreign_key, model)
9
- value = (@cache1[class_name][[id, foreign_key]] ||= get_id_from(object, id, foreign_key, model))
10
- return ->{ (value == :not_set ? query_value(object, class_name, id, foreign_key) : value)}
11
- end
12
-
13
- def query_value(object, class_name, id, foreign_key)
14
- cache = @cache2[class_name]
15
- if cache.empty?
16
- no_data_keys = @cache1[class_name].select{|k, v| v == :not_set }.keys
17
- ids = no_data_keys.map(&:first).uniq
18
- columns = ['id', *no_data_keys.map(&:second)].uniq
19
- pluck_columns(object, object.where(id: ids).limit(ids.size), columns).each do |columns_data|
20
- model_id = columns_data.first
21
- columns.each_with_index do |column, index|
22
- cache[[model_id, column]] = columns_data[index]
23
- end
24
- end
25
- end
26
- return cache[[id, foreign_key]]
27
- end
28
-
29
- def clean_cache
30
- @cache1.clear
31
- @cache2.clear
32
- end
33
-
34
- private
35
-
36
- def pluck_columns(_, relation, columns)
37
- relation.pluck(*columns)
38
- end
39
-
40
- def get_id_from(object, id, column, model)
41
- return id if column == 'id'
42
- model ||= object.cacher.peek_by(id: id) if object.has_cacher?
43
- return model.send(column) if model
44
- return :not_set
45
- end
46
- end
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveModelCachers::ColumnValueCache
4
+ def initialize
5
+ @cache1 = Hash.new{|h, k| h[k] = {} }
6
+ @cache2 = Hash.new{|h, k| h[k] = {} }
7
+ end
8
+
9
+ def add(object, class_name, id, foreign_key, model)
10
+ value = (@cache1[class_name][[id, foreign_key]] ||= get_id_from(object, id, foreign_key, model))
11
+ return ->{ (value == :not_set ? query_value(object, class_name, id, foreign_key) : value)}
12
+ end
13
+
14
+ def query_value(object, class_name, id, foreign_key)
15
+ cache = @cache2[class_name]
16
+ if cache.empty?
17
+ no_data_keys = @cache1[class_name].select{|k, v| v == :not_set }.keys
18
+ ids = no_data_keys.map(&:first).uniq
19
+ columns = ['id', *no_data_keys.map(&:second)].uniq
20
+ pluck_columns(object, object.where(id: ids).limit(ids.size), columns).each do |columns_data|
21
+ model_id = columns_data.first
22
+ columns.each_with_index do |column, index|
23
+ cache[[model_id, column]] = columns_data[index]
24
+ end
25
+ end
26
+ end
27
+ return cache[[id, foreign_key]]
28
+ end
29
+
30
+ def clean_cache
31
+ @cache1.clear
32
+ @cache2.clear
33
+ end
34
+
35
+ private
36
+
37
+ def pluck_columns(_, relation, columns)
38
+ relation.pluck(*columns)
39
+ end
40
+
41
+ def get_id_from(object, id, column, model)
42
+ return id if column == 'id'
43
+ model ||= object.cacher.peek_by(id: id) if object.has_cacher?
44
+ return model.send(column) if model and model.has_attribute?(column)
45
+ return :not_set
46
+ end
47
+ end