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 +5 -5
- data/README.md +252 -1
- data/active_model_cachers.gemspec +2 -2
- data/lib/active_model_cachers/active_record/attr_model.rb +96 -0
- data/lib/active_model_cachers/active_record/cacher.rb +61 -0
- data/lib/active_model_cachers/active_record/extension.rb +115 -0
- data/lib/active_model_cachers/active_record/patch_rails_3.rb +56 -0
- data/lib/active_model_cachers/cache_service.rb +55 -6
- data/lib/active_model_cachers/cache_service_factory.rb +26 -14
- data/lib/active_model_cachers/column_value_cache.rb +45 -0
- data/lib/active_model_cachers/config.rb +1 -0
- data/lib/active_model_cachers/false_object.rb +5 -0
- data/lib/active_model_cachers/hook/associations.rb +38 -0
- data/lib/active_model_cachers/hook/dependencies.rb +26 -0
- data/lib/active_model_cachers/hook/on_model_delete.rb +55 -0
- data/lib/active_model_cachers/nil_object.rb +5 -0
- data/lib/active_model_cachers/version.rb +2 -1
- data/lib/active_model_cachers.rb +3 -23
- metadata +18 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bb3fef2b82bccaa79965f493c0e8daa2e1f943fba503858bc950fb69fc81360a
|
4
|
+
data.tar.gz: 25ea760393d9412968a8473ab2fe876c82d2389d21154b22e9db5a58ffe0cc88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 314aee638ef81c03e9f6188ee5dd5b7ae022befd232e2e4e0d667bee310d2375dd81f679093b76b2e0416600a2cc3c045f035d734ca8b9dbff91dd4e20caebf2
|
7
|
+
data.tar.gz: 85df0799d1e2731e3518ce310a3e10fce24776eba0b6a7802fb76ef389994d9db75cfbc8166211f1bc08c5bce3e1854b905160aef43321e5bd22b467eccf3f51
|
data/README.md
CHANGED
@@ -1 +1,252 @@
|
|
1
|
-
#
|
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
|
-
|
47
|
+
key = self.class.cache_key
|
48
|
+
return @id ? "#{key}_#{@id}" : key
|
20
49
|
end
|
21
50
|
|
22
|
-
def get_without_cache
|
23
|
-
|
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.
|
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
|
8
|
-
|
10
|
+
def has_cacher?(attr)
|
11
|
+
return (@key_class_mapping[get_cache_key(attr)] != nil)
|
12
|
+
end
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
instance(id)
|
18
|
-
end
|
19
|
-
end
|
29
|
+
private
|
20
30
|
|
21
|
-
|
22
|
-
|
23
|
-
return
|
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
|
@@ -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)
|
data/lib/active_model_cachers.rb
CHANGED
@@ -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
|
-
|
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:
|
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-
|
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
|
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: []
|