active_model_cachers 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7cf82e0fa5854ee62fee1f65d40b847f56776487
4
- data.tar.gz: 9be2536f67e4f748ee13ed00655403c3dbfc79cd
2
+ SHA256:
3
+ metadata.gz: bb3fef2b82bccaa79965f493c0e8daa2e1f943fba503858bc950fb69fc81360a
4
+ data.tar.gz: 25ea760393d9412968a8473ab2fe876c82d2389d21154b22e9db5a58ffe0cc88
5
5
  SHA512:
6
- metadata.gz: 0e6e221037b749a572c29784475ab1ec9f97531ac88b3f001c88a4fd04a2177aca0037a1365ff2368a3a4701895cd86b90fe62bf5cd12266e46fa309cd70b754
7
- data.tar.gz: 3e522c636f1e1e20af67fff9a20c842f22457ec7a8ba5d2c718a2dfc87616ceba00491412246abbaec66b00c6321b5cfb8bd83ba5755a8a65289f73ee2713a6a
6
+ metadata.gz: 314aee638ef81c03e9f6188ee5dd5b7ae022befd232e2e4e0d667bee310d2375dd81f679093b76b2e0416600a2cc3c045f035d734ca8b9dbff91dd4e20caebf2
7
+ data.tar.gz: 85df0799d1e2731e3518ce310a3e10fce24776eba0b6a7802fb76ef389994d9db75cfbc8166211f1bc08c5bce3e1854b905160aef43321e5bd22b467eccf3f51
data/README.md CHANGED
@@ -1 +1,252 @@
1
- # active_model_cachers
1
+ # ActiveModelCachers
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/active_model_cachers.svg?style=flat)](http://rubygems.org/gems/active_model_cachers)
4
+ [![Build Status](https://travis-ci.org/khiav223577/active_model_cachers.svg?branch=master)](https://travis-ci.org/khiav223577/active_model_cachers)
5
+ [![RubyGems](http://img.shields.io/gem/dt/active_model_cachers.svg?style=flat)](http://rubygems.org/gems/active_model_cachers)
6
+ [![Code Climate](https://codeclimate.com/github/khiav223577/active_model_cachers/badges/gpa.svg)](https://codeclimate.com/github/khiav223577/active_model_cachers)
7
+ [![Test Coverage](https://codeclimate.com/github/khiav223577/active_model_cachers/badges/coverage.svg)](https://codeclimate.com/github/khiav223577/active_model_cachers/coverage)
8
+
9
+ Provide cachers to the model so that you could specify which you want to cache. Data will be cached at `Rails.cache` and also at application level via `RequestStore` to cache values between requests. Cachers will maintain cached objects and expire them when they are changed (by create, update, destroy, and even delete).
10
+
11
+ - [Multi-level Cache](#multi-level-cache)
12
+ - Do not pollute original ActiveModel API.
13
+ - Support ActiveRecord 3, 4, 5.
14
+ - High test coverage
15
+
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'active_model_cachers'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install active_model_cachers
32
+
33
+ ### Specify where the cache will be stored
34
+
35
+ Add the following to your environment files (production.rb, development.rb, test.rb):
36
+ ```rb
37
+ ActiveModelCachers.config do |config|
38
+ config.store = Rails.cache
39
+ end
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ `cache_at(name, query = nil, options = {})`
45
+
46
+ Specifie a cache on the model.
47
+ - name: the attribute name.
48
+ - query: how to get data on cache miss. It will be set automatically if the name match an association or an attribute.
49
+ - options: see [here](#options)
50
+
51
+ ## Cache whatever you want
52
+
53
+ ### Example 1: Cache the number of active user
54
+
55
+ After specifying the name as `active_count` and how to get data when cache miss by lambda `User.active.count`.
56
+ You could access the cached data by calling `active_count` method on the cacher, `User.cacher`.
57
+
58
+ ```rb
59
+ class User < ActiveRecord::Base
60
+ scope :active, ->{ where('last_login_at > ?', 7.days.ago) }
61
+  cache_at :active_count, ->{ User.active.count }, expire_by: 'User#last_login_at'
62
+ end
63
+
64
+ @count = User.cacher.active_count
65
+ ```
66
+
67
+ You may want to flush cache on the number of active user changed. It can be done by simply setting [`expire_by`](#expire_by). In this case, `User#last_login_at` means flushing the cache when a user's `last_login_at` is changed (whenever by save, update, create, destroy or delete).
68
+
69
+ ### Example 2: Cache the number of user
70
+
71
+ In this example, the cache should be cleaned on user `destroyed`, or new user `created`, but not on user `updated`. You could specify the cleaning callback to only fire on certain events by [`on`](#on).
72
+
73
+ ```rb
74
+ class User < ActiveRecord::Base
75
+ cache_at :count, ->{ User.count }, expire_by: 'User', on: [:create, :destroy]
76
+ end
77
+
78
+ @count = User.cacher.count
79
+ ```
80
+
81
+ ### Example 3: Access the cacher from a model instance
82
+
83
+ You could use the cacher from instance scope, e.g. `user.cacher`, instead of `User.cacher`. The difference is that the `binding` of query lambda is changed. In this example, you could write the query as `posts.exists?` in that it's in instance scope, and the binding of the lambda is `user`, not `User`. So that it accesses `posts` method of `user`.
84
+
85
+ ```rb
86
+ class User < ActiveRecord::Base
87
+ has_many :posts
88
+ cache_at :has_post?, ->{ posts.exists? }, expire_by: :posts
89
+ end
90
+
91
+ do_something if current_user.cacher.has_post?
92
+ ```
93
+
94
+ In this example, the cache should be cleaned when the `posts` of the user changed. You could just set `expire_by` to the association: `:posts`, and then it will do all the works for you magically. (If you want know more details, it actually set [`expire_by`](#expire_by) to `Post#user_id` and [`foreign_key`](#foreign_key), which is needed for backtracing the user id from post, to `:user_id`)
95
+
96
+
97
+ ### Example 4: Pass an argument to the query lambda.
98
+
99
+ You could cache not only the query result of database but also the result of outer service. Becasue `email_valid?` doesn't match an association or an attribute, by default, the cache will not be cleaned by any changes.
100
+
101
+ ```rb
102
+ class User < ActiveRecord::Base
103
+ cache_at :email_valid?, ->(email){ ValidEmail2::Address.new(email).valid_mx? }
104
+ end
105
+
106
+ render_error if not User.cacher_at('pearl@example.com').email_valid?
107
+ ```
108
+
109
+ The query lambda can have one parameter, you could pass variable to it by using `cacher_at`. For example, `User.cacher_at(email)`.
110
+
111
+ ```rb
112
+ class User < ActiveRecord::Base
113
+ cache_at :email_valid?, ->(email){ ValidEmail2::Address.new(email).valid_mx? }, primary_key: :email
114
+ end
115
+
116
+ render_error if not current_user.cacher.email_valid?
117
+ ```
118
+
119
+ It can also be accessed from instance cacher. But you have to set [`primary_key`](#primary_key), which is needed to know which attribute should be passed to the parameter.
120
+
121
+ ### Example 5: Clean the cache manually
122
+
123
+ Sometimes it needs to maintain the cache manually. For example, after calling `update_all`, `delete_all` or `import` records without calling callbacks.
124
+
125
+ ```rb
126
+ class User < ActiveRecord::Base
127
+ has_one :profile
128
+ cache_at :profile
129
+ end
130
+
131
+ # clean the cache by name
132
+ current_user.cacher.clean(:profile)
133
+
134
+ # Or calling the clean_* method
135
+ current_user.cacher.clean_profile
136
+ ```
137
+
138
+ ## Smart Caching
139
+
140
+ ### Multi-level Cache
141
+ There is multi-level cache in order to make the speed of data access go faster.
142
+
143
+ 1. RequestStore
144
+ 2. Rails.cache
145
+ 3. Association Cache
146
+ 4. Database
147
+
148
+ `RequestStore` is used to make sure same object will not loaded from cache twice, since the data transfer between `Cache` and `Application` still consumes time.
149
+
150
+ `Association Cache` will be used to prevent preloaded objects being loaded again.
151
+
152
+ For example:
153
+ ```rb
154
+ user = User.includes(:posts).take
155
+ user.cacher.posts # => no query will be made even on cache miss.
156
+ ```
157
+
158
+ ## Convenient syntax sugar for caching ActiveRecord
159
+
160
+ ### Caching Associations
161
+ ```rb
162
+ class User < ActiveRecord::Base
163
+ has_one :profile
164
+ cache_at :profile
165
+ end
166
+
167
+ @profile = current_user.cacher.profile
168
+
169
+ # directly get profile without loading user.
170
+ @profile = User.cacher_at(profile_id).profile
171
+ ```
172
+
173
+ ### Caching Polymorphic Associations
174
+
175
+ TODO
176
+
177
+ ### Caching Self
178
+
179
+ Cache self by id.
180
+ ```rb
181
+ class User < ActiveRecord::Base
182
+ cache_self
183
+ end
184
+
185
+ @user = User.cacher_at(user_id).self
186
+ ```
187
+
188
+ Also support caching self by other columns.
189
+ ```rb
190
+ class User < ActiveRecord::Base
191
+ cache_self, by: :account
192
+ end
193
+
194
+ @user = User.cacher_at('khiav').self_by_account
195
+ ```
196
+
197
+ ### Caching Attributes
198
+
199
+ ```rb
200
+ class Profile < ActiveRecord::Base
201
+ cache_at :point
202
+ end
203
+
204
+ @point = Profile.cacher_at(profile_id).point
205
+ ```
206
+
207
+ ## Options
208
+
209
+ ### :expire_by
210
+
211
+ Monitor on the specific model. Clean the cached objects if target are changed.
212
+
213
+ - if empty, e.g. `nil` or `''`: Monitoring nothing.
214
+
215
+ - if string, e.g. `User`: Monitoring all attributes of `User`.
216
+
217
+ - if string with keyword `#`, e.g. `User#last_login_in_at`: Monitoring only the specific attribute.
218
+
219
+ - if symbol, e.g. `:posts`: Monitoring on the association. It will trying to do all the things for you, including monitoring all attributes of `Post` and set the `foreign_key`.
220
+
221
+ - Default value depends on the `name`. If is an association, monitoring the association klass. If is an attribute, monitoring current klass and the attrribute name. If others, monitoring nothing.
222
+
223
+ ### :on
224
+
225
+ Fire changes only by a certain action with the `on` option. Like the same option of [after_commit](https://apidock.com/rails/ActiveRecord/Transactions/ClassMethods/after_commit).
226
+
227
+ - if `:create`: Clean the cache only on new record is created, e.g. `Model.create`.
228
+
229
+ - if `:update`: Clean the cache only on the record is updated, e.g. `model.update`.
230
+
231
+ - if `:destroy`: Clean the cache only on the record id destroyed, e.g. `model.destroy`, `model.delete`.
232
+
233
+ - if `array`, e.g. `[:create, :update]`: Clean the cache by any of specified actions.
234
+
235
+ - Default value is `[:create, :update, :destroy]`
236
+
237
+ ### :foreign_key
238
+
239
+ This option is needed only for caching assoication and need not to set if [`expire_by`](#expire_by) is set to monitor association. Used for backtracing the cache key from cached objects. For examle, if `user` has_many `posts`, and cached the `posts` by user.id. When a post is changed, it needs to know which column to use (in this example, `user_id`) to clean the cache at user.
240
+
241
+ - Default value is `:id`
242
+
243
+ - Will be automatically determined if [`expire_by`](#expire_by) is symbol.
244
+
245
+ ### :primary_key
246
+
247
+ This option is needed to know which attribute should be passed to the parameter when you are using instance cacher. For example, if a query, named `email_valid?`, uses `user.email` as parameter, and you call it from instance: `user.cacher.email_valid?`. You need to tell it to pass `user.email` instead of `user.id` as the argument.
248
+
249
+ - Default value is `:id`
250
+
251
+
252
+
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["khiav reoy"]
10
10
  spec.email = ["mrtmrt15xn@yahoo.com.tw"]
11
11
 
12
- spec.summary = %q{}
13
- spec.description = %q{}
12
+ spec.summary = %q{Let you cache whatever you want with ease by providing cachers to active model. Support Rails 3, 4, 5.}
13
+ spec.description = %q{Let you cache whatever you want with ease by providing cachers to active model. Cachers will maintain cached objects and expire them when they are changed (by create, update, destroy, and even delete). Support Rails 3, 4, 5.}
14
14
  spec.homepage = "https://github.com/khiav223577/active_model_cachers"
15
15
  spec.license = "MIT"
16
16
 
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ module ActiveModelCachers
3
+ module ActiveRecord
4
+ class AttrModel
5
+ attr_reader :klass, :column, :reflect
6
+
7
+ def initialize(klass, column, primary_key: nil)
8
+ @klass = klass
9
+ @column = column
10
+ @primary_key = primary_key
11
+ @reflect = klass.reflect_on_association(column)
12
+ end
13
+
14
+ def association?
15
+ return (@reflect != nil)
16
+ end
17
+
18
+ def class_name
19
+ return if not association?
20
+ return @reflect.class_name
21
+ end
22
+
23
+ def join_table
24
+ return nil if @reflect == nil
25
+ options = @reflect.options
26
+ return options[:through] if options[:through]
27
+ return (options[:join_table] || @reflect.send(:derive_join_table)) if @reflect.macro == :has_and_belongs_to_many
28
+ return nil
29
+ end
30
+
31
+ def belongs_to?
32
+ return false if not association?
33
+ return @reflect.belongs_to?
34
+ end
35
+
36
+ def has_one?
37
+ return false if not association?
38
+ #return @reflect.has_one? # Rails 3 doesn't have this method
39
+ return false if @reflect.collection?
40
+ return false if @reflect.belongs_to?
41
+ return true
42
+ end
43
+
44
+ def primary_key
45
+ return @primary_key if @primary_key
46
+ return if not association?
47
+ return (@reflect.belongs_to? ? @reflect.klass : @reflect.active_record).primary_key
48
+ end
49
+
50
+ def foreign_key(reverse: false)
51
+ return if not association?
52
+ # key may be symbol if specify foreign_key in association options
53
+ return @reflect.chain.last.foreign_key.to_s if reverse and join_table
54
+ return (@reflect.belongs_to? == reverse ? primary_key : @reflect.foreign_key).to_s
55
+ end
56
+
57
+ def single_association?
58
+ return false if not association?
59
+ return !collection?
60
+ end
61
+
62
+ def collection?
63
+ return false if not association?
64
+ return @reflect.collection?
65
+ end
66
+
67
+ def query_model(binding, id)
68
+ return query_self(binding, id) if @column == nil
69
+ return query_association(binding, id) if association?
70
+ return query_attribute(binding, id)
71
+ end
72
+
73
+ private
74
+
75
+ def query_self(binding, id)
76
+ return binding if binding.is_a?(::ActiveRecord::Base)
77
+ return @klass.find_by(primary_key => id)
78
+ end
79
+
80
+ def query_attribute(binding, id)
81
+ return binding.send(@column) if binding.is_a?(::ActiveRecord::Base) and binding.has_attribute?(@column)
82
+ return @klass.where(id: id).limit(1).pluck(@column).first
83
+ end
84
+
85
+ def query_association(binding, id)
86
+ return binding.send(@column) if binding.is_a?(::ActiveRecord::Base)
87
+ id = @reflect.active_record.where(id: id).limit(1).pluck(foreign_key).first if foreign_key != 'id'
88
+ if @reflect.collection?
89
+ return id ? @reflect.klass.where(@reflect.foreign_key => id).to_a : []
90
+ else
91
+ return id ? @reflect.klass.find_by(primary_key => id) : nil
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ module ActiveModelCachers
3
+ module ActiveRecord
4
+ class Cacher
5
+ @defined_map = {}
6
+
7
+ class << self
8
+ def define_cacher_method(attr, primary_key, service_klasses)
9
+ method = attr.column || (primary_key == :id ? :self : :"self_by_#{primary_key}")
10
+ cacher_klass = get_cacher_klass(attr.klass)
11
+ cacher_klass.attributes << method
12
+ cacher_klass.send(:define_method, method){ exec_by(attr, primary_key, service_klasses, :get) }
13
+ cacher_klass.send(:define_method, "peek_#{method}"){ exec_by(attr, primary_key, service_klasses, :peek) }
14
+ cacher_klass.send(:define_method, "clean_#{method}"){ exec_by(attr, primary_key, service_klasses, :clean_cache) }
15
+ end
16
+
17
+ def get_cacher_klass(klass)
18
+ @defined_map[klass] ||= create_cacher_klass_at(klass)
19
+ end
20
+
21
+ private
22
+
23
+ def create_cacher_klass_at(target)
24
+ cacher_klass = Class.new(self)
25
+ cacher_klass.define_singleton_method(:attributes){ @attributes ||= [] }
26
+ cacher_klass.send(:define_method, 'peek'){|column| send("peek_#{column}") }
27
+ cacher_klass.send(:define_method, 'clean'){|column| send("clean_#{column}") }
28
+
29
+ target.define_singleton_method(:cacher_at){|id| cacher_klass.new(id: id) }
30
+ target.define_singleton_method(:cacher){ cacher_klass.new }
31
+ target.send(:define_method, :cacher){ cacher_klass.new(model: self) }
32
+ return cacher_klass
33
+ end
34
+ end
35
+
36
+ def initialize(id: nil, model: nil)
37
+ @id = id
38
+ @model = model
39
+ end
40
+
41
+ private
42
+
43
+ def exec_by(attr, primary_key, service_klasses, method)
44
+ bindings = [@model]
45
+ if @model and attr.association?
46
+ if attr.has_one?
47
+ data = @model.send(attr.column).try(primary_key)
48
+ else
49
+ bindings << @model.send(attr.column) if @model.is_a?(::ActiveRecord::Base)
50
+ end
51
+ end
52
+ data ||= (@model ? @model.send(primary_key) : nil) || @id
53
+ service_klasses.each_with_index do |service_klass, index|
54
+ data = service_klass.instance(data).send(method, binding: bindings[index])
55
+ return if data == nil
56
+ end
57
+ return data
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+ require 'active_model_cachers/column_value_cache'
3
+ require 'active_model_cachers/active_record/attr_model'
4
+ require 'active_model_cachers/active_record/cacher'
5
+ require 'active_model_cachers/hook/dependencies'
6
+ require 'active_model_cachers/hook/associations'
7
+ require 'active_model_cachers/hook/on_model_delete'
8
+
9
+ module ActiveModelCachers
10
+ module ActiveRecord
11
+ module Extension
12
+ def cache_self(by: :id)
13
+ cache_at(nil, expire_by: self.name, primary_key: by, foreign_key: by)
14
+ end
15
+
16
+ def cache_at(column, query = nil, expire_by: nil, on: nil, foreign_key: nil, primary_key: nil)
17
+ attr = AttrModel.new(self, column, primary_key: primary_key)
18
+ return cache_belongs_to(attr) if attr.belongs_to?
19
+
20
+ query ||= ->(id){ attr.query_model(self, id) }
21
+ service_klass = CacheServiceFactory.create_for_active_model(attr, query)
22
+ Cacher.define_cacher_method(attr, attr.primary_key || :id, [service_klass])
23
+
24
+ with_id = true if expire_by.is_a?(Symbol) or query.parameters.size == 1
25
+ expire_class, expire_column, foreign_key = get_expire_infos(attr, expire_by, foreign_key)
26
+ if expire_class
27
+ define_callback_for_cleaning_cache(expire_class, expire_column, foreign_key, on: on) do |id|
28
+ service_klass.clean_at(with_id ? id : nil)
29
+ end
30
+ end
31
+ return service_klass
32
+ end
33
+
34
+ def has_cacher?(column = nil)
35
+ attr = AttrModel.new(self, column)
36
+ return CacheServiceFactory.has_cacher?(attr)
37
+ end
38
+
39
+ private
40
+
41
+ def get_expire_infos(attr, expire_by, foreign_key)
42
+ if expire_by.is_a?(Symbol)
43
+ expire_attr = get_association_attr(expire_by)
44
+ expire_by = get_expire_by_from(expire_attr)
45
+ else
46
+ expire_attr = attr
47
+ expire_by ||= get_expire_by_from(expire_attr)
48
+ end
49
+ return if expire_by == nil
50
+
51
+ class_name, column = expire_by.split('#', 2)
52
+ foreign_key ||= expire_attr.foreign_key(reverse: true) || 'id'
53
+
54
+ return class_name, column, foreign_key.to_s
55
+ end
56
+
57
+ def get_association_attr(column)
58
+ attr = AttrModel.new(self, column)
59
+ raise "#{column} is not an association" if not attr.association?
60
+ return attr
61
+ end
62
+
63
+ def get_expire_by_from(attr)
64
+ return attr.class_name if attr.association?
65
+ return "#{self}##{attr.column}" if column_names.include?(attr.column.to_s)
66
+ end
67
+
68
+ def cache_belongs_to(attr)
69
+ service_klasses = [cache_at(attr.foreign_key)]
70
+ Cacher.define_cacher_method(attr, attr.primary_key, service_klasses)
71
+ ActiveSupport::Dependencies.onload(attr.class_name) do
72
+ service_klasses << cache_self
73
+ end
74
+ end
75
+
76
+ def get_column_value_from_id(id, column, model)
77
+ return id if column == 'id'
78
+ model ||= cacher_at(id).peek_self if has_cacher?
79
+ return model.send(column) if model
80
+ return where(id: id).limit(1).pluck(column).first
81
+ end
82
+
83
+ @@column_value_cache = ActiveModelCachers::ColumnValueCache.new
84
+ def define_callback_for_cleaning_cache(class_name, column, foreign_key, on: nil, &clean)
85
+ ActiveSupport::Dependencies.onload(class_name) do
86
+ clean_ids = []
87
+
88
+ prepend_before_delete do |id, model|
89
+ clean_ids << @@column_value_cache.add(self, class_name, id, foreign_key, model)
90
+ end
91
+
92
+ before_delete do |_, model|
93
+ clean_ids.each{|s| clean.call(s.call) }
94
+ clean_ids = []
95
+ end
96
+
97
+ after_delete do
98
+ @@column_value_cache.clean_cache()
99
+ end
100
+
101
+ on_nullify(column){|ids| ids.each{|s| clean.call(s) }}
102
+
103
+ after_commit ->{
104
+ changed = column ? previous_changes.key?(column) : previous_changes.present?
105
+ clean.call(send(foreign_key)) if changed || destroyed?
106
+ }, on: on
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4')
114
+ require 'active_model_cachers/active_record/patch_rails_3'
115
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module ActiveModelCachers
3
+ module ActiveRecord
4
+ module Extension
5
+ # define #find_by for Rails 3
6
+ def find_by(*args)
7
+ where(*args).order('').first
8
+ end
9
+
10
+ # after_commit in Rails 3 cannot specify multiple :on
11
+ # EX:
12
+ # after_commit ->{ ... }, on: [:create, :destroy]
13
+ #
14
+ # Should rewrite it as:
15
+ # after_commit ->{ ... }, on: :create
16
+ # after_commit ->{ ... }, on: :destroy
17
+
18
+ def after_commit(*args, &block) # mass-assign protected attributes `id` In Rails 3
19
+ if args.last.is_a?(Hash)
20
+ if (on = args.last[:on]).is_a?(Array)
21
+ return on.each{|s| after_commit(*[*args[0...-1], { **args[-1], on: s }], &block) }
22
+ end
23
+ end
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ module ActiveRecord
31
+ module Associations
32
+ # = Active Record Associations
33
+ #
34
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
35
+ #
36
+ # Association
37
+ # SingularAssociation
38
+ # HasOneAssociation
39
+ # HasOneThroughAssociation + ThroughAssociation
40
+ # BelongsToAssociation
41
+ # BelongsToPolymorphicAssociation
42
+ # CollectionAssociation
43
+ # HasAndBelongsToManyAssociation
44
+ # HasManyAssociation
45
+ # HasManyThroughAssociation + ThroughAssociation
46
+ class Association #:nodoc:
47
+ alias_method :scope, :scoped
48
+ end
49
+ end
50
+ end
51
+
52
+ class ActiveModelCachers::ColumnValueCache
53
+ def pluck_columns(object, relation, columns)
54
+ object.connection.select_all(relation.select(columns)).map(&:values)
55
+ end
56
+ end
@@ -1,30 +1,79 @@
1
+ # frozen_string_literal: true
2
+ require 'active_model_cachers/nil_object'
3
+ require 'active_model_cachers/false_object'
4
+
1
5
  module ActiveModelCachers
2
6
  class CacheService
7
+ class << self
8
+ attr_accessor :cache_key
9
+ attr_accessor :query
10
+
11
+ def instance(id)
12
+ hash = (RequestStore.store[self] ||= {})
13
+ return hash[id] ||= new(id)
14
+ end
15
+
16
+ def clean_at(id)
17
+ instance(id).clean_cache
18
+ end
19
+ end
20
+
21
+ # ----------------------------------------------------------------
22
+ # ● instance methods
23
+ # ----------------------------------------------------------------
3
24
  def initialize(id)
4
25
  @id = id
5
26
  end
6
27
 
7
- def get
28
+ def get(binding: nil)
29
+ @cached_data ||= fetch_from_cache(binding: binding)
30
+ return cache_to_raw_data(@cached_data)
31
+ end
32
+
33
+ def peek(binding: nil)
8
34
  @cached_data ||= get_from_cache
35
+ return cache_to_raw_data(@cached_data)
9
36
  end
10
37
 
11
- def clean_cache
38
+ def clean_cache(binding: nil)
12
39
  @cached_data = nil
13
40
  Rails.cache.delete(cache_key)
41
+ return nil
14
42
  end
15
43
 
16
44
  private
17
45
 
18
46
  def cache_key
19
- fail 'not implement'
47
+ key = self.class.cache_key
48
+ return @id ? "#{key}_#{@id}" : key
20
49
  end
21
50
 
22
- def get_without_cache
23
- fail 'not implement'
51
+ def get_without_cache(binding)
52
+ query = self.class.query
53
+ return binding ? binding.instance_exec(@id, &query) : query.call(@id) if @id and query.parameters.size == 1
54
+ return binding ? binding.instance_exec(&query) : query.call
55
+ end
56
+
57
+ def raw_to_cache_data(raw)
58
+ return NilObject if raw == nil
59
+ return FalseObject if raw == false
60
+ return raw
61
+ end
62
+
63
+ def cache_to_raw_data(cached_data)
64
+ return nil if cached_data == NilObject
65
+ return false if cached_data == FalseObject
66
+ return cached_data
24
67
  end
25
68
 
26
69
  def get_from_cache
27
- ActiveModelCachers.config.store.fetch(cache_key, expires_in: 30.minutes){ get_without_cache }
70
+ ActiveModelCachers.config.store.read(cache_key)
71
+ end
72
+
73
+ def fetch_from_cache(binding: nil)
74
+ ActiveModelCachers.config.store.fetch(cache_key, expires_in: 30.minutes) do
75
+ raw_to_cache_data(get_without_cache(binding))
76
+ end
28
77
  end
29
78
  end
30
79
  end
@@ -1,26 +1,38 @@
1
+ # frozen_string_literal: true
1
2
  require 'request_store'
2
3
  require 'active_model_cachers/cache_service'
3
4
 
4
5
  module ActiveModelCachers
5
6
  class CacheServiceFactory
7
+ @key_class_mapping = {}
8
+
6
9
  class << self
7
- def create(reflect, cache_key, &query)
8
- klass = Class.new(CacheService)
10
+ def has_cacher?(attr)
11
+ return (@key_class_mapping[get_cache_key(attr)] != nil)
12
+ end
9
13
 
10
- class << klass
11
- def instance(id)
12
- hash = (RequestStore.store[self] ||= {})
13
- return hash[id] ||= new(id)
14
- end
14
+ def create_for_active_model(attr, query)
15
+ cache_key = get_cache_key(attr)
16
+ service_klass = create(cache_key, query)
17
+ return service_klass
18
+ end
19
+
20
+ def create(cache_key, query)
21
+ @key_class_mapping[cache_key] ||= ->{
22
+ klass = Class.new(CacheService)
23
+ klass.cache_key = cache_key
24
+ klass.query = query
25
+ next klass
26
+ }[]
27
+ end
15
28
 
16
- def [](id)
17
- instance(id)
18
- end
19
- end
29
+ private
20
30
 
21
- klass.send(:define_method, :cache_key){ "#{cache_key}_#{@id}" }
22
- klass.send(:define_method, :get_without_cache){ query.call(@id) }
23
- return klass
31
+ def get_cache_key(attr)
32
+ class_name, column = (attr.single_association? ? [attr.class_name, nil] : [attr.klass, attr.column])
33
+ return "active_model_cachers_#{class_name}_at_#{column}" if column
34
+ return "active_model_cachers_#{class_name}_by_#{attr.primary_key}" if attr.primary_key and attr.primary_key.to_s != 'id'
35
+ return "active_model_cachers_#{class_name}"
24
36
  end
25
37
  end
26
38
  end
@@ -0,0 +1,45 @@
1
+ class ActiveModelCachers::ColumnValueCache
2
+ def initialize
3
+ @cache1 = Hash.new{|h, k| h[k] = {} }
4
+ @cache2 = Hash.new{|h, k| h[k] = {} }
5
+ end
6
+
7
+ def add(object, class_name, id, foreign_key, model)
8
+ value = (@cache1[class_name][[id, foreign_key]] ||= get_id_from(object, id, foreign_key, model))
9
+ return ->{ (value == :not_set ? query_value(object, class_name, id, foreign_key) : value)}
10
+ end
11
+
12
+ def query_value(object, class_name, id, foreign_key)
13
+ cache = @cache2[class_name]
14
+ if cache.empty?
15
+ no_data_keys = @cache1[class_name].select{|k, v| v == :not_set }.keys
16
+ ids = no_data_keys.map(&:first).uniq
17
+ columns = ['id', *no_data_keys.map(&:second)].uniq
18
+ pluck_columns(object, object.where(id: ids).limit(ids.size), columns).each do |columns_data|
19
+ model_id = columns_data.first
20
+ columns.each_with_index do |column, index|
21
+ cache[[model_id, column]] = columns_data[index]
22
+ end
23
+ end
24
+ end
25
+ return cache[[id, foreign_key]]
26
+ end
27
+
28
+ def clean_cache
29
+ @cache1.clear
30
+ @cache2.clear
31
+ end
32
+
33
+ private
34
+
35
+ def pluck_columns(_, relation, columns)
36
+ relation.pluck(*columns)
37
+ end
38
+
39
+ def get_id_from(object, id, column, model)
40
+ return id if column == 'id'
41
+ model ||= object.cacher_at(id).peek_self if object.has_cacher?
42
+ return model.send(column) if model
43
+ return :not_set
44
+ end
45
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveModelCachers
2
3
  class Config
3
4
  attr_accessor :store
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module ActiveModelCachers
3
+ class FalseObject
4
+ end
5
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require 'active_record'
3
+ require 'active_record/associations/has_many_association'
4
+ require 'active_model_cachers/hook/on_model_delete'
5
+
6
+ module ActiveModelCachers::Hook
7
+ module Associations
8
+ def delete_count(method, scope)
9
+ if method == :delete_all
10
+ # TODO:
11
+ else # nullify
12
+ call_hooks{ scope.pluck(:id) }
13
+ end
14
+ super
15
+ end
16
+
17
+ def delete_records(records, method)
18
+ case method
19
+ when :destroy
20
+ when :delete_all
21
+ # TODO:
22
+ else
23
+ call_hooks{ records.map(&:id) }
24
+ end
25
+ super
26
+ end
27
+
28
+ private
29
+
30
+ def call_hooks
31
+ return if (hooks = reflection.klass.nullify_hooks_at(reflection.foreign_key)).blank?
32
+ ids = yield
33
+ hooks.each{|s| s.call(ids) }
34
+ end
35
+ end
36
+ end
37
+
38
+ ActiveRecord::Associations::HasManyAssociation.send(:prepend, ActiveModelCachers::Hook::Associations)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/dependencies'
3
+
4
+ module ActiveModelCachers::Hook
5
+ module Depdenencies
6
+ def onload(const_name, &block)
7
+ const = const_name if not const_name.is_a?(String)
8
+ if const or Module.const_defined?(const_name)
9
+ (const || const_name.constantize).instance_exec(&block)
10
+ else
11
+ load_hooks[const_name].push(block)
12
+ end
13
+ end
14
+
15
+ def load_hooks
16
+ @load_hooks ||= Hash.new{|h, k| h[k] = [] }
17
+ end
18
+
19
+ def new_constants_in(*)
20
+ new_constants = super.each{|s| load_hooks[s].each{|hook| s.constantize.instance_exec(&hook) } }
21
+ return new_constants
22
+ end
23
+ end
24
+ end
25
+
26
+ ActiveSupport::Dependencies.send(:extend, ActiveModelCachers::Hook::Depdenencies)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ require 'active_record'
3
+
4
+ module ActiveModelCachers::Hook
5
+ module OnModelDelete
6
+ module InstanceMethods
7
+ def delete
8
+ self.class.delete(id, self) if persisted?
9
+ @destroyed = true
10
+ freeze
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def prepend_before_delete(&callback)
16
+ before_delete_hooks.unshift(callback)
17
+ end
18
+
19
+ def before_delete(&callback)
20
+ before_delete_hooks << callback
21
+ end
22
+
23
+ def after_delete(&callback)
24
+ after_delete_hooks << callback
25
+ end
26
+
27
+ def before_delete_hooks
28
+ @before_delete_hooks ||= []
29
+ end
30
+
31
+ def after_delete_hooks
32
+ @after_delete_hooks ||= []
33
+ end
34
+
35
+ def delete(id, model = nil)
36
+ before_delete_hooks.each{|s| s.call(id, model) }
37
+ result = super(id)
38
+ after_delete_hooks.each{|s| s.call(id, model) }
39
+ return result
40
+ end
41
+
42
+ def nullify_hooks_at(column)
43
+ @nullify_hooks ||= Hash.new{|h, k| h[k] = [] }
44
+ return @nullify_hooks[column]
45
+ end
46
+
47
+ def on_nullify(column, &callback)
48
+ nullify_hooks_at(column) << callback
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ ActiveRecord::Base.send(:include, ActiveModelCachers::Hook::OnModelDelete::InstanceMethods)
55
+ ActiveRecord::Base.send(:extend, ActiveModelCachers::Hook::OnModelDelete::ClassMethods)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module ActiveModelCachers
3
+ class NilObject
4
+ end
5
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ActiveModelCachers
2
- VERSION = "1.0.0"
3
+ VERSION = "2.0.0"
3
4
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_model_cachers/version'
2
3
  require 'active_model_cachers/config'
3
4
  require 'active_model_cachers/cache_service_factory'
4
5
  require 'active_record'
5
6
  require 'active_record/relation'
7
+ require 'active_model_cachers/active_record/extension'
6
8
 
7
9
  module ActiveModelCachers
8
10
  def self.config
@@ -12,26 +14,4 @@ module ActiveModelCachers
12
14
  end
13
15
  end
14
16
 
15
- class << ActiveRecord::Base
16
- def cache_at(column, query = nil)
17
- reflect = reflect_on_association(column)
18
- if reflect
19
- query ||= ->(id){ (reflect.belongs_to? ? reflect.active_record : reflect.klass).find_by(id: id) }
20
- else
21
- query ||= ->(id){ where(id: id).limit(1).pluck(column).first }
22
- end
23
-
24
- service_klass = ActiveModelCachers::CacheServiceFactory.create(reflect, "cacher_key_of_#{self}_at_#{column}", &query)
25
- after_commit ->{ service_klass.instance(id).clean_cache if previous_changes.key?(column) || destroyed? }
26
-
27
- define_singleton_method(:"#{column}_cachers") do
28
- service_klass
29
- end
30
- end
31
-
32
- if not method_defined?(:find_by) # define #find_by for Rails 3
33
- def find_by(*args)
34
- where(*args).order('').first
35
- end
36
- end
37
- end
17
+ ActiveRecord::Base.send(:extend, ActiveModelCachers::ActiveRecord::Extension)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_model_cachers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - khiav reoy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-04-28 00:00:00.000000000 Z
11
+ date: 2018-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,7 +94,9 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3'
97
- description: ''
97
+ description: Let you cache whatever you want with ease by providing cachers to active
98
+ model. Cachers will maintain cached objects and expire them when they are changed
99
+ (by create, update, destroy, and even delete). Support Rails 3, 4, 5.
98
100
  email:
99
101
  - mrtmrt15xn@yahoo.com.tw
100
102
  executables: []
@@ -115,9 +117,19 @@ files:
115
117
  - gemfiles/5.0.gemfile
116
118
  - gemfiles/5.1.gemfile
117
119
  - lib/active_model_cachers.rb
120
+ - lib/active_model_cachers/active_record/attr_model.rb
121
+ - lib/active_model_cachers/active_record/cacher.rb
122
+ - lib/active_model_cachers/active_record/extension.rb
123
+ - lib/active_model_cachers/active_record/patch_rails_3.rb
118
124
  - lib/active_model_cachers/cache_service.rb
119
125
  - lib/active_model_cachers/cache_service_factory.rb
126
+ - lib/active_model_cachers/column_value_cache.rb
120
127
  - lib/active_model_cachers/config.rb
128
+ - lib/active_model_cachers/false_object.rb
129
+ - lib/active_model_cachers/hook/associations.rb
130
+ - lib/active_model_cachers/hook/dependencies.rb
131
+ - lib/active_model_cachers/hook/on_model_delete.rb
132
+ - lib/active_model_cachers/nil_object.rb
121
133
  - lib/active_model_cachers/version.rb
122
134
  homepage: https://github.com/khiav223577/active_model_cachers
123
135
  licenses:
@@ -139,8 +151,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
151
  version: '0'
140
152
  requirements: []
141
153
  rubyforge_project:
142
- rubygems_version: 2.6.13
154
+ rubygems_version: 2.7.6
143
155
  signing_key:
144
156
  specification_version: 4
145
- summary: ''
157
+ summary: Let you cache whatever you want with ease by providing cachers to active
158
+ model. Support Rails 3, 4, 5.
146
159
  test_files: []