mongoid-cached-json 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: