mongoid-cached-json 1.6.0 → 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 +4 -4
- 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 +120 -67
- data/CHANGELOG.md +59 -65
- data/CONTRIBUTING.md +2 -0
- data/Dangerfile +5 -0
- data/Gemfile +14 -17
- data/README.md +94 -9
- data/UPGRADING.md +59 -0
- data/lib/mongoid-cached-json/cached_json.rb +61 -16
- data/lib/mongoid-cached-json/version.rb +1 -1
- data/spec/all_fields_spec.rb +47 -0
- data/spec/cache_version_spec.rb +58 -0
- data/spec/cached_json_spec.rb +20 -11
- 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/json_employee.rb +1 -0
- data/spec/support/json_manager.rb +6 -1
- metadata +16 -8
- data/.travis.yml +0 -40
data/CHANGELOG.md
CHANGED
|
@@ -1,103 +1,97 @@
|
|
|
1
|
-
1.
|
|
2
|
-
------------
|
|
1
|
+
### 1.7.1 (Next)
|
|
3
2
|
|
|
4
3
|
* Your contribution here.
|
|
5
4
|
|
|
6
|
-
1.
|
|
7
|
-
------------
|
|
5
|
+
### 1.7.0 (2026/10/16)
|
|
8
6
|
|
|
9
|
-
* [#
|
|
10
|
-
* [#
|
|
11
|
-
* [#
|
|
12
|
-
* [#
|
|
7
|
+
* [#33](https://github.com/mongoid/mongoid-cached-json/pull/33): Allow per-model cache bypass with `json_fields disable_caching: true` - [@dblock](https://github.com/dblock).
|
|
8
|
+
* [#32](https://github.com/mongoid/mongoid-cached-json/pull/32): Support opt-in declaration-time expansion of all stored fields with explicit overrides and computed fields - [@dblock](https://github.com/dblock).
|
|
9
|
+
* [#31](https://github.com/mongoid/mongoid-cached-json/pull/31): Add explicit per-model schema cache versions, independent of API versions - [@dblock](https://github.com/dblock).
|
|
10
|
+
* [#39](https://github.com/mongoid/mongoid-cached-json/pull/39): Check pull request changelogs with Danger and report results in PR comments - [@dblock](https://github.com/dblock).
|
|
11
|
+
* [#34](https://github.com/mongoid/mongoid-cached-json/pull/34): Support `cache: false` on individual JSON fields - [@dblock](https://github.com/dblock).
|
|
12
|
+
* [#36](https://github.com/mongoid/mongoid-cached-json/pull/36): Document cache invalidation boundaries for callback-bypassing and concurrent writes - [@dblock](https://github.com/dblock).
|
|
13
|
+
* [#35](https://github.com/mongoid/mongoid-cached-json/pull/35): Omit hidden child references while preserving `nil` for missing references (see [UPGRADING](UPGRADING.md) for migration notes) - [@dblock](https://github.com/dblock).
|
|
14
|
+
* [#38](https://github.com/mongoid/mongoid-cached-json/pull/38): Fix a `:reference` field returning `nil` instead of `false` when its definition evaluates to a false-y, non-nil value - [@mzikherman](https://github.com/mzikherman).
|
|
15
|
+
* [#30](https://github.com/mongoid/mongoid-cached-json/pull/30): Replace the obsolete quality badge with Coveralls, generate real SimpleCov coverage in one modern CI job, and scope badges to upstream master - [@dblock](https://github.com/dblock).
|
|
16
|
+
* [#30](https://github.com/mongoid/mongoid-cached-json/pull/30): Modernize CI with GitHub Actions, testing representative Ruby/Mongoid/MongoDB combinations through Ruby 4.0, Mongoid 9, and MongoDB Server 8.0, with modern memcached cache integration - [@dblock](https://github.com/dblock).
|
|
13
17
|
|
|
14
|
-
1.
|
|
15
|
-
------------------
|
|
18
|
+
### 1.6.0 (2019/05/31)
|
|
16
19
|
|
|
17
|
-
* Compatibility with
|
|
20
|
+
* [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): Compatibility with Ruby 2.3, 2.4, 2.5 and 2.6 - [@yuki24](https://github.com/yuki24).
|
|
21
|
+
* [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): Compatibility with Mongoid 6 - [@yuki24](https://github.com/yuki24).
|
|
22
|
+
* [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): No longer test against Ruby 1.9.3 - [@yuki24](https://github.com/yuki24).
|
|
23
|
+
* [#22](https://github.com/mongoid/mongoid-cached-json/pull/22): Compatibility with Mongoid 7 - [@yuki24](https://github.com/yuki24).
|
|
18
24
|
|
|
19
|
-
1.5.
|
|
20
|
-
------------------
|
|
25
|
+
### 1.5.3 (2015/09/17)
|
|
21
26
|
|
|
22
|
-
*
|
|
23
|
-
* Implemented RuboCop, Ruby-style linter - [@dblock](http://github.com/dblock).
|
|
24
|
-
* Upgraded to RSpec 3.1 - [@dblock](http://github.com/dblock).
|
|
25
|
-
* Removed Jeweler - [@dblock](http://github.com/dblock).
|
|
27
|
+
* Compatibility with Mongoid 5 - [@dblock](https://github.com/dblock).
|
|
26
28
|
|
|
27
|
-
1.5.
|
|
28
|
-
--------------------
|
|
29
|
+
### 1.5.2 (2014/12/29)
|
|
29
30
|
|
|
30
|
-
* Fixed
|
|
31
|
+
* Fixed support for Ruby 2.2.0 - [@dblock](https://github.com/dblock).
|
|
32
|
+
* Implemented RuboCop, Ruby-style linter - [@dblock](https://github.com/dblock).
|
|
33
|
+
* Upgraded to RSpec 3.1 - [@dblock](https://github.com/dblock).
|
|
34
|
+
* Removed Jeweler - [@dblock](https://github.com/dblock).
|
|
31
35
|
|
|
32
|
-
1.5 (2013/
|
|
33
|
-
|
|
36
|
+
### 1.5.1 (2013/05/07)
|
|
37
|
+
|
|
38
|
+
* Fixed `read_multi` calls so as to enable proper cache reading behavior in stores other than `ActiveSupport::Cache::DalliStore` - [@macreery](https://github.com/macreery).
|
|
39
|
+
|
|
40
|
+
### 1.5.0 (2013/04/13)
|
|
34
41
|
|
|
35
42
|
* Added `:reference_properties` that disables dynamic selection of the type of JSON to return for a reference - [@dblock](https://github.com/dblock).
|
|
36
43
|
|
|
37
|
-
1.4.3 (2013/01/25)
|
|
38
|
-
------------------
|
|
44
|
+
### 1.4.3 (2013/01/25)
|
|
39
45
|
|
|
40
46
|
* For caches that support `read_multi`, do not attempt to fetch JSON a second time via `fetch`, write it directly to cache - [@dblock](https://github.com/dblock).
|
|
41
47
|
|
|
42
|
-
1.4.2 (2013/01/24)
|
|
43
|
-
------------------
|
|
44
|
-
|
|
45
|
-
* Fix: calling `as_json` on a destroyed Mongoid 3.1 object with a HABTM relationship raises `undefined method 'map' for nil:NilClass` - [@dblock](http://github.com/dblock).
|
|
48
|
+
### 1.4.2 (2013/01/24)
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
------------------
|
|
50
|
+
* Fix: calling `as_json` on a destroyed Mongoid 3.1 object with a HABTM relationship raises `undefined method 'map' for nil:NilClass` - [@dblock](https://github.com/dblock).
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
* Do not invalidate cache when the document is created - [@dblock](http://github.com/dblock).
|
|
52
|
-
* Invalidate cache in `after_update` instead of `before_update` - [@dblock](http://github.com/dblock).
|
|
52
|
+
### 1.4.1 (2013/01/22)
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
* Invalidate cache in `after_destroy` - [@dblock](https://github.com/dblock).
|
|
55
|
+
* Do not invalidate cache when the document is created - [@dblock](https://github.com/dblock).
|
|
56
|
+
* Invalidate cache in `after_update` instead of `before_update` - [@dblock](https://github.com/dblock).
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
* Use `read_multi` if the cache store supports it to fetch data from cache in bulk - [@dblock](http://github.com/dblock), [@macreery](http://github.com/macreery).
|
|
59
|
-
* Added a benchmark test suite - [@dblock](http://github.com/dblock), [@macreery](http://github.com/macreery).
|
|
58
|
+
### 1.4.0 (2013/01/20)
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
* Collect a JSON partial representation first, then fetch data from cache only once per-key - [@dblock](https://github.com/dblock), [@macreery](https://github.com/macreery).
|
|
61
|
+
* Use `read_multi` if the cache store supports it to fetch data from cache in bulk - [@dblock](https://github.com/dblock), [@macreery](https://github.com/macreery).
|
|
62
|
+
* Added a benchmark test suite - [@dblock](https://github.com/dblock), [@macreery](https://github.com/macreery).
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
### 1.3.0 (2012/11/12)
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
------------------
|
|
66
|
+
* Removed requirement for `bson_ext`, support for Mongoid 3.0 - [@dblock](https://github.com/dblock).
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
### 1.2.3 (2012/07/03)
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
------------------
|
|
70
|
+
* Fix: including a `referenced_in` field in `json_fields` within a child `embedded_in` a parent causes an "access to the collection is not allowed since it is an embedded document" error - [@dblock](https://github.com/dblock).
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
### 1.2.2 (2012/07/03)
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
------------------
|
|
74
|
+
* Fix [#6](https://github.com/mongoid/mongoid-cached-json/issues/6): including parent in `json_fields` within a polymorphic reference fails with an "uninitialized constant" error - [@dblock](https://github.com/dblock).
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
### 1.2.1 (2012/06/12)
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
------------------
|
|
78
|
+
* Allow `nil` parameter in as_json - [@dblock](https://github.com/dblock).
|
|
83
79
|
|
|
84
|
-
|
|
80
|
+
### 1.2.0 (2012/05/28)
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
------------------
|
|
82
|
+
* Fix: cache key generation bug when using Mongoid 3 - [@marbemac](https://github.com/marbemac).
|
|
88
83
|
|
|
89
|
-
|
|
84
|
+
### 1.1.1 (2012/03/21)
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
----------------
|
|
86
|
+
* Fix: caching/invalidating referenced polymorphic documents - [@macreery](https://github.com/macreery).
|
|
93
87
|
|
|
94
|
-
|
|
88
|
+
### 1.1.0 (2012/02/29)
|
|
95
89
|
|
|
96
|
-
|
|
97
|
-
----------------
|
|
90
|
+
* Added support for versioning - [@dblock](https://github.com/dblock).
|
|
98
91
|
|
|
99
|
-
|
|
100
|
-
* Retired support for `:markdown` in favor of `Mongoid::CachedJson::transform` - [@dblock](http://github.com/dblock).
|
|
101
|
-
* Added `Mongoid::CachedJson::configure` - [@dblock](http://github.com/dblock).
|
|
102
|
-
* Added support for `:markdown` - [@macreery](http://github.com/macreery).
|
|
92
|
+
### 1.0.0 (2012/02/20)
|
|
103
93
|
|
|
94
|
+
* Initial release - [@aaw](https://github.com/aaw).
|
|
95
|
+
* Retired support for `:markdown` in favor of `Mongoid::CachedJson::transform` - [@dblock](https://github.com/dblock).
|
|
96
|
+
* Added `Mongoid::CachedJson::configure` - [@dblock](https://github.com/dblock).
|
|
97
|
+
* Added support for `:markdown` - [@macreery](https://github.com/macreery).
|
data/CONTRIBUTING.md
CHANGED
|
@@ -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:
|
data/Dangerfile
ADDED
data/Gemfile
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
source '
|
|
1
|
+
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
3
|
gemspec
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
gem 'mongoid', '~> 6.0'
|
|
10
|
-
when /5/
|
|
11
|
-
gem 'mongoid', '~> 5.0'
|
|
12
|
-
when /4/
|
|
13
|
-
gem 'mongoid', '~> 4.0'
|
|
14
|
-
when /3/
|
|
15
|
-
gem 'mongoid', '~> 3.1'
|
|
16
|
-
else
|
|
17
|
-
gem 'mongoid', version
|
|
18
|
-
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
|
|
19
9
|
|
|
20
10
|
group :development do
|
|
21
|
-
|
|
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'
|
|
22
17
|
gem 'rake'
|
|
23
|
-
gem '
|
|
18
|
+
gem 'rdoc', RUBY_VERSION >= '3.0' ? '>= 6' : '~> 4.2'
|
|
19
|
+
gem 'rubocop', ENV.fetch('RUBOCOP_VERSION', '>= 0')
|
|
24
20
|
gem 'yard', '~> 0.6'
|
|
25
21
|
end
|
|
26
22
|
|
|
27
23
|
group :test do
|
|
28
24
|
gem 'mongoid-compatibility'
|
|
29
25
|
gem 'rspec', '~> 3.1'
|
|
26
|
+
gem 'simplecov', '~> 0.22' if ENV['COVERAGE'] == 'true'
|
|
30
27
|
end
|
data/README.md
CHANGED
|
@@ -2,8 +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.
|
|
7
9
|
|
|
8
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`.
|
|
9
11
|
|
|
@@ -11,16 +13,12 @@ Typical `as_json` definitions may involve lots of database point queries and met
|
|
|
11
13
|
|
|
12
14
|
Using `Mongoid::CachedJson` we were able to cut our JSON API average response time by about a factor of 10.
|
|
13
15
|
|
|
14
|
-
Compatibility
|
|
15
|
-
-------------
|
|
16
|
-
|
|
17
|
-
This gem is compatible with Mongoid 3, 4, 5, 6, and 7.
|
|
18
|
-
|
|
19
16
|
Resources
|
|
20
17
|
---------
|
|
21
18
|
|
|
22
19
|
* [Need Help? Google Group](http://groups.google.com/group/mongoid-cached-json)
|
|
23
20
|
* [Source Code](http://github.com/mongoid/mongoid-cached-json)
|
|
21
|
+
* [Upgrading](UPGRADING.md)
|
|
24
22
|
|
|
25
23
|
Quickstart
|
|
26
24
|
----------
|
|
@@ -101,7 +99,7 @@ Defining Fields
|
|
|
101
99
|
|
|
102
100
|
`Mongoid::CachedJson` supports the following options:
|
|
103
101
|
|
|
104
|
-
* `: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.
|
|
105
103
|
|
|
106
104
|
`Mongoid::CachedJson` field definitions support the following options:
|
|
107
105
|
|
|
@@ -110,8 +108,66 @@ Defining Fields
|
|
|
110
108
|
* `:properties` can be one of `:short`, `:public`, `:all`, in this order
|
|
111
109
|
* `:version` can be a single version for this field to appear in
|
|
112
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)
|
|
113
112
|
* `:reference_properties` can be one of `:short`, `:public`, `:all`, default will select the reference properties format dynamically (see below)
|
|
114
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
|
+
|
|
115
171
|
Reference Properties
|
|
116
172
|
--------------------
|
|
117
173
|
|
|
@@ -119,7 +175,7 @@ When calling `as_json` on a model that contains references to other models the v
|
|
|
119
175
|
|
|
120
176
|
* Use the value of the `:reference_properties` option, if specified.
|
|
121
177
|
* For `:short` JSON, use `:short`.
|
|
122
|
-
* For `:public` JSON, use `:
|
|
178
|
+
* For `:public` JSON, use `:public`.
|
|
123
179
|
* For `:all` JSON, use `:all`.
|
|
124
180
|
|
|
125
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.
|
|
@@ -177,6 +233,13 @@ person.as_json(version: :v2) # { first: 'John', middle: 'F.', last: 'Kennedy', n
|
|
|
177
233
|
person.as_json(version: :v3) # { first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy', born: 'May 29, 1917' }
|
|
178
234
|
```
|
|
179
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
|
+
|
|
180
243
|
Transformations
|
|
181
244
|
---------------
|
|
182
245
|
|
|
@@ -213,6 +276,28 @@ Turning Caching Off
|
|
|
213
276
|
|
|
214
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).
|
|
215
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
|
+
|
|
216
301
|
Testing JSON
|
|
217
302
|
------------
|
|
218
303
|
|
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
|
|
@@ -151,13 +191,11 @@ module Mongoid
|
|
|
151
191
|
reference_def_definition = reference_def[:definition]
|
|
152
192
|
reference = reference_def_definition.is_a?(Symbol) ? object.send(reference_def_definition) : reference_def_definition.call(object)
|
|
153
193
|
reference_json = nil
|
|
154
|
-
if reference
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
reference_json = reference.as_json(options)
|
|
160
|
-
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)
|
|
161
199
|
end
|
|
162
200
|
end
|
|
163
201
|
[keys, reference_json]
|
|
@@ -166,8 +204,10 @@ module Mongoid
|
|
|
166
204
|
|
|
167
205
|
# Check whether the cache supports :read_multi and prefetch the data if it does.
|
|
168
206
|
def self.materialize_json_references_with_read_multi(key_refs, partial_json)
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
171
211
|
local_cache = read_multi ? Mongoid::CachedJson.config.cache.read_multi(*unfrozen_keys) : {}
|
|
172
212
|
Mongoid::CachedJson.materialize_json_references(key_refs, local_cache, read_multi) if key_refs
|
|
173
213
|
partial_json
|
|
@@ -180,7 +220,9 @@ module Mongoid
|
|
|
180
220
|
_ref = ref.delete(:_ref)
|
|
181
221
|
key = _ref[:_key]
|
|
182
222
|
fetched_json = local_cache[key] if local_cache.key?(key)
|
|
183
|
-
|
|
223
|
+
if _ref[:_disable_caching]
|
|
224
|
+
fetched_json = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json])
|
|
225
|
+
elsif !fetched_json
|
|
184
226
|
if read_multi
|
|
185
227
|
# no value in cache, materialize and write
|
|
186
228
|
fetched_json = (local_cache[key] = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json]))
|
|
@@ -192,8 +234,11 @@ module Mongoid
|
|
|
192
234
|
end)
|
|
193
235
|
end
|
|
194
236
|
end
|
|
195
|
-
if fetched_json
|
|
237
|
+
if fetched_json == HIDDEN_JSON
|
|
238
|
+
_ref[:_parent].delete(_ref[:_field]) if _ref[:_parent]
|
|
239
|
+
elsif fetched_json
|
|
196
240
|
ref.merge! fetched_json
|
|
241
|
+
ref.merge! _ref[:_clazz].materialize_uncached_json(* _ref[:_materialize_cached_json])
|
|
197
242
|
elsif _ref[:_parent]
|
|
198
243
|
# a single _ref that resolved to a nil
|
|
199
244
|
_ref[:_parent][_ref[:_field]] = nil
|