forked from mongodb/mongoid
/
criteria.rb
369 lines (347 loc) · 12 KB
/
criteria.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
module Mongoid #:nodoc:
# The +Criteria+ class is the core object needed in Mongoid to retrieve
# objects from the database. It is a DSL that essentially sets up the
# selector and options arguments that get passed on to a <tt>Mongo::Collection</tt>
# in the Ruby driver. Each method on the +Criteria+ returns self to they
# can be chained in order to create a readable criterion to be executed
# against the database.
#
# Example setup:
#
# <tt>criteria = Criteria.new</tt>
#
# <tt>criteria.select(:field => "value").only(:field).skip(20).limit(20)</tt>
#
# <tt>criteria.execute</tt>
class Criteria
attr_accessor :klass
attr_reader :selector, :options, :type
AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }"
# Aggregate the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with counts.
#
# Example:
#
# <tt>criteria.select(:field1).where(:field1 => "Title").aggregate(Person)</tt>
def aggregate(klass = nil)
@klass = klass if klass
@klass.collection.group(@options[:fields], @selector, { :count => 0 }, AGGREGATE_REDUCE)
end
# Adds a criterion to the +Criteria+ that specifies values that must all
# be matched in order to return results. Similar to an "in" clause but the
# underlying conditional logic is an "AND" and not an "OR". The MongoDB
# conditional operator that will be used is "$all".
#
# Options:
#
# selections: A +Hash+ where the key is the field name and the value is an
# +Array+ of values that must all match.
#
# Example:
#
# <tt>criteria.all(:field => ["value1", "value2"])</tt>
#
# <tt>criteria.all(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
#
# Returns: <tt>self</tt>
def all(selections = {})
selections.each { |key, value| @selector[key] = { "$all" => value } }; self
end
# Get the count of matching documents in the database for the +Criteria+.
#
# Options:
#
# klass: Optional class that the collection will be retrieved from.
#
# Example:
#
# <tt>criteria.count</tt>
#
# Returns: <tt>Integer</tt>
def count(klass = nil)
return @count if @count
@klass = klass if klass
return @klass.collection.find(@selector, @options.dup).count
end
# Adds a criterion to the +Criteria+ that specifies values that are not allowed
# to match any document in the database. The MongoDB conditional operator that
# will be used is "$ne".
#
# Options:
#
# excludes: A +Hash+ where the key is the field name and the value is a
# value that must not be equal to the corresponding field value in the database.
#
# Example:
#
# <tt>criteria.excludes(:field => "value1")</tt>
#
# <tt>criteria.excludes(:field1 => "value1", :field2 => "value1")</tt>
#
# Returns: <tt>self</tt>
def excludes(exclusions = {})
exclusions.each { |key, value| @selector[key] = { "$ne" => value } }; self
end
# Execute the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +find()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned new documents of the type of class provided will be instantiated.
#
# If this is a +Criteria+ to only find the first object, this will return a
# single object of the type of class provided.
#
# If this is a +Criteria+ to find multiple results, will return an +Array+ of
# objects of the type of class provided.
def execute(klass = nil)
@klass = klass if klass
if type == :first
attributes = klass.collection.find_one(@selector, @options.dup)
return attributes ? @klass.instantiate(attributes) : nil
else
attributes = @klass.collection.find(@selector, @options.dup)
if attributes
@count = attributes.count
return attributes.collect { |doc| @klass.instantiate(doc) }
else
return []
end
end
end
# Adds a criterion to the +Criteria+ that specifies additional options
# to be passed to the Ruby driver, in the exact format for the driver.
#
# Options:
#
# extras: A +Hash+ that gets set to the driver options.
#
# Example:
#
# <tt>criteria.extras(:limit => 20, :skip => 40)</tt>
#
# Returns: <tt>self</tt>
def extras(extras)
@options = extras
filter_options
self
end
GROUP_REDUCE = "function(obj, prev) { prev.group.push(obj); }"
# Groups the criteria. This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
# query has returned it will provided a grouping of keys with objects.
#
# Example:
#
# <tt>criteria.select(:field1).where(:field1 => "Title").group(Person)</tt>
def group(klass = nil)
@klass = klass if klass
@klass.collection.group(
@options[:fields],
@selector,
{ :group => [] },
GROUP_REDUCE
).collect do |docs|
docs["group"] = docs["group"].collect { |attrs| @klass.instantiate(attrs) }; docs
end
end
# Adds a criterion to the +Criteria+ that specifies values where any can
# be matched in order to return results. This is similar to an SQL "IN"
# clause. The MongoDB conditional operator that will be used is "$in".
#
# Options:
#
# inclusions: A +Hash+ where the key is the field name and the value is an
# +Array+ of values that any can match.
#
# Example:
#
# <tt>criteria.in(:field => ["value1", "value2"])</tt>
#
# <tt>criteria.in(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
#
# Returns: <tt>self</tt>
def in(inclusions = {})
inclusions.each { |key, value| @selector[key] = { "$in" => value } }; self
end
# Adds a criterion to the +Criteria+ that specifies an id that must be matched.
#
# Options:
#
# object_id: A +String+ representation of a <tt>Mongo::ObjectID</tt>
#
# Example:
#
# <tt>criteria.id("4ab2bc4b8ad548971900005c")</tt>
#
# Returns: <tt>self</tt>
def id(object_id)
@selector[:_id] = object_id; self
end
# Create the new +Criteria+ object. This will initialize the selector
# and options hashes, as well as the type of criteria.
#
# Options:
#
# type: One of :all, :first:, or :last
# klass: The class to execute on.
def initialize(type, klass = nil)
@selector, @options, @type, @klass = {}, {}, type, klass
end
# Adds a criterion to the +Criteria+ that specifies the maximum number of
# results to return. This is mostly used in conjunction with <tt>skip()</tt>
# to handle paginated results.
#
# Options:
#
# value: An +Integer+ specifying the max number of results. Defaults to 20.
#
# Example:
#
# <tt>criteria.limit(100)</tt>
#
# Returns: <tt>self</tt>
def limit(value = 20)
@options[:limit] = value; self
end
# Adds a criterion to the +Criteria+ that specifies values where none
# should match in order to return results. This is similar to an SQL "NOT IN"
# clause. The MongoDB conditional operator that will be used is "$nin".
#
# Options:
#
# exclusions: A +Hash+ where the key is the field name and the value is an
# +Array+ of values that none can match.
#
# Example:
#
# <tt>criteria.not_in(:field => ["value1", "value2"])</tt>
#
# <tt>criteria.not_in(:field1 => ["value1", "value2"], :field2 => ["value1"])</tt>
#
# Returns: <tt>self</tt>
def not_in(exclusions)
exclusions.each { |key, value| @selector[key] = { "$nin" => value } }; self
end
# Returns the offset option. If a per_page option is in the list then it
# will replace it with a skip parameter and return the same value. Defaults
# to 20 if nothing was provided.
def offset
@options[:skip]
end
# Adds a criterion to the +Criteria+ that specifies the sort order of
# the returned documents in the database. Similar to a SQL "ORDER BY".
#
# Options:
#
# params: An +Array+ of [field, direction] sorting pairs.
#
# Example:
#
# <tt>criteria.order_by([[:field1, :asc], [:field2, :desc]])</tt>
#
# Returns: <tt>self</tt>
def order_by(params = [])
@options[:sort] = params; self
end
# Either returns the page option and removes it from the options, or
# returns a default value of 1.
def page
if @options[:skip] && @options[:limit]
(@options[:skip].to_i + @options[:limit].to_i) / @options[:limit].to_i
else
1
end
end
# Returns the number of results per page or the default of 20.
def per_page
(@options[:limit] || 20).to_i
end
# Adds a criterion to the +Criteria+ that specifies the fields that will
# get returned from the Document. Used mainly for list views that do not
# require all fields to be present. This is similar to SQL "SELECT" values.
#
# Options:
#
# args: A list of field names to retrict the returned fields to.
#
# Example:
#
# <tt>criteria.select(:field1, :field2, :field3)</tt>
#
# Returns: <tt>self</tt>
def select(*args)
@options[:fields] = args.flatten if args.any?; self
end
# Adds a criterion to the +Criteria+ that specifies how many results to skip
# when returning Documents. This is mostly used in conjunction with
# <tt>limit()</tt> to handle paginated results, and is similar to the
# traditional "offset" parameter.
#
# Options:
#
# value: An +Integer+ specifying the number of results to skip. Defaults to 0.
#
# Example:
#
# <tt>criteria.skip(20)</tt>
#
# Returns: <tt>self</tt>
def skip(value = 0)
@options[:skip] = value; self
end
# Translate the supplied arguments into a +Criteria+ object.
#
# If the passed in args is a single +String+, then it will
# construct an id +Criteria+ from it.
#
# If the passed in args are a type and a hash, then it will construct
# the +Criteria+ with the proper selector, options, and type.
#
# Options:
#
# args: either a +String+ or a +Symbol+, +Hash combination.
#
# Example:
#
# <tt>Criteria.translate("4ab2bc4b8ad548971900005c")</tt>
#
# <tt>Criteria.translate(:all, :conditions => { :field => "value"}, :limit => 20)</tt>
#
# Returns a new +Criteria+ object.
def self.translate(*args)
type = args[0] || :all
params = args[1] || {}
return new(:first).id(args[0]) unless type.is_a?(Symbol)
return new(type).where(params.delete(:conditions)).extras(params)
end
# Adds a criterion to the +Criteria+ that specifies values that must
# be matched in order to return results. This is similar to a SQL "WHERE"
# clause. This is the actual selector that will be provided to MongoDB,
# similar to the Javascript object that is used when performing a find()
# in the MongoDB console.
#
# Options:
#
# selectior: A +Hash+ that must match the attributes of the +Document+.
#
# Example:
#
# <tt>criteria.where(:field1 => "value1", :field2 => 15)</tt>
#
# Returns: <tt>self</tt>
def where(selector = {})
@selector = selector; self
end
protected
def filter_options
page_num = @options.delete(:page)
per_page_num = @options.delete(:per_page)
if (page_num || per_page_num)
@options[:limit] = (per_page_num || 20).to_i
@options[:skip] = (page_num || 1).to_i * @options[:limit] - @options[:limit]
end
end
end
end