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.
data/CHANGELOG.md CHANGED
@@ -1,103 +1,97 @@
1
- 1.6.1 (Next)
2
- ------------
1
+ ### 1.7.1 (Next)
3
2
 
4
3
  * Your contribution here.
5
4
 
6
- 1.6.0 (2019/05/31)
7
- ------------
5
+ ### 1.7.0 (2026/10/16)
8
6
 
9
- * [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): Compatibility with Ruby 2.3, 2.4, 2.5 and 2.6 - [@yuki24](http://github.com/yuki24).
10
- * [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): Compatibility with Mongoid 6 - [@yuki24](http://github.com/yuki24).
11
- * [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): No longer test against Ruby 1.9.3 - [@yuki24](http://github.com/yuki24).
12
- * [#22](https://github.com/mongoid/mongoid-cached-json/pull/22): Compatibility with Mongoid 7 - [@yuki24](https://github.com/yuki24)
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.5.3 (2015/09/17)
15
- ------------------
18
+ ### 1.6.0 (2019/05/31)
16
19
 
17
- * Compatibility with Mongoid 5 - [@dblock](http://github.com/dblock).
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.2 (2014/29/12)
20
- ------------------
25
+ ### 1.5.3 (2015/09/17)
21
26
 
22
- * Fixed support for Ruby 2.2.0 - [@dblock](http://github.com/dblock).
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.1 (2013/05/07)
28
- --------------------
29
+ ### 1.5.2 (2014/12/29)
29
30
 
30
- * Fixed `read_multi` calls so as to enable proper cache reading behavior in stores other than `ActiveSupport::Cache::DalliStore` - [@macreery](http://github.com/macreery).
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/04/13)
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
- 1.4.1 (2013/01/22)
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
- * Invalidate cache in `after_destroy` - [@dblock](http://github.com/dblock).
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
- 1.4 (2013/01/20)
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
- * Collect a JSON partial representation first, then fetch data from cache only once per-key - [@dblock](http://github.com/dblock), [@macreery](http://github.com/macreery).
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
- 1.3 (2012/11/12)
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
- * Removed requirement for `bson_ext`, support for Mongoid 3.0 - [@dblock](http://github.com/dblock).
64
+ ### 1.3.0 (2012/11/12)
65
65
 
66
- 1.2.3 (2012/07/03)
67
- ------------------
66
+ * Removed requirement for `bson_ext`, support for Mongoid 3.0 - [@dblock](https://github.com/dblock).
68
67
 
69
- * 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](http://github.com/dblock).
68
+ ### 1.2.3 (2012/07/03)
70
69
 
71
- 1.2.2 (2012/07/03)
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
- * 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](http://github.com/dblock).
72
+ ### 1.2.2 (2012/07/03)
75
73
 
76
- 1.2.1 (2012/06/12)
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
- * Allow `nil` parameter in as_json - [@dblock](http://github.com/dblock).
76
+ ### 1.2.1 (2012/06/12)
80
77
 
81
- 1.2.0 (2012/05/28)
82
- ------------------
78
+ * Allow `nil` parameter in as_json - [@dblock](https://github.com/dblock).
83
79
 
84
- * Fix: cache key generation bug when using Mongoid 3 - [@marbemac](http://github.com/marbemac).
80
+ ### 1.2.0 (2012/05/28)
85
81
 
86
- 1.1.1 (2012/03/21)
87
- ------------------
82
+ * Fix: cache key generation bug when using Mongoid 3 - [@marbemac](https://github.com/marbemac).
88
83
 
89
- * Fix: caching/invalidating referenced polymorphic documents - [@macreery](http://github.com/macreery).
84
+ ### 1.1.1 (2012/03/21)
90
85
 
91
- 1.1 (2012/02/29)
92
- ----------------
86
+ * Fix: caching/invalidating referenced polymorphic documents - [@macreery](https://github.com/macreery).
93
87
 
94
- * Added support for versioning - [@dblock](http://github.com/dblock).
88
+ ### 1.1.0 (2012/02/29)
95
89
 
96
- 1.0 (2012/02/20)
97
- ----------------
90
+ * Added support for versioning - [@dblock](https://github.com/dblock).
98
91
 
99
- * Initial release - [@aaw](http://github.com/aaw).
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ danger.import_dangerfile(gem: 'danger-pr-comment')
4
+
5
+ changelog.check!
data/Gemfile CHANGED
@@ -1,30 +1,27 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- case version = ENV['MONGOID_VERSION'] || '~> 5.0'
6
- when /7/
7
- gem 'mongoid', '~> 7.0'
8
- when /6/
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
- gem 'dalli', '~> 2.6'
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 'rubocop'
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
  [![Gem Version](https://badge.fury.io/rb/mongoid-cached-json.svg)](http://badge.fury.io/rb/mongoid-cached-json)
5
- [![Build Status](https://travis-ci.org/mongoid/mongoid-cached-json.svg?branch=master)](https://travis-ci.org/mongoid/mongoid-cached-json)
6
- [![Code Climate](https://codeclimate.com/github/mongoid/mongoid-cached-json.svg)](https://codeclimate.com/github/mongoid/mongoid-cached-json)
5
+ [![Test](https://github.com/mongoid/mongoid-cached-json/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/mongoid/mongoid-cached-json/actions/workflows/test.yml?query=branch%3Amaster)
6
+ [![Coverage Status](https://coveralls.io/repos/github/mongoid/mongoid-cached-json/badge.svg?branch=master)](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 hides the child JSON from `as_json` parent objects, eg. `cached_json hide_as_child_json_when: lambda { |instance| ! instance.secret? }`
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 `:short`.
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 || (!is_top_level_json && options[:properties] != :all && clazz.hide_as_child_json_when.call(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, _materialize_cached_json: [clazz, id, object_reference, options] } }
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
- if reference.respond_to?(:as_json_partial)
156
- reference_keys, reference_json = reference.as_json_partial(options)
157
- keys = keys ? keys.merge_set(reference_keys) : reference_keys
158
- else
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
- unfrozen_keys = key_refs.keys.to_a.map(&:dup) if key_refs # see https://github.com/mperham/dalli/pull/320
170
- read_multi = unfrozen_keys && Mongoid::CachedJson.config.cache.respond_to?(:read_multi)
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
- unless fetched_json
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
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module CachedJson
3
- VERSION = '1.6.0'
3
+ VERSION = '1.7.0'
4
4
  end
5
5
  end