mongoid-cached-json 1.3.0 → 1.4.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/README.md CHANGED
@@ -10,9 +10,8 @@ Using `Mongoid::CachedJson` we were able to cut our JSON API average response ti
10
10
  Resources
11
11
  ---------
12
12
 
13
- * [Need Help?](http://groups.google.com/group/mongoid-cached-json)
13
+ * [Need Help? Google Group](http://groups.google.com/group/mongoid-cached-json)
14
14
  * [Source Code](http://github.com/dblock/mongoid-cached-json)
15
- * [Travis CI](https://secure.travis-ci.org/dblock/mongoid-cached-json)
16
15
 
17
16
  Quickstart
18
17
  ----------
@@ -231,10 +230,26 @@ describe "updating a person" do
231
230
  end
232
231
  ```
233
232
 
233
+ Performance
234
+ -----------
235
+
236
+ This implements two interesting optimizations.
237
+
238
+ ### Bulk Reference Resolving w/ Local Store
239
+
240
+ Consider an array of Mongoid instances, each with numerous references to other objects. It's typical to see such instances reference the same object. `Mongoid::CachedJson` first collects all JSON references, then resolves them after suppressing duplicates. This significantly reduces the number of cache queries.
241
+
242
+ ### Fetching Cache Data in Bulk
243
+
244
+ Various cache stores, including Memcached, support bulk read operations. The [Dalli](https://github.com/mperham/dalli) gem exposes this via the `read_multi` method. `Mongoid::CachedJson` will always invoke `read_multi` where available, which significantly reduces the number of network roundtrips to the cache servers.
245
+
234
246
  Contributing
235
247
  ------------
236
248
 
237
- Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
249
+ * Fork the project.
250
+ * Make your feature addition or bug fix with tests.
251
+ * Don't forget to update `CHANGELOG.md`.
252
+ * Send a pull request. Bonus points for topic branches.
238
253
 
239
254
  Copyright and License
240
255
  ---------------------
@@ -1,6 +1,10 @@
1
1
  # encoding: utf-8
2
- require 'mongoid'
3
2
  require 'active_support/concern'
3
+ require 'mongoid'
4
+ require 'mongoid-cached-json/key_references'
5
+ require 'mongoid-cached-json/array'
6
+ require 'mongoid-cached-json/hash'
7
+ require 'mongoid-cached-json/mongoid_criteria'
4
8
  require 'mongoid-cached-json/version'
5
9
  require 'mongoid-cached-json/config'
6
10
  require 'mongoid-cached-json/cached_json'
@@ -0,0 +1,22 @@
1
+ class Array
2
+
3
+ def as_json_partial(options = {})
4
+ json_keys = nil
5
+ json = map do |i|
6
+ if i.respond_to?(:as_json_partial)
7
+ partial_json_keys, json = i.as_json_partial(options)
8
+ json_keys = json_keys ? json_keys.merge_set(partial_json_keys) : partial_json_keys
9
+ json
10
+ else
11
+ i.as_json(options)
12
+ end
13
+ end
14
+ [ json_keys, json ]
15
+ end
16
+
17
+ def as_json(options = {})
18
+ json_keys, json = as_json_partial(options)
19
+ Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json)
20
+ end
21
+
22
+ end
@@ -2,7 +2,7 @@
2
2
  module Mongoid
3
3
  module CachedJson
4
4
  extend ActiveSupport::Concern
5
-
5
+
6
6
  included do
7
7
  class_attribute :all_json_properties
8
8
  class_attribute :all_json_versions
@@ -10,9 +10,9 @@ module Mongoid
10
10
  class_attribute :cached_json_reference_defs
11
11
  class_attribute :hide_as_child_json_when
12
12
  end
13
-
13
+
14
14
  module ClassMethods
15
-
15
+
16
16
  # Define JSON fields for a class.
17
17
  #
18
18
  # @param [ hash ] defs JSON field definition.
@@ -44,7 +44,27 @@ module Mongoid
44
44
  end
45
45
  before_save :expire_cached_json
46
46
  end
47
-
47
+
48
+ # Materialize a cached JSON within a cache block.
49
+ def materialize_cached_json(clazz, id, object_reference, options)
50
+ is_top_level_json = options[:is_top_level_json] || false
51
+ object_reference = clazz.where({ :_id => id }).first if !object_reference
52
+ if !object_reference || (!is_top_level_json && options[:properties] != :all && clazz.hide_as_child_json_when.call(object_reference))
53
+ nil
54
+ else
55
+ Hash[clazz.cached_json_field_defs[options[:properties]].map do |field, definition|
56
+ # version match
57
+ versions = ([definition[:version] ] | Array(definition[:versions])).compact
58
+ next unless versions.empty? or versions.include?(options[:version])
59
+ json_value = (definition[:definition].is_a?(Symbol) ? object_reference.send(definition[:definition]) : definition[:definition].call(object_reference))
60
+ Mongoid::CachedJson.config.transform.each do |t|
61
+ json_value = t.call(field, definition, json_value)
62
+ end
63
+ [field, json_value]
64
+ end]
65
+ end
66
+ end
67
+
48
68
  # Given an object definition in the form of either an object or a class, id pair,
49
69
  # grab the as_json representation from the cache if possible, otherwise create
50
70
  # the as_json representation by loading the object from the database. For any
@@ -61,36 +81,29 @@ module Mongoid
61
81
  object_reference = nil
62
82
  clazz, id = object_def[:clazz], object_def[:id]
63
83
  end
64
- json = Mongoid::CachedJson.config.cache.fetch(self.cached_json_key(options, clazz, id), { :force => !! Mongoid::CachedJson.config.disable_caching }) do
65
- object_reference = clazz.where({ :_id => id }).first if !object_reference
66
- if !object_reference or (!is_top_level_json and options[:properties] != :all and clazz.hide_as_child_json_when.call(object_reference))
67
- nil
68
- else
69
- Hash[clazz.cached_json_field_defs[options[:properties]].map do |field, definition|
70
- # version match
71
- versions = ([definition[:version] ] | Array(definition[:versions])).compact
72
- next unless versions.empty? or versions.include?(options[:version])
73
- json_value = (definition[:definition].is_a?(Symbol) ? object_reference.send(definition[:definition]) : definition[:definition].call(object_reference))
74
- Mongoid::CachedJson.config.transform.each do |t|
75
- json_value = t.call(field, definition, json_value)
76
- end
77
- [field, json_value]
78
- end]
79
- end
80
- end
84
+ key = self.cached_json_key(options, clazz, id)
85
+ json = { :_ref => { :_clazz => self, :_key => key, :_materialize_cached_json => [ clazz, id, object_reference, options ] }}
86
+ keys = KeyReferences.new
87
+ keys.set_and_add(key, json)
81
88
  reference_defs = clazz.cached_json_reference_defs[options[:properties]]
82
89
  if !reference_defs.empty?
83
90
  object_reference = clazz.where({ :_id => id }).first if !object_reference
84
91
  if object_reference and (is_top_level_json or options[:properties] == :all or !clazz.hide_as_child_json_when.call(object_reference))
85
- json = json.merge(Hash[reference_defs.map do |field, definition|
92
+ json.merge!(Hash[reference_defs.map do |field, definition|
86
93
  json_properties_type = (options[:properties] == :all) ? :all : :short
87
- [field, clazz.resolve_json_reference(options.merge({ :properties => json_properties_type, :is_top_level_json => false}), object_reference, field, definition)]
94
+ reference_keys, reference = clazz.resolve_json_reference(options.merge({ :properties => json_properties_type, :is_top_level_json => false}), object_reference, field, definition)
95
+ if (reference.is_a?(Hash) && ref = reference[:_ref])
96
+ ref[:_parent] = json
97
+ ref[:_field] = field
98
+ end
99
+ keys.merge_set(reference_keys)
100
+ [field, reference]
88
101
  end])
89
102
  end
90
103
  end
91
- json
104
+ [ keys, json ]
92
105
  end
93
-
106
+
94
107
  # Cache key.
95
108
  def cached_json_key(options, cached_class, cached_id)
96
109
  base_class_name = cached_class.collection_name.to_s.singularize.camelize
@@ -102,6 +115,7 @@ module Mongoid
102
115
  # be able to load the as_json representation from the cache without even getting the
103
116
  # model from the database and materializing it through Mongoid. We'll try to do this first.
104
117
  def resolve_json_reference(options, object, field, reference_def)
118
+ keys = nil
105
119
  reference_json = nil
106
120
  if reference_def[:metadata]
107
121
  key = reference_def[:metadata].key.to_sym
@@ -112,10 +126,10 @@ module Mongoid
112
126
  end
113
127
  if reference_def[:metadata].relation == Mongoid::Relations::Referenced::ManyToMany
114
128
  reference_json = object.send(key).map do |id|
115
- materialize_json(options, { :clazz => clazz, :id => id })
129
+ materialize_keys, json = materialize_json(options, { :clazz => clazz, :id => id })
130
+ keys = keys ? keys.merge_set(materialize_keys) : materialize_keys
131
+ json
116
132
  end.compact
117
- elsif reference_def[:metadata].relation == Mongoid::Relations::Referenced::In
118
- reference_json = materialize_json(options, { :clazz => clazz, :id => object.send(key) })
119
133
  end
120
134
  end
121
135
  # If we get to this point and reference_json is still nil, there's no chance we can
@@ -123,23 +137,73 @@ module Mongoid
123
137
  if ! reference_json
124
138
  reference_def_definition = reference_def[:definition]
125
139
  reference = reference_def_definition.is_a?(Symbol) ? object.send(reference_def_definition) : reference_def_definition.call(object)
126
- reference_json = reference.as_json(options) if reference
140
+ reference_json = nil
141
+ if reference
142
+ if reference.respond_to?(:as_json_partial)
143
+ reference_keys, reference_json = reference.as_json_partial(options)
144
+ keys = keys ? keys.merge_set(reference_keys) : reference_keys
145
+ else
146
+ reference_json = reference.as_json(options)
147
+ end
148
+ end
127
149
  end
128
- reference_json
150
+ [ keys, reference_json ]
129
151
  end
130
-
152
+
131
153
  end
132
-
133
- def as_json(options = {})
154
+
155
+ # Check whether the cache supports :read_multi and prefetch the data if it does.
156
+ def self.materialize_json_references_with_read_multi(key_refs, partial_json)
157
+ unfrozen_keys = key_refs.keys.to_a.map(&:dup) if key_refs # see https://github.com/mperham/dalli/pull/320
158
+ local_cache = unfrozen_keys && Mongoid::CachedJson.config.cache.respond_to?(:read_multi) ? Mongoid::CachedJson.config.cache.read_multi(unfrozen_keys) : {}
159
+ Mongoid::CachedJson.materialize_json_references(key_refs, local_cache) if key_refs
160
+ partial_json
161
+ end
162
+
163
+ # Materialize all the JSON references in place.
164
+ def self.materialize_json_references(key_refs, local_cache = {})
165
+ key_refs.each_pair do |key, refs|
166
+ refs.each do |ref|
167
+ _ref = ref.delete(:_ref)
168
+ key = _ref[:_key]
169
+ fetched_json = local_cache[key] if local_cache.has_key?(key)
170
+ fetched_json ||= (local_cache[key] = Mongoid::CachedJson.config.cache.fetch(key, { :force => !! Mongoid::CachedJson.config.disable_caching }) do
171
+ _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json])
172
+ end)
173
+ if fetched_json
174
+ ref.merge! fetched_json
175
+ elsif _ref[:_parent]
176
+ # a single _ref that resolved to a nil
177
+ _ref[:_parent][_ref[:_field]] = nil
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ # Return a partial JSON without resolved references and all the keys.
184
+ def as_json_partial(options = {})
134
185
  options ||= {}
135
186
  if options[:properties] and ! self.all_json_properties.member?(options[:properties])
136
187
  raise ArgumentError.new("Unknown properties option: #{options[:properties]}")
137
188
  end
138
- self.class.materialize_json({
189
+ # partial, unmaterialized JSON
190
+ keys, partial_json = self.class.materialize_json({
139
191
  :properties => :short, :is_top_level_json => true, :version => Mongoid::CachedJson.config.default_version
140
192
  }.merge(options), { :object => self })
193
+ [ keys, partial_json ]
141
194
  end
142
-
195
+
196
+ # Fetch the partial JSON and materialize all JSON references.
197
+ def as_json_cached(options = {})
198
+ keys, json = as_json_partial(options)
199
+ Mongoid::CachedJson.materialize_json_references_with_read_multi(keys, json)
200
+ end
201
+
202
+ # Return the JSON representation of the object.
203
+ def as_json(options = {})
204
+ as_json_cached(options)
205
+ end
206
+
143
207
  # Expire all JSON entries for this class.
144
208
  def expire_cached_json
145
209
  self.all_json_properties.each do |properties|
@@ -152,9 +216,9 @@ module Mongoid
152
216
  end
153
217
  end
154
218
  end
155
-
219
+
156
220
  class << self
157
-
221
+
158
222
  # Set the configuration options. Best used by passing a block.
159
223
  #
160
224
  # @example Set up configuration options.
@@ -168,6 +232,6 @@ module Mongoid
168
232
  end
169
233
  alias :config :configure
170
234
  end
171
-
235
+
172
236
  end
173
237
  end
@@ -0,0 +1,23 @@
1
+ class Hash
2
+
3
+ def as_json_partial(options = {})
4
+ json_keys = nil
5
+ json = inject({}) do |h, (k, v)|
6
+ if v.respond_to?(:as_json_partial)
7
+ partial_json_keys, partial_json = v.as_json_partial(options)
8
+ json_keys = json_keys ? json_keys.merge_set(partial_json_keys) : partial_json_keys
9
+ h[k] = partial_json
10
+ else
11
+ h[k] = v.as_json(options)
12
+ end
13
+ h
14
+ end
15
+ [ json_keys, json ]
16
+ end
17
+
18
+ def as_json(options = {})
19
+ json_keys, json = as_json_partial(options)
20
+ Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json)
21
+ end
22
+
23
+ end
@@ -0,0 +1,26 @@
1
+ # Keep key references to be replaced once the entire JSON is available.
2
+ # encoding: utf-8
3
+ module Mongoid
4
+ module CachedJson
5
+ class KeyReferences < Hash
6
+
7
+ def merge_set(keys)
8
+ if keys
9
+ keys.each_pair do |k, jsons|
10
+ self[k] ||= []
11
+ self[k].concat(jsons)
12
+ end
13
+ end
14
+ self
15
+ end
16
+
17
+ def set_and_add(key, json)
18
+ self[key] ||= []
19
+ self[key] << json
20
+ self
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,24 @@
1
+ module Mongoid
2
+ class Criteria
3
+
4
+ def as_json_partial(options = {})
5
+ json_keys = nil
6
+ json = map do |i|
7
+ if i.respond_to?(:as_json_partial)
8
+ partial_json_keys, partial_json = i.as_json_partial(options)
9
+ json_keys = json_keys ? json_keys.merge_set(partial_json_keys) : partial_json_keys
10
+ partial_json
11
+ else
12
+ i.as_json(options)
13
+ end
14
+ end
15
+ [ json_keys, json ]
16
+ end
17
+
18
+ def as_json(options = {})
19
+ json_keys, json = as_json_partial(options)
20
+ Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json)
21
+ end
22
+
23
+ end
24
+ end
@@ -1,6 +1,6 @@
1
1
  module Mongoid
2
2
  module CachedJson
3
- VERSION = '1.3.0'
3
+ VERSION = '1.4.0'
4
4
  end
5
5
  end
6
6
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-cached-json
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-11-12 00:00:00.000000000 Z
14
+ date: 2013-01-20 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -125,6 +125,22 @@ dependencies:
125
125
  - - ~>
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0.6'
128
+ - !ruby/object:Gem::Dependency
129
+ name: dalli
130
+ requirement: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ~>
134
+ - !ruby/object:Gem::Version
135
+ version: '2.6'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ~>
142
+ - !ruby/object:Gem::Version
143
+ version: '2.6'
128
144
  description: Cached-json is a DSL for describing JSON representations of Mongoid models.
129
145
  email: dblock@dblock.org
130
146
  executables: []
@@ -134,8 +150,12 @@ extra_rdoc_files:
134
150
  - README.md
135
151
  files:
136
152
  - lib/mongoid-cached-json.rb
153
+ - lib/mongoid-cached-json/array.rb
137
154
  - lib/mongoid-cached-json/cached_json.rb
138
155
  - lib/mongoid-cached-json/config.rb
156
+ - lib/mongoid-cached-json/hash.rb
157
+ - lib/mongoid-cached-json/key_references.rb
158
+ - lib/mongoid-cached-json/mongoid_criteria.rb
139
159
  - lib/mongoid-cached-json/version.rb
140
160
  - LICENSE.md
141
161
  - README.md
@@ -154,7 +174,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
154
174
  version: '0'
155
175
  segments:
156
176
  - 0
157
- hash: 1107312364045411622
177
+ hash: -1533991509568558766
158
178
  required_rubygems_version: !ruby/object:Gem::Requirement
159
179
  none: false
160
180
  requirements: