mongoid-cached-json 1.5.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/danger-comment.yml +11 -0
- data/.github/workflows/danger.yml +13 -0
- data/.github/workflows/test.yml +67 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +258 -28
- data/CHANGELOG.md +64 -57
- data/CONTRIBUTING.md +6 -4
- data/Dangerfile +5 -0
- data/Gemfile +14 -14
- data/README.md +96 -12
- data/Rakefile +1 -1
- data/UPGRADING.md +59 -0
- data/lib/mongoid-cached-json/cached_json.rb +68 -17
- data/lib/mongoid-cached-json/config.rb +0 -1
- data/lib/mongoid-cached-json/version.rb +1 -1
- data/mongoid-cached-json.gemspec +2 -2
- data/spec/all_fields_spec.rb +47 -0
- data/spec/cache_version_spec.rb +58 -0
- data/spec/cached_json_spec.rb +26 -17
- data/spec/dalli_spec.rb +3 -4
- data/spec/field_cache_spec.rb +73 -0
- data/spec/model_cache_spec.rb +64 -0
- data/spec/mongoid_criteria_spec.rb +2 -1
- data/spec/spec_helper.rb +22 -0
- data/spec/support/fast_json_image.rb +5 -1
- data/spec/support/json_employee.rb +1 -0
- data/spec/support/json_manager.rb +12 -2
- data/spec/support/tool.rb +6 -1
- metadata +17 -11
- data/.travis.yml +0 -17
data/CONTRIBUTING.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Contributing to Mongoid::CachedJson
|
|
2
2
|
===================================
|
|
3
3
|
|
|
4
|
-
Mongoid::CachedJson is work of [many of contributors](https://github.com/
|
|
4
|
+
Mongoid::CachedJson is work of [many of contributors](https://github.com/mongoid/mongoid-cached-json/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/mongoid/mongoid-cached-json/pulls), [propose features and discuss issues](https://github.com/mongoid/mongoid-cached-json/issues). When in doubt, ask a question in the [Google Group](http://groups.google.com/group/mongoid-cached-json).
|
|
5
5
|
|
|
6
6
|
#### Fork the Project
|
|
7
7
|
|
|
8
|
-
Fork the [project on Github](https://github.com/
|
|
8
|
+
Fork the [project on Github](https://github.com/mongoid/mongoid-cached-json) and check out your copy.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
git clone https://github.com/contributor/mongoid-cached-json.git
|
|
12
12
|
cd mongoid-cached-json
|
|
13
|
-
git remote add upstream https://github.com/
|
|
13
|
+
git remote add upstream https://github.com/mongoid/mongoid-cached-json.git
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
#### Create a Topic Branch
|
|
@@ -63,6 +63,8 @@ Document any external behavior in the [README](README.md).
|
|
|
63
63
|
|
|
64
64
|
Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account.
|
|
65
65
|
|
|
66
|
+
Danger checks pull requests with `danger-changelog` and reports missing changelog entries in a PR comment. Include the pull request number in your entry once it is available.
|
|
67
|
+
|
|
66
68
|
#### Commit Changes
|
|
67
69
|
|
|
68
70
|
Make sure git knows your name and email address:
|
|
@@ -104,7 +106,7 @@ git push origin my-feature-branch -f
|
|
|
104
106
|
Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows.
|
|
105
107
|
|
|
106
108
|
```
|
|
107
|
-
* [#123](https://github.com/
|
|
109
|
+
* [#123](https://github.com/mongoid/mongoid-cached-json/pull/123): Reticulated splines - [@contributor](https://github.com/contributor).
|
|
108
110
|
```
|
|
109
111
|
|
|
110
112
|
Amend your previous commit and force push the changes.
|
data/Dangerfile
ADDED
data/Gemfile
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
source '
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
3
|
gemspec
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
gem 'mongoid', '~> 4.0'
|
|
10
|
-
when /3/
|
|
11
|
-
gem 'mongoid', '~> 3.1'
|
|
12
|
-
else
|
|
13
|
-
gem 'mongoid', version
|
|
14
|
-
end
|
|
5
|
+
version = ENV['MONGOID_VERSION'] || '9'
|
|
6
|
+
gem 'bigdecimal' if RUBY_VERSION >= '3.4'
|
|
7
|
+
gem 'logger' if RUBY_VERSION >= '3.4'
|
|
8
|
+
gem 'mongoid', version =~ /\A\d+\z/ ? "~> #{version}.0" : version
|
|
15
9
|
|
|
16
10
|
group :development do
|
|
11
|
+
if RUBY_VERSION >= '2.7'
|
|
12
|
+
gem 'danger'
|
|
13
|
+
gem 'danger-changelog', require: false
|
|
14
|
+
gem 'danger-pr-comment', require: false
|
|
15
|
+
end
|
|
16
|
+
gem 'dalli', RUBY_VERSION >= '3.0' ? '~> 3.0' : '~> 2.6'
|
|
17
17
|
gem 'rake'
|
|
18
|
-
gem '
|
|
18
|
+
gem 'rdoc', RUBY_VERSION >= '3.0' ? '>= 6' : '~> 4.2'
|
|
19
|
+
gem 'rubocop', ENV.fetch('RUBOCOP_VERSION', '>= 0')
|
|
19
20
|
gem 'yard', '~> 0.6'
|
|
20
|
-
gem 'dalli', '~> 2.6'
|
|
21
|
-
gem 'rubocop', '0.33.0'
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
group :test do
|
|
25
24
|
gem 'mongoid-compatibility'
|
|
26
25
|
gem 'rspec', '~> 3.1'
|
|
26
|
+
gem 'simplecov', '~> 0.22' if ENV['COVERAGE'] == 'true'
|
|
27
27
|
end
|
data/README.md
CHANGED
|
@@ -2,9 +2,10 @@ Mongoid::CachedJson
|
|
|
2
2
|
===================
|
|
3
3
|
|
|
4
4
|
[](http://badge.fury.io/rb/mongoid-cached-json)
|
|
5
|
-
[](https://github.com/mongoid/mongoid-cached-json/actions/workflows/test.yml?query=branch%3Amaster)
|
|
6
|
+
[](https://coveralls.io/github/mongoid/mongoid-cached-json?branch=master)
|
|
7
|
+
|
|
8
|
+
The Ruby 4.0 CI job measures library line coverage with SimpleCov and submits `coverage/.resultset.json` to Coveralls using GitHub's automatic token. Other matrix jobs do not submit duplicate reports. Run `COVERAGE=true bundle install` followed by `COVERAGE=true bundle exec rake spec` for a local report. The default-branch badge requires Coveralls to accept an upstream `master` build; configuring this workflow does not activate the external service.
|
|
8
9
|
|
|
9
10
|
Typical `as_json` definitions may involve lots of database point queries and method calls. When returning collections of objects, a single call may yield hundreds of database queries that can take seconds. This library mitigates the problem by implementing a module called `CachedJson`.
|
|
10
11
|
|
|
@@ -12,16 +13,12 @@ Typical `as_json` definitions may involve lots of database point queries and met
|
|
|
12
13
|
|
|
13
14
|
Using `Mongoid::CachedJson` we were able to cut our JSON API average response time by about a factor of 10.
|
|
14
15
|
|
|
15
|
-
Compatibility
|
|
16
|
-
-------------
|
|
17
|
-
|
|
18
|
-
This gem is compatible with Mongoid 3, 4 and 5.
|
|
19
|
-
|
|
20
16
|
Resources
|
|
21
17
|
---------
|
|
22
18
|
|
|
23
19
|
* [Need Help? Google Group](http://groups.google.com/group/mongoid-cached-json)
|
|
24
|
-
* [Source Code](http://github.com/
|
|
20
|
+
* [Source Code](http://github.com/mongoid/mongoid-cached-json)
|
|
21
|
+
* [Upgrading](UPGRADING.md)
|
|
25
22
|
|
|
26
23
|
Quickstart
|
|
27
24
|
----------
|
|
@@ -102,7 +99,7 @@ Defining Fields
|
|
|
102
99
|
|
|
103
100
|
`Mongoid::CachedJson` supports the following options:
|
|
104
101
|
|
|
105
|
-
* `:hide_as_child_json_when` is an optional function that
|
|
102
|
+
* `:hide_as_child_json_when` is an optional function that omits the child key from `as_json` parent objects when it returns true, eg. `cached_json hide_as_child_json_when: lambda { |instance| ! instance.secret? }`. A missing relationship remains a `nil` value.
|
|
106
103
|
|
|
107
104
|
`Mongoid::CachedJson` field definitions support the following options:
|
|
108
105
|
|
|
@@ -111,8 +108,66 @@ Defining Fields
|
|
|
111
108
|
* `:properties` can be one of `:short`, `:public`, `:all`, in this order
|
|
112
109
|
* `:version` can be a single version for this field to appear in
|
|
113
110
|
* `:versions` can be an array of versions for this field to appear in
|
|
111
|
+
* `:cache` defaults to `true`; set it to `false` to recompute a field on every serialization (see below)
|
|
114
112
|
* `:reference_properties` can be one of `:short`, `:public`, `:all`, default will select the reference properties format dynamically (see below)
|
|
115
113
|
|
|
114
|
+
Per-Field Caching
|
|
115
|
+
-----------------
|
|
116
|
+
|
|
117
|
+
Use `cache: false` for a field whose value should be evaluated on every `as_json` call, even when the rest of the object's JSON comes from the cache. The field is excluded from the cached fragment and merged into the response after that fragment is fetched. Its `definition`, property/version filters, and configured transformations still apply.
|
|
118
|
+
|
|
119
|
+
``` ruby
|
|
120
|
+
class Product
|
|
121
|
+
include Mongoid::Document
|
|
122
|
+
include Mongoid::CachedJson
|
|
123
|
+
|
|
124
|
+
field :name
|
|
125
|
+
field :stock, type: Integer
|
|
126
|
+
|
|
127
|
+
json_fields \
|
|
128
|
+
name: {},
|
|
129
|
+
stock: { cache: false }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
product = Product.create!(name: 'Widget', stock: 10)
|
|
133
|
+
product.as_json # => { name: 'Widget', stock: 10 }
|
|
134
|
+
|
|
135
|
+
# Unsaved changes illustrate which fields are recomputed on a cache hit.
|
|
136
|
+
product.name = 'Renamed widget'
|
|
137
|
+
product.stock = 9
|
|
138
|
+
product.as_json # => { name: 'Widget', stock: 9 }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The cached `name` stays unchanged until its cache entry expires or is invalidated (for example, by saving the product); `stock` is read from the current model instance each time. This option does not reload the model from MongoDB: reload a stale instance yourself if another process has changed its stored values.
|
|
142
|
+
|
|
143
|
+
For a `type: :reference` field, `cache: false` bypasses reads and writes of the referenced object's own JSON fragment through that relationship. The parent's other fields remain cached:
|
|
144
|
+
|
|
145
|
+
``` ruby
|
|
146
|
+
class Order
|
|
147
|
+
include Mongoid::Document
|
|
148
|
+
include Mongoid::CachedJson
|
|
149
|
+
|
|
150
|
+
field :number
|
|
151
|
+
belongs_to :product
|
|
152
|
+
|
|
153
|
+
json_fields \
|
|
154
|
+
number: {},
|
|
155
|
+
product: { type: :reference, cache: false }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
order = Order.create!(number: '123', product: product)
|
|
159
|
+
order.as_json # => { number: '123', product: { name: 'Renamed widget', stock: 9 } }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The `product` reference is recomputed even if the product has a cached JSON fragment from another serialization. This does not disable caching on the `Product` model itself: `product.as_json` and other relationships to that product still use their normal cache policies. References nested inside the product also retain their own cache policies; the bypass is not recursive.
|
|
163
|
+
|
|
164
|
+
All Stored Fields
|
|
165
|
+
-----------------
|
|
166
|
+
|
|
167
|
+
Use `json_fields :all, display_name: { definition: :display_name }` to expand every Mongoid field declared so far (including `_id`) into an ordinary JSON field, then merge explicit definitions. An explicit definition overrides the generated definition for the same symbol key; for example, `json_fields :all, secret: { properties: :all }` restricts that field to the `:all` representation.
|
|
168
|
+
|
|
169
|
+
Declare Mongoid fields before `json_fields :all`. Later declarations, dynamic attributes, and associations are not added automatically; declare references explicitly with `type: :reference`. This is opt-in and may expose sensitive stored data or foreign keys, so prefer an explicit allowlist for public APIs. Runtime `as_json(methods: ...)` selection remains unsupported: declare computed methods in `json_fields`.
|
|
170
|
+
|
|
116
171
|
Reference Properties
|
|
117
172
|
--------------------
|
|
118
173
|
|
|
@@ -120,7 +175,7 @@ When calling `as_json` on a model that contains references to other models the v
|
|
|
120
175
|
|
|
121
176
|
* Use the value of the `:reference_properties` option, if specified.
|
|
122
177
|
* For `:short` JSON, use `:short`.
|
|
123
|
-
* For `:public` JSON, use `:
|
|
178
|
+
* For `:public` JSON, use `:public`.
|
|
124
179
|
* For `:all` JSON, use `:all`.
|
|
125
180
|
|
|
126
181
|
The dynamic selection where `:public` generates `:short` references allows to return smaller embedded collections, while `:all` allows to fetch deep data. Another way of looking at this is to say that a field in a `:short` JSON appears in collections, a field declared in the `:public` JSON appears for all users and the field declared in the `:all` JSON appears for object owners only.
|
|
@@ -178,6 +233,13 @@ person.as_json(version: :v2) # { first: 'John', middle: 'F.', last: 'Kennedy', n
|
|
|
178
233
|
person.as_json(version: :v3) # { first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy', born: 'May 29, 1917' }
|
|
179
234
|
```
|
|
180
235
|
|
|
236
|
+
Schema Cache Versions
|
|
237
|
+
---------------------
|
|
238
|
+
|
|
239
|
+
Declare `json_fields cache_version: 2, name: {}` to add a model-specific schema namespace to its cache keys. Increment it when changing fields, computed values, or transformations in a deployment. This is independent of the API `version:` passed to `as_json`; callers need not change. Omitting `cache_version` preserves existing keys.
|
|
240
|
+
|
|
241
|
+
New code stops reading old fragments, including referenced fragments of that model. Old entries are not deleted and remain until eviction or a cache-store TTL. The gem does not set a default TTL; configure `expires_in` on your cache store if needed. Updates expire only the current namespace. For rolling deployments or rollbacks, do not reuse a namespace containing stale values: coordinate invalidation or choose another version.
|
|
242
|
+
|
|
181
243
|
Transformations
|
|
182
244
|
---------------
|
|
183
245
|
|
|
@@ -214,6 +276,28 @@ Turning Caching Off
|
|
|
214
276
|
|
|
215
277
|
You can set `Mongoid::CachedJson.config.disable_caching = true`. It may be a good idea to set it to `ENV['DISABLE_JSON_CACHING']`, in case this turns out not to be The Solution To All Of Your Performance Problems (TM).
|
|
216
278
|
|
|
279
|
+
To bypass shared-cache reads and writes for one model while retaining its JSON fields, versions, and property hierarchy, declare `json_fields disable_caching: true, name: {}`. This applies when the model is serialized directly, in collections, or as a reference. Other models, including its parent and referenced children, keep their own caching policy. Changes to an uncached child are visible even when its parent's scalar fields are cached.
|
|
280
|
+
|
|
281
|
+
Cache Invalidation Boundaries
|
|
282
|
+
-----------------------------
|
|
283
|
+
|
|
284
|
+
Normal model updates and destroys expire that document's JSON fragments through Mongoid callbacks. Bulk or raw collection updates that bypass model callbacks cannot invalidate the cache automatically. Explicitly expire each affected document after such a write:
|
|
285
|
+
|
|
286
|
+
``` ruby
|
|
287
|
+
User.where(id: user_id).update_all(status: 'active')
|
|
288
|
+
User.find(user_id).expire_cached_json
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Concurrent writes have a separate boundary: an older in-memory instance can repopulate a fragment after another instance has saved newer values and expired the cache. Reload stale instances before serializing them, and expire their fragments if a stale writer may already have repopulated the cache:
|
|
292
|
+
|
|
293
|
+
``` ruby
|
|
294
|
+
user.reload
|
|
295
|
+
user.expire_cached_json
|
|
296
|
+
user.as_json
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Reloading and expiring are not an atomic cross-process operation. This gem does not provide a distributed revision or compare-and-set protocol for cache stores. Applications requiring strict consistency across concurrent writers should coordinate persistence and cache invalidation themselves or disable caching for the affected data.
|
|
300
|
+
|
|
217
301
|
Testing JSON
|
|
218
302
|
------------
|
|
219
303
|
|
|
@@ -293,6 +377,6 @@ See [CONTRIBUTING](CONTRIBUTING.md).
|
|
|
293
377
|
Copyright and License
|
|
294
378
|
---------------------
|
|
295
379
|
|
|
296
|
-
MIT License, see [LICENSE](https://github.com/
|
|
380
|
+
MIT License, see [LICENSE](https://github.com/mongoid/mongoid-cached-json/blob/master/LICENSE.md) for details.
|
|
297
381
|
|
|
298
382
|
(c) 2012-2014 [Artsy](https://artsy.net) and [Contributors](CHANGELOG.md)
|
data/Rakefile
CHANGED
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Upgrading
|
|
2
|
+
|
|
3
|
+
## Upgrading to 1.7.0
|
|
4
|
+
|
|
5
|
+
### Hidden child references
|
|
6
|
+
|
|
7
|
+
[#35](https://github.com/mongoid/mongoid-cached-json/pull/35) changes how a single child reference is serialized when the child's `hide_as_child_json_when` predicate returns `true`. Previously, an existing but hidden child and a missing child both appeared as `child: nil` (`"child": null` in JSON), so clients could not distinguish them. Now, an existing but hidden child's key is omitted; a missing child still appears as `child: nil`.
|
|
8
|
+
|
|
9
|
+
Here, `parent` is a `SecretParent` document with a `has_one` relationship to a `SometimesSecret` document. The parent exposes that relationship as the JSON field `child`; the hiding predicate is defined on the child model:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class SecretParent
|
|
13
|
+
include Mongoid::Document
|
|
14
|
+
include Mongoid::CachedJson
|
|
15
|
+
|
|
16
|
+
field :name
|
|
17
|
+
has_one :sometimes_secret
|
|
18
|
+
|
|
19
|
+
json_fields \
|
|
20
|
+
name: {},
|
|
21
|
+
child: { definition: :sometimes_secret, type: :reference }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class SometimesSecret
|
|
25
|
+
include Mongoid::Document
|
|
26
|
+
include Mongoid::CachedJson
|
|
27
|
+
|
|
28
|
+
field :secret
|
|
29
|
+
field :should_tell_secret, type: Boolean
|
|
30
|
+
belongs_to :secret_parent
|
|
31
|
+
|
|
32
|
+
json_fields \
|
|
33
|
+
hide_as_child_json_when: lambda { |child| !child.should_tell_secret },
|
|
34
|
+
secret: {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
parent = SecretParent.create!(name: 'Parent')
|
|
38
|
+
parent.create_sometimes_secret!(secret: 'Afraid of the dark', should_tell_secret: false)
|
|
39
|
+
|
|
40
|
+
# The child exists, but its hiding predicate returns true.
|
|
41
|
+
# Before 1.7.0:
|
|
42
|
+
parent.as_json(properties: :short)
|
|
43
|
+
# => { name: 'Parent', child: nil }
|
|
44
|
+
|
|
45
|
+
# From 1.7.0:
|
|
46
|
+
parent.as_json(properties: :short)
|
|
47
|
+
# => { name: 'Parent' }
|
|
48
|
+
|
|
49
|
+
# By contrast, a parent with no child still includes an explicit nil.
|
|
50
|
+
parent_without_child = SecretParent.create!(name: 'No child')
|
|
51
|
+
parent_without_child.as_json(properties: :short)
|
|
52
|
+
# => { name: 'No child', child: nil }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A genuinely missing relationship still includes `child: nil`. Requests using `properties: :all` and serialization of the child itself are unchanged.
|
|
56
|
+
|
|
57
|
+
Update clients and response schemas that require the child key to allow its absence. In Ruby, `json[:child]` still returns `nil` for either case; use `json.key?(:child)` to distinguish an omitted key from an explicit `nil`.
|
|
58
|
+
|
|
59
|
+
Invalidate existing cached JSON fragments when deploying this change so cached results use the new representation. If the cache is shared with other applications, invalidate only this application's cached JSON entries rather than clearing the entire store.
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# encoding: utf-8
|
|
2
2
|
module Mongoid
|
|
3
3
|
module CachedJson
|
|
4
|
+
HIDDEN_JSON = :__mongoid_cached_json_hidden__
|
|
5
|
+
|
|
4
6
|
extend ActiveSupport::Concern
|
|
5
7
|
|
|
6
8
|
included do
|
|
7
9
|
class_attribute :all_json_properties
|
|
8
10
|
class_attribute :all_json_versions
|
|
9
11
|
class_attribute :cached_json_field_defs
|
|
12
|
+
class_attribute :cached_json_uncached_field_defs
|
|
10
13
|
class_attribute :cached_json_reference_defs
|
|
11
14
|
class_attribute :hide_as_child_json_when
|
|
15
|
+
class_attribute :cached_json_cache_version
|
|
16
|
+
class_attribute :cached_json_disable_caching
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
module ClassMethods
|
|
@@ -17,11 +22,17 @@ module Mongoid
|
|
|
17
22
|
# @param [ hash ] defs JSON field definition.
|
|
18
23
|
#
|
|
19
24
|
# @since 1.0
|
|
20
|
-
def json_fields(defs)
|
|
25
|
+
def json_fields(defs, additions = {})
|
|
26
|
+
if defs == :all
|
|
27
|
+
defs = Hash[fields.keys.map { |field| [field.to_sym, {}] }].merge(additions)
|
|
28
|
+
end
|
|
29
|
+
self.cached_json_cache_version = defs.delete(:cache_version)
|
|
30
|
+
self.cached_json_disable_caching = defs.delete(:disable_caching)
|
|
21
31
|
self.hide_as_child_json_when = defs.delete(:hide_as_child_json_when) || lambda { |_a| false }
|
|
22
32
|
self.all_json_properties = [:short, :public, :all]
|
|
23
33
|
cached_json_defs = Hash[defs.map { |k, v| [k, { type: :callable, properties: :short, definition: k }.merge(v)] }]
|
|
24
34
|
self.cached_json_field_defs = {}
|
|
35
|
+
self.cached_json_uncached_field_defs = {}
|
|
25
36
|
self.cached_json_reference_defs = {}
|
|
26
37
|
# Collect all versions for clearing cache
|
|
27
38
|
self.all_json_versions = cached_json_defs.map do |_field, definition|
|
|
@@ -29,7 +40,10 @@ module Mongoid
|
|
|
29
40
|
end.flatten.compact.uniq
|
|
30
41
|
all_json_properties.each_with_index do |property, i|
|
|
31
42
|
cached_json_field_defs[property] = Hash[cached_json_defs.find_all do |_field, definition|
|
|
32
|
-
all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable
|
|
43
|
+
all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable && definition[:cache] != false
|
|
44
|
+
end]
|
|
45
|
+
cached_json_uncached_field_defs[property] = Hash[cached_json_defs.find_all do |_field, definition|
|
|
46
|
+
all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable && definition[:cache] == false
|
|
33
47
|
end]
|
|
34
48
|
cached_json_reference_defs[property] = Hash[cached_json_defs.find_all do |_field, definition|
|
|
35
49
|
all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :reference
|
|
@@ -49,8 +63,10 @@ module Mongoid
|
|
|
49
63
|
def materialize_cached_json(clazz, id, object_reference, options)
|
|
50
64
|
is_top_level_json = options[:is_top_level_json] || false
|
|
51
65
|
object_reference = clazz.where(_id: id).first unless object_reference
|
|
52
|
-
if !object_reference
|
|
66
|
+
if !object_reference
|
|
53
67
|
nil
|
|
68
|
+
elsif !is_top_level_json && options[:properties] != :all && clazz.hide_as_child_json_when.call(object_reference)
|
|
69
|
+
HIDDEN_JSON
|
|
54
70
|
else
|
|
55
71
|
Hash[clazz.cached_json_field_defs[options[:properties]].map do |field, definition|
|
|
56
72
|
# version match
|
|
@@ -65,6 +81,22 @@ module Mongoid
|
|
|
65
81
|
end
|
|
66
82
|
end
|
|
67
83
|
|
|
84
|
+
# Materialize JSON fields that opt out of shared caching.
|
|
85
|
+
def materialize_uncached_json(clazz, id, object_reference, options)
|
|
86
|
+
object_reference = clazz.where(_id: id).first unless object_reference
|
|
87
|
+
return {} unless object_reference
|
|
88
|
+
|
|
89
|
+
Hash[clazz.cached_json_uncached_field_defs[options[:properties]].map do |field, definition|
|
|
90
|
+
versions = ([definition[:version]] | Array(definition[:versions])).compact
|
|
91
|
+
next unless versions.empty? || versions.include?(options[:version])
|
|
92
|
+
json_value = (definition[:definition].is_a?(Symbol) ? object_reference.send(definition[:definition]) : definition[:definition].call(object_reference))
|
|
93
|
+
Mongoid::CachedJson.config.transform.each do |t|
|
|
94
|
+
json_value = t.call(field, definition, json_value)
|
|
95
|
+
end
|
|
96
|
+
[field, json_value]
|
|
97
|
+
end.compact]
|
|
98
|
+
end
|
|
99
|
+
|
|
68
100
|
# Given an object definition in the form of either an object or a class, id pair,
|
|
69
101
|
# grab the as_json representation from the cache if possible, otherwise create
|
|
70
102
|
# the as_json representation by loading the object from the database. For any
|
|
@@ -84,7 +116,8 @@ module Mongoid
|
|
|
84
116
|
id = object_def[:id]
|
|
85
117
|
end
|
|
86
118
|
key = cached_json_key(options, clazz, id)
|
|
87
|
-
json = { _ref: { _clazz: self, _key: key,
|
|
119
|
+
json = { _ref: { _clazz: self, _key: key, _disable_caching: clazz.cached_json_disable_caching,
|
|
120
|
+
_materialize_cached_json: [clazz, id, object_reference, options] } }
|
|
88
121
|
keys = KeyReferences.new
|
|
89
122
|
keys.set_and_add(key, json)
|
|
90
123
|
reference_defs = clazz.cached_json_reference_defs[options[:properties]]
|
|
@@ -98,6 +131,12 @@ module Mongoid
|
|
|
98
131
|
ref[:_parent] = json
|
|
99
132
|
ref[:_field] = field
|
|
100
133
|
end
|
|
134
|
+
if definition[:cache] == false
|
|
135
|
+
references = reference.is_a?(Array) ? reference : [reference]
|
|
136
|
+
references.each do |item|
|
|
137
|
+
item[:_ref][:_disable_caching] = true if item.is_a?(Hash) && item[:_ref]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
101
140
|
keys.merge_set(reference_keys)
|
|
102
141
|
[field, reference]
|
|
103
142
|
end])
|
|
@@ -109,7 +148,8 @@ module Mongoid
|
|
|
109
148
|
# Cache key.
|
|
110
149
|
def cached_json_key(options, cached_class, cached_id)
|
|
111
150
|
base_class_name = cached_class.collection_name.to_s.singularize.camelize
|
|
112
|
-
"as_json/#{options[:version]}/#{base_class_name}/#{cached_id}/#{options[:properties]}/#{!!options[:is_top_level_json]}"
|
|
151
|
+
key = "as_json/#{options[:version]}/#{base_class_name}/#{cached_id}/#{options[:properties]}/#{!!options[:is_top_level_json]}"
|
|
152
|
+
cached_class.cached_json_cache_version.nil? ? key : "#{key}/schema/#{cached_class.cached_json_cache_version}"
|
|
113
153
|
end
|
|
114
154
|
|
|
115
155
|
# If the reference is a symbol, we may be lucky and be able to figure out the as_json
|
|
@@ -126,7 +166,13 @@ module Mongoid
|
|
|
126
166
|
else
|
|
127
167
|
clazz = reference_def[:metadata].class_name.constantize
|
|
128
168
|
end
|
|
129
|
-
|
|
169
|
+
relation_class = if Mongoid::Compatibility::Version.mongoid7_or_newer?
|
|
170
|
+
Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy
|
|
171
|
+
else
|
|
172
|
+
Mongoid::Relations::Referenced::ManyToMany
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if reference_def[:metadata].relation == relation_class
|
|
130
176
|
object_ids = object.send(key)
|
|
131
177
|
if object_ids
|
|
132
178
|
reference_json = object_ids.map do |id|
|
|
@@ -145,13 +191,11 @@ module Mongoid
|
|
|
145
191
|
reference_def_definition = reference_def[:definition]
|
|
146
192
|
reference = reference_def_definition.is_a?(Symbol) ? object.send(reference_def_definition) : reference_def_definition.call(object)
|
|
147
193
|
reference_json = nil
|
|
148
|
-
if reference
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
reference_json = reference.as_json(options)
|
|
154
|
-
end
|
|
194
|
+
if reference.respond_to?(:as_json_partial)
|
|
195
|
+
reference_keys, reference_json = reference.as_json_partial(options)
|
|
196
|
+
keys = keys ? keys.merge_set(reference_keys) : reference_keys
|
|
197
|
+
else
|
|
198
|
+
reference_json = reference.as_json(options)
|
|
155
199
|
end
|
|
156
200
|
end
|
|
157
201
|
[keys, reference_json]
|
|
@@ -160,8 +204,10 @@ module Mongoid
|
|
|
160
204
|
|
|
161
205
|
# Check whether the cache supports :read_multi and prefetch the data if it does.
|
|
162
206
|
def self.materialize_json_references_with_read_multi(key_refs, partial_json)
|
|
163
|
-
|
|
164
|
-
|
|
207
|
+
if key_refs
|
|
208
|
+
unfrozen_keys = key_refs.select { |_key, refs| refs.any? { |ref| !ref[:_ref][:_disable_caching] } }.keys.map(&:dup)
|
|
209
|
+
end
|
|
210
|
+
read_multi = unfrozen_keys && !unfrozen_keys.empty? && Mongoid::CachedJson.config.cache.respond_to?(:read_multi)
|
|
165
211
|
local_cache = read_multi ? Mongoid::CachedJson.config.cache.read_multi(*unfrozen_keys) : {}
|
|
166
212
|
Mongoid::CachedJson.materialize_json_references(key_refs, local_cache, read_multi) if key_refs
|
|
167
213
|
partial_json
|
|
@@ -174,7 +220,9 @@ module Mongoid
|
|
|
174
220
|
_ref = ref.delete(:_ref)
|
|
175
221
|
key = _ref[:_key]
|
|
176
222
|
fetched_json = local_cache[key] if local_cache.key?(key)
|
|
177
|
-
|
|
223
|
+
if _ref[:_disable_caching]
|
|
224
|
+
fetched_json = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json])
|
|
225
|
+
elsif !fetched_json
|
|
178
226
|
if read_multi
|
|
179
227
|
# no value in cache, materialize and write
|
|
180
228
|
fetched_json = (local_cache[key] = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json]))
|
|
@@ -186,8 +234,11 @@ module Mongoid
|
|
|
186
234
|
end)
|
|
187
235
|
end
|
|
188
236
|
end
|
|
189
|
-
if fetched_json
|
|
237
|
+
if fetched_json == HIDDEN_JSON
|
|
238
|
+
_ref[:_parent].delete(_ref[:_field]) if _ref[:_parent]
|
|
239
|
+
elsif fetched_json
|
|
190
240
|
ref.merge! fetched_json
|
|
241
|
+
ref.merge! _ref[:_clazz].materialize_uncached_json(* _ref[:_materialize_cached_json])
|
|
191
242
|
elsif _ref[:_parent]
|
|
192
243
|
# a single _ref that resolved to a nil
|
|
193
244
|
_ref[:_parent][_ref[:_field]] = nil
|
data/mongoid-cached-json.gemspec
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
$LOAD_PATH.push File.expand_path('
|
|
1
|
+
$LOAD_PATH.push File.expand_path('lib', __dir__)
|
|
2
2
|
require 'mongoid-cached-json/version'
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
|
10
10
|
s.required_rubygems_version = '>= 1.3.6'
|
|
11
11
|
s.files = `git ls-files`.split("\n")
|
|
12
12
|
s.require_paths = ['lib']
|
|
13
|
-
s.homepage = 'http://github.com/
|
|
13
|
+
s.homepage = 'http://github.com/mongoid/mongoid-cached-json'
|
|
14
14
|
s.licenses = ['MIT']
|
|
15
15
|
s.summary = 'Cached-json is a DSL for describing JSON representations of Mongoid models.'
|
|
16
16
|
s.add_dependency 'mongoid', '>= 3.0'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Declaring all stored fields' do
|
|
4
|
+
before do
|
|
5
|
+
class AllFieldsDocument
|
|
6
|
+
include Mongoid::Document
|
|
7
|
+
include Mongoid::CachedJson
|
|
8
|
+
field :name
|
|
9
|
+
field :secret
|
|
10
|
+
|
|
11
|
+
def display_name
|
|
12
|
+
name.upcase
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
json_fields :all, secret: { properties: :all }, display_name: {}
|
|
16
|
+
field :added_later
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
after do
|
|
21
|
+
AllFieldsDocument.delete_all
|
|
22
|
+
Object.send(:remove_const, :AllFieldsDocument)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'expands fields at declaration time and adds computed fields' do
|
|
26
|
+
document = AllFieldsDocument.create!(name: 'example', secret: 'hidden', added_later: 'not declared')
|
|
27
|
+
expected = { _id: document.id, name: 'example', display_name: 'EXAMPLE' }
|
|
28
|
+
expect(document.as_json).to eq(expected)
|
|
29
|
+
expect([document].as_json).to eq([expected])
|
|
30
|
+
expect(document.as_json(properties: :all)).to eq(expected.merge(secret: 'hidden'))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'retains the normal field cache and invalidation behavior' do
|
|
34
|
+
document = AllFieldsDocument.create!(name: 'example')
|
|
35
|
+
document.as_json
|
|
36
|
+
document.name = 'unsaved'
|
|
37
|
+
expect(document.as_json[:name]).to eq('example')
|
|
38
|
+
document.save!
|
|
39
|
+
expect(document.as_json[:name]).to eq('unsaved')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'does not expose extra fields for ordinary explicit declarations' do
|
|
43
|
+
document = JsonFoobar.create!(foo: 'visible', bar: 'private')
|
|
44
|
+
expect(document.as_json).not_to have_key(:bar)
|
|
45
|
+
expect(document.as_json).not_to have_key(:_id)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Schema cache versions' do
|
|
4
|
+
before do
|
|
5
|
+
class VersionedDocument
|
|
6
|
+
include Mongoid::Document
|
|
7
|
+
include Mongoid::CachedJson
|
|
8
|
+
field :value
|
|
9
|
+
json_fields cache_version: 1, value: {}
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after do
|
|
14
|
+
VersionedDocument.delete_all
|
|
15
|
+
Object.send(:remove_const, :VersionedDocument)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
[true, false].each do |bulk|
|
|
19
|
+
context "with bulk reads #{bulk}" do
|
|
20
|
+
before do
|
|
21
|
+
@store = ActiveSupport::Cache::MemoryStore.new
|
|
22
|
+
@store.singleton_class.send(:undef_method, :read_multi) unless bulk
|
|
23
|
+
Mongoid::CachedJson.config.cache = @store
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'separates new schema fragments without changing the API version' do
|
|
27
|
+
document = VersionedDocument.create!(value: 'old')
|
|
28
|
+
expect(document.as_json).to eq(value: 'old')
|
|
29
|
+
document.value = 'new'
|
|
30
|
+
expect(document.as_json).to eq(value: 'old')
|
|
31
|
+
VersionedDocument.cached_json_cache_version = 2
|
|
32
|
+
expect(document.as_json).to eq(value: 'new')
|
|
33
|
+
VersionedDocument.cached_json_cache_version = 1
|
|
34
|
+
expect(document.as_json).to eq(value: 'old')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'invalidates the current namespace on a model update' do
|
|
38
|
+
document = VersionedDocument.create!(value: 'old')
|
|
39
|
+
[1, 2].each do |version|
|
|
40
|
+
VersionedDocument.cached_json_cache_version = version
|
|
41
|
+
document.as_json
|
|
42
|
+
end
|
|
43
|
+
document.update_attributes!(value: 'new')
|
|
44
|
+
expect(document.as_json).to eq(value: 'new')
|
|
45
|
+
VersionedDocument.cached_json_cache_version = 1
|
|
46
|
+
expect(document.as_json).to eq(value: 'old')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'uses the schema version for child fragments too' do
|
|
50
|
+
document = VersionedDocument.create!(value: 'old')
|
|
51
|
+
options = { properties: :short, version: :unspecified, is_top_level_json: false }
|
|
52
|
+
key = VersionedDocument.cached_json_key(options, VersionedDocument, document.id)
|
|
53
|
+
expect(key).to end_with('/false/schema/1')
|
|
54
|
+
expect([document].as_json).to eq([{ value: 'old' }])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|