usergrid_ironhorse 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.rvmrc +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/Rakefile +7 -0
- data/lib/extensions/hash.rb +7 -0
- data/lib/usergrid_ironhorse.rb +24 -0
- data/lib/usergrid_ironhorse/base.rb +360 -0
- data/lib/usergrid_ironhorse/query.rb +890 -0
- data/lib/usergrid_ironhorse/user_context.rb +79 -0
- data/lib/usergrid_ironhorse/version.rb +5 -0
- data/spec/spec_helper.rb +78 -0
- data/spec/spec_settings.yaml +5 -0
- data/spec/support/active_model_lint.rb +17 -0
- data/spec/usergrid_ironhorse/base_spec.rb +283 -0
- data/usergrid_ironhorse.gemspec +29 -0
- metadata +186 -0
@@ -0,0 +1,890 @@
|
|
1
|
+
# http://guides.rubyonrails.org/active_record_querying.html
|
2
|
+
|
3
|
+
module Usergrid
|
4
|
+
module Ironhorse
|
5
|
+
|
6
|
+
class Query
|
7
|
+
|
8
|
+
RecordNotFound = ActiveRecord::RecordNotFound
|
9
|
+
|
10
|
+
def initialize(model_class)
|
11
|
+
@model_class = model_class
|
12
|
+
@options = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
## Initializes new record from relation while maintaining the current
|
16
|
+
## scope.
|
17
|
+
##
|
18
|
+
## Expects arguments in the same format as +Base.new+.
|
19
|
+
##
|
20
|
+
## users = User.where(name: 'DHH')
|
21
|
+
## user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
|
22
|
+
##
|
23
|
+
## You can also pass a block to new with the new record as argument:
|
24
|
+
##
|
25
|
+
## user = users.new { |user| user.name = 'Oscar' }
|
26
|
+
## user.name # => Oscar
|
27
|
+
#def new(*args, &block)
|
28
|
+
# scoping { @model_class.new(*args, &block) }
|
29
|
+
#end
|
30
|
+
|
31
|
+
# Find by uuid or name - This can either be a specific uuid or name (1), a list of uuids
|
32
|
+
# or names (1, 5, 6), or an array of uuids or names ([5, 6, 10]).
|
33
|
+
# If no record can be found for all of the listed ids, then RecordNotFound will be raised.
|
34
|
+
#
|
35
|
+
# Person.find(1) # returns the object for ID = 1
|
36
|
+
# Person.find("1") # returns the object for ID = 1
|
37
|
+
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
|
38
|
+
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
|
39
|
+
# Person.find([1]) # returns an array for the object with ID = 1
|
40
|
+
# Person.where("administrator = 1").order("created_on DESC").find(1)
|
41
|
+
#
|
42
|
+
def find(*ids)
|
43
|
+
raise RecordNotFound unless ids
|
44
|
+
ids = ids.first if ids.first.is_a? Array
|
45
|
+
@records = ids.collect { |id| find_one! id } # todo: can this be optimized in one call?
|
46
|
+
#entities = @model_class.resource[ids.join '&'].get.entities
|
47
|
+
#raise RecordNotFound unless (entities.size == ids.size)
|
48
|
+
#@records = entities.collect {|entity| @model_class.model_name.constantize.new(entity.data) }
|
49
|
+
@records.size == 1 ? @records.first : @records
|
50
|
+
end
|
51
|
+
|
52
|
+
# Finds the first record matching the specified conditions. There
|
53
|
+
# is no implied ordering so if order matters, you should specify it
|
54
|
+
# yourself.
|
55
|
+
#
|
56
|
+
# If no record is found, returns <tt>nil</tt>.
|
57
|
+
#
|
58
|
+
# Post.find_by name: 'Spartacus', rating: 4
|
59
|
+
# Post.find_by "published_at < ?", 2.weeks.ago
|
60
|
+
def find_by(*conditions)
|
61
|
+
where(*conditions).take
|
62
|
+
end
|
63
|
+
|
64
|
+
# Like <tt>find_by</tt>, except that if no record is found, raises
|
65
|
+
# an <tt>ActiveRecord::RecordNotFound</tt> error.
|
66
|
+
def find_by!(*conditions)
|
67
|
+
where(*conditions).take!
|
68
|
+
end
|
69
|
+
|
70
|
+
# Gives a record (or N records if a parameter is supplied) without any implied
|
71
|
+
# order.
|
72
|
+
#
|
73
|
+
# Person.take # returns an object fetched by SELECT * FROM people
|
74
|
+
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
|
75
|
+
# Person.where(["name LIKE '%?'", name]).take
|
76
|
+
def take(limit=1)
|
77
|
+
limit(limit).to_a
|
78
|
+
end
|
79
|
+
|
80
|
+
# Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
|
81
|
+
# is found. Note that <tt>take!</tt> accepts no arguments.
|
82
|
+
def take!
|
83
|
+
take or raise RecordNotFound
|
84
|
+
end
|
85
|
+
|
86
|
+
# Find the first record (or first N records if a parameter is supplied).
|
87
|
+
# If no order is defined it will order by primary key.
|
88
|
+
#
|
89
|
+
# Person.first # returns the first object fetched by SELECT * FROM people
|
90
|
+
# Person.where(["user_name = ?", user_name]).first
|
91
|
+
# Person.where(["user_name = :u", { :u => user_name }]).first
|
92
|
+
# Person.order("created_on DESC").offset(5).first
|
93
|
+
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
|
94
|
+
def first(limit=1)
|
95
|
+
limit(limit).load.first
|
96
|
+
end
|
97
|
+
|
98
|
+
# Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
|
99
|
+
# is found. Note that <tt>first!</tt> accepts no arguments.
|
100
|
+
def first!
|
101
|
+
first or raise RecordNotFound
|
102
|
+
end
|
103
|
+
|
104
|
+
# Find the last record (or last N records if a parameter is supplied).
|
105
|
+
# If no order is defined it will order by primary key.
|
106
|
+
#
|
107
|
+
# Person.last # returns the last object fetched by SELECT * FROM people
|
108
|
+
# Person.where(["user_name = ?", user_name]).last
|
109
|
+
# Person.order("created_on DESC").offset(5).last
|
110
|
+
# Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
|
111
|
+
#
|
112
|
+
# Take note that in that last case, the results are sorted in ascending order:
|
113
|
+
#
|
114
|
+
# [#<Person id:2>, #<Person id:3>, #<Person id:4>]
|
115
|
+
#
|
116
|
+
# and not:
|
117
|
+
#
|
118
|
+
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
|
119
|
+
def last(limit=1)
|
120
|
+
limit(limit).reverse_order.load.first
|
121
|
+
end
|
122
|
+
|
123
|
+
# Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
|
124
|
+
# is found. Note that <tt>last!</tt> accepts no arguments.
|
125
|
+
def last!
|
126
|
+
last or raise RecordNotFound
|
127
|
+
end
|
128
|
+
|
129
|
+
def each
|
130
|
+
to_a.each { |*block_args| yield(*block_args) }
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns +true+ if a record exists in the table that matches the +id+ or
|
134
|
+
# conditions given, or +false+ otherwise. The argument can take six forms:
|
135
|
+
#
|
136
|
+
# * String - Finds the record with a primary key corresponding to this
|
137
|
+
# string (such as <tt>'5'</tt>).
|
138
|
+
# * Array - Finds the record that matches these +find+-style conditions
|
139
|
+
# (such as <tt>['color = ?', 'red']</tt>).
|
140
|
+
# * Hash - Finds the record that matches these +find+-style conditions
|
141
|
+
# (such as <tt>{color: 'red'}</tt>).
|
142
|
+
# * +false+ - Returns always +false+.
|
143
|
+
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
|
144
|
+
#
|
145
|
+
# For more information about specifying conditions as a Hash or Array,
|
146
|
+
# see the Conditions section in the introduction to ActiveRecord::Base.
|
147
|
+
def exists?(conditions=nil)
|
148
|
+
# todo: does not yet handle all conditions described above
|
149
|
+
case conditions
|
150
|
+
when Array, Hash
|
151
|
+
pluck :uuid
|
152
|
+
!where(conditions).take.empty?
|
153
|
+
else
|
154
|
+
!!find_one(conditions)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
alias_method :any?, :exists?
|
158
|
+
alias_method :many?, :exists?
|
159
|
+
|
160
|
+
def limit(limit=1)
|
161
|
+
@options[:limit] = limit
|
162
|
+
self
|
163
|
+
end
|
164
|
+
|
165
|
+
def offset(num)
|
166
|
+
@options[:offset] = num
|
167
|
+
self
|
168
|
+
end
|
169
|
+
|
170
|
+
# Removes from the query the condition(s) specified in +skips+.
|
171
|
+
#
|
172
|
+
# Example:
|
173
|
+
#
|
174
|
+
# Post.order('id asc').except(:order) # discards the order condition
|
175
|
+
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
|
176
|
+
#
|
177
|
+
def except(*skips)
|
178
|
+
skips.each {|option| @options.delete option}
|
179
|
+
end
|
180
|
+
|
181
|
+
# Removes any condition from the query other than the one(s) specified in +onlies+.
|
182
|
+
#
|
183
|
+
# Example:
|
184
|
+
#
|
185
|
+
# Post.order('id asc').only(:where) # discards the order condition
|
186
|
+
# Post.order('id asc').only(:where, :order) # uses the specified order
|
187
|
+
#
|
188
|
+
def only(*onlies)
|
189
|
+
@options.keys do |k|
|
190
|
+
unless onlines.include? k
|
191
|
+
@options.delete k
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Allows to specify an order attribute:
|
197
|
+
#
|
198
|
+
# User.order('name')
|
199
|
+
# => SELECT "users".* FROM "users" ORDER BY name
|
200
|
+
#
|
201
|
+
# User.order('name DESC')
|
202
|
+
# => SELECT "users".* FROM "users" ORDER BY name DESC
|
203
|
+
#
|
204
|
+
# User.order('name DESC, email')
|
205
|
+
# => SELECT "users".* FROM "users" ORDER BY name DESC, email
|
206
|
+
def order(*args)
|
207
|
+
@options[:order] << args
|
208
|
+
end
|
209
|
+
|
210
|
+
# Replaces any existing order defined on the relation with the specified order.
|
211
|
+
#
|
212
|
+
# User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
|
213
|
+
#
|
214
|
+
# Subsequent calls to order on the same relation will be appended. For example:
|
215
|
+
#
|
216
|
+
# User.order('email DESC').reorder('id ASC').order('name ASC')
|
217
|
+
#
|
218
|
+
# generates a query with 'ORDER BY name ASC, id ASC'.
|
219
|
+
def reorder(*args)
|
220
|
+
@options[:order] = args
|
221
|
+
end
|
222
|
+
|
223
|
+
def all
|
224
|
+
@options[:conditions] = nil
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
# Works in two unique ways.
|
229
|
+
#
|
230
|
+
# First: takes a block so it can be used just like Array#select.
|
231
|
+
#
|
232
|
+
# Model.all.select { |m| m.field == value }
|
233
|
+
#
|
234
|
+
# This will build an array of objects from the database for the scope,
|
235
|
+
# converting them into an array and iterating through them using Array#select.
|
236
|
+
#
|
237
|
+
# Second: Modifies the SELECT statement for the query so that only certain
|
238
|
+
# fields are retrieved:
|
239
|
+
#
|
240
|
+
# Model.select(:field)
|
241
|
+
# # => [#<Model field:value>]
|
242
|
+
#
|
243
|
+
# Although in the above example it looks as though this method returns an
|
244
|
+
# array, it actually returns a relation object and can have other query
|
245
|
+
# methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
|
246
|
+
#
|
247
|
+
# The argument to the method can also be an array of fields.
|
248
|
+
#
|
249
|
+
# Model.select(:field, :other_field, :and_one_more)
|
250
|
+
# # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
|
251
|
+
#
|
252
|
+
def select(*fields)
|
253
|
+
if block_given?
|
254
|
+
to_a.select { |*block_args| yield(*block_args) }
|
255
|
+
else
|
256
|
+
raise ArgumentError, 'Call this with at least one field' if fields.empty?
|
257
|
+
clone.select!(*fields)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Like #select, but modifies relation in place.
|
262
|
+
def select!(*fields)
|
263
|
+
@options[:select] ||= fields.join ','
|
264
|
+
self
|
265
|
+
end
|
266
|
+
alias_method :pluck, :select!
|
267
|
+
|
268
|
+
def reverse_order
|
269
|
+
@options[:reversed] = true
|
270
|
+
self
|
271
|
+
end
|
272
|
+
|
273
|
+
def readonly
|
274
|
+
@options[:readonly] = true
|
275
|
+
self
|
276
|
+
end
|
277
|
+
|
278
|
+
def to_a
|
279
|
+
load
|
280
|
+
@records
|
281
|
+
end
|
282
|
+
|
283
|
+
def as_json(options = nil) #:nodoc:
|
284
|
+
to_a.as_json(options)
|
285
|
+
end
|
286
|
+
|
287
|
+
# Returns size of the results (not size of the stored collection)
|
288
|
+
def size
|
289
|
+
loaded? ? @records.length : count
|
290
|
+
end
|
291
|
+
|
292
|
+
# true if there are no records
|
293
|
+
def empty?
|
294
|
+
return @records.empty? if loaded?
|
295
|
+
|
296
|
+
c = count
|
297
|
+
c.respond_to?(:zero?) ? c.zero? : c.empty?
|
298
|
+
end
|
299
|
+
|
300
|
+
# true if there are any records
|
301
|
+
def any?
|
302
|
+
if block_given?
|
303
|
+
to_a.any? { |*block_args| yield(*block_args) }
|
304
|
+
else
|
305
|
+
!empty?
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# true if there is more than one record
|
310
|
+
def many?
|
311
|
+
if block_given?
|
312
|
+
to_a.many? { |*block_args| yield(*block_args) }
|
313
|
+
else
|
314
|
+
limit_value ? to_a.many? : size > 1
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# find_all_by_
|
319
|
+
# find_by_
|
320
|
+
# find_first_by_
|
321
|
+
# find_last_by_
|
322
|
+
def method_missing(method_name, *args)
|
323
|
+
|
324
|
+
method = method_name.to_s
|
325
|
+
if method.start_with? 'find_all_by_'
|
326
|
+
attribs = method.gsub /^find_all_by_/, ''
|
327
|
+
elsif method.start_with? 'find_by_'
|
328
|
+
attribs = method.gsub /^find_by_/, ''
|
329
|
+
limit(1)
|
330
|
+
elsif method.start_with? 'find_first_by_'
|
331
|
+
limit(1)
|
332
|
+
find_first = true
|
333
|
+
attribs = method.gsub /^find_first_by_/, ''
|
334
|
+
elsif method.start_with? 'find_last_by_'
|
335
|
+
limit(1)
|
336
|
+
find_last = true
|
337
|
+
attribs = method.gsub /^find_last_by_/, ''
|
338
|
+
else
|
339
|
+
super
|
340
|
+
end
|
341
|
+
|
342
|
+
if method.end_with? '!'
|
343
|
+
method.chop!
|
344
|
+
error_on_empty = true
|
345
|
+
end
|
346
|
+
|
347
|
+
attribs = attribs.split '_and_'
|
348
|
+
conditions = {}
|
349
|
+
attribs.each { |attr| conditions[attr] = args.shift }
|
350
|
+
|
351
|
+
where(conditions, *args)
|
352
|
+
load
|
353
|
+
raise RecordNotFound if error_on_empty && @records.empty?
|
354
|
+
return @records.first if limit_value == 1
|
355
|
+
@records
|
356
|
+
end
|
357
|
+
|
358
|
+
# Tries to create a new record with the same scoped attributes
|
359
|
+
# defined in the relation. Returns the initialized object if validation fails.
|
360
|
+
#
|
361
|
+
# Expects arguments in the same format as +Base.create+.
|
362
|
+
#
|
363
|
+
# ==== Examples
|
364
|
+
# users = User.where(name: 'Oscar')
|
365
|
+
# users.create # #<User id: 3, name: "oscar", ...>
|
366
|
+
#
|
367
|
+
# users.create(name: 'fxn')
|
368
|
+
# users.create # #<User id: 4, name: "fxn", ...>
|
369
|
+
#
|
370
|
+
# users.create { |user| user.name = 'tenderlove' }
|
371
|
+
# # #<User id: 5, name: "tenderlove", ...>
|
372
|
+
#
|
373
|
+
# users.create(name: nil) # validation on name
|
374
|
+
# # #<User id: nil, name: nil, ...>
|
375
|
+
def create(*args, &block)
|
376
|
+
@model_class.create(*args, &block)
|
377
|
+
end
|
378
|
+
|
379
|
+
# Similar to #create, but calls +create!+ on the base class. Raises
|
380
|
+
# an exception if a validation error occurs.
|
381
|
+
#
|
382
|
+
# Expects arguments in the same format as <tt>Base.create!</tt>.
|
383
|
+
def create!(*args, &block)
|
384
|
+
@model_class.create!(*args, &block)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
|
388
|
+
#
|
389
|
+
# Expects arguments in the same format as +Base.create+.
|
390
|
+
#
|
391
|
+
# ==== Examples
|
392
|
+
# # Find the first user named Penélope or create a new one.
|
393
|
+
# User.where(:first_name => 'Penélope').first_or_create
|
394
|
+
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
|
395
|
+
#
|
396
|
+
# # Find the first user named Penélope or create a new one.
|
397
|
+
# # We already have one so the existing record will be returned.
|
398
|
+
# User.where(:first_name => 'Penélope').first_or_create
|
399
|
+
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
|
400
|
+
#
|
401
|
+
# # Find the first user named Scarlett or create a new one with a particular last name.
|
402
|
+
# User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
|
403
|
+
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
|
404
|
+
#
|
405
|
+
# # Find the first user named Scarlett or create a new one with a different last name.
|
406
|
+
# # We already have one so the existing record will be returned.
|
407
|
+
# User.where(:first_name => 'Scarlett').first_or_create do |user|
|
408
|
+
# user.last_name = "O'Hara"
|
409
|
+
# end
|
410
|
+
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
|
411
|
+
def first_or_create(attributes={}, &block)
|
412
|
+
result = first
|
413
|
+
unless result
|
414
|
+
attributes = @options[:hash].merge(attributes) if @options[:hash]
|
415
|
+
result = create(attributes, &block)
|
416
|
+
end
|
417
|
+
result
|
418
|
+
end
|
419
|
+
|
420
|
+
# Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
|
421
|
+
#
|
422
|
+
# Expects arguments in the same format as <tt>Base.create!</tt>.
|
423
|
+
def first_or_create!(attributes={}, &block)
|
424
|
+
result = first
|
425
|
+
unless result
|
426
|
+
attributes = @options[:hash].merge(attributes) if @options[:hash]
|
427
|
+
result = create!(attributes, &block)
|
428
|
+
end
|
429
|
+
result
|
430
|
+
end
|
431
|
+
|
432
|
+
# Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
|
433
|
+
#
|
434
|
+
# Expects arguments in the same format as <tt>Base.new</tt>.
|
435
|
+
def first_or_initialize(attributes={}, &block)
|
436
|
+
result = first
|
437
|
+
unless result
|
438
|
+
attributes = @options[:hash].merge(attributes) if @options[:hash]
|
439
|
+
result = @model_class.new(attributes, &block)
|
440
|
+
end
|
441
|
+
result
|
442
|
+
end
|
443
|
+
|
444
|
+
# Destroys the records matching +conditions+ by instantiating each
|
445
|
+
# record and calling its +destroy+ method. Each object's callbacks are
|
446
|
+
# executed (including <tt>:dependent</tt> association options and
|
447
|
+
# +before_destroy+/+after_destroy+ Observer methods). Returns the
|
448
|
+
# collection of objects that were destroyed; each will be frozen, to
|
449
|
+
# reflect that no changes should be made (since they can't be
|
450
|
+
# persisted).
|
451
|
+
#
|
452
|
+
# Note: Instantiation, callback execution, and deletion of each
|
453
|
+
# record can be time consuming when you're removing many records at
|
454
|
+
# once. It generates at least one SQL +DELETE+ query per record (or
|
455
|
+
# possibly more, to enforce your callbacks). If you want to delete many
|
456
|
+
# rows quickly, without concern for their associations or callbacks, use
|
457
|
+
# +delete_all+ instead.
|
458
|
+
#
|
459
|
+
# ==== Parameters
|
460
|
+
#
|
461
|
+
# * +conditions+ - A string, array, or hash that specifies which records
|
462
|
+
# to destroy. If omitted, all records are destroyed. See the
|
463
|
+
# Conditions section in the introduction to ActiveRecord::Base for
|
464
|
+
# more information.
|
465
|
+
#
|
466
|
+
# ==== Examples
|
467
|
+
#
|
468
|
+
# Person.destroy_all("last_login < '2004-04-04'")
|
469
|
+
# Person.destroy_all(status: "inactive")
|
470
|
+
# Person.where(:age => 0..18).destroy_all
|
471
|
+
def destroy_all(conditions=nil)
|
472
|
+
if conditions
|
473
|
+
where(conditions).destroy_all
|
474
|
+
else
|
475
|
+
to_a.each {|object| object.destroy}
|
476
|
+
@records
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
|
481
|
+
# therefore all callbacks and filters are fired off before the object is deleted. This method is
|
482
|
+
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
|
483
|
+
#
|
484
|
+
# This essentially finds the object (or multiple objects) with the given id, creates a new object
|
485
|
+
# from the attributes, and then calls destroy on it.
|
486
|
+
#
|
487
|
+
# ==== Parameters
|
488
|
+
#
|
489
|
+
# * +id+ - Can be either an Integer or an Array of Integers.
|
490
|
+
#
|
491
|
+
# ==== Examples
|
492
|
+
#
|
493
|
+
# # Destroy a single object
|
494
|
+
# Foo.destroy(1)
|
495
|
+
#
|
496
|
+
# # Destroy multiple objects
|
497
|
+
# foos = [1,2,3]
|
498
|
+
# Foo.destroy(foos)
|
499
|
+
def destroy(id)
|
500
|
+
if id.is_a?(Array)
|
501
|
+
id.map {|one_id| destroy(one_id)}
|
502
|
+
else
|
503
|
+
find(id).destroy
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# Deletes the records matching +conditions+ without instantiating the records
|
508
|
+
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
|
509
|
+
# is a single SQL DELETE statement that goes straight to the database, much more
|
510
|
+
# efficient than +destroy_all+. Be careful with relations though, in particular
|
511
|
+
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
|
512
|
+
# number of rows affected.
|
513
|
+
#
|
514
|
+
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
|
515
|
+
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
|
516
|
+
# Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
|
517
|
+
#
|
518
|
+
# Both calls delete the affected posts all at once with a single DELETE statement.
|
519
|
+
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
|
520
|
+
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
|
521
|
+
#
|
522
|
+
# If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
|
523
|
+
#
|
524
|
+
# Post.limit(100).delete_all
|
525
|
+
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
|
526
|
+
def delete_all(conditions=nil)
|
527
|
+
raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
|
528
|
+
|
529
|
+
if conditions
|
530
|
+
where(conditions).delete_all
|
531
|
+
else
|
532
|
+
pluck :uuid
|
533
|
+
response = run_query
|
534
|
+
response.entities.each {|entity| entity.delete} # todo: can this be optimized into one call?
|
535
|
+
response.entities.size
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Deletes the row with a primary key matching the +id+ argument, using a
|
540
|
+
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
|
541
|
+
# Record objects are not instantiated, so the object's callbacks are not
|
542
|
+
# executed, including any <tt>:dependent</tt> association options or
|
543
|
+
# Observer methods.
|
544
|
+
#
|
545
|
+
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
|
546
|
+
#
|
547
|
+
# Note: Although it is often much faster than the alternative,
|
548
|
+
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
|
549
|
+
# your application that ensures referential integrity or performs other
|
550
|
+
# essential jobs.
|
551
|
+
#
|
552
|
+
# ==== Examples
|
553
|
+
#
|
554
|
+
# # Delete a single row
|
555
|
+
# Foo.delete(1)
|
556
|
+
#
|
557
|
+
# # Delete multiple rows
|
558
|
+
# Foo.delete([2,3,4])
|
559
|
+
def delete(id_or_array)
|
560
|
+
if id_or_array.is_a? Array
|
561
|
+
id_or_array.each {|id| @model_class.resource[id].delete} # todo: can this be optimized into one call?
|
562
|
+
else
|
563
|
+
@model_class.resource[id_or_array].delete
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
# Updates all records with details given if they match a set of conditions supplied, limits and order can
|
568
|
+
# also be supplied. This method sends a single update straight to the database. It does not instantiate
|
569
|
+
# the involved models and it does not trigger Active Record callbacks or validations.
|
570
|
+
#
|
571
|
+
# ==== Parameters
|
572
|
+
#
|
573
|
+
# * +updates+ - hash of attribute updates
|
574
|
+
#
|
575
|
+
# ==== Examples
|
576
|
+
#
|
577
|
+
# # Update all customers with the given attributes
|
578
|
+
# Customer.update_all wants_email: true
|
579
|
+
#
|
580
|
+
# # Update all books with 'Rails' in their title
|
581
|
+
# Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
|
582
|
+
#
|
583
|
+
# # Update all books that match conditions, but limit it to 5 ordered by date
|
584
|
+
# Book.where('title LIKE ?', '%Rails%').order(:created).limit(5).update_all(:author => 'David')
|
585
|
+
def update_all(updates)
|
586
|
+
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
|
587
|
+
raise ArgumentError, "updates must be a Hash" unless updates.is_a? Hash
|
588
|
+
run_update(updates)
|
589
|
+
end
|
590
|
+
|
591
|
+
# Looping through a collection of records from the database
|
592
|
+
# (using the +all+ method, for example) is very inefficient
|
593
|
+
# since it will try to instantiate all the objects at once.
|
594
|
+
#
|
595
|
+
# In that case, batch processing methods allow you to work
|
596
|
+
# with the records in batches, thereby greatly reducing memory consumption.
|
597
|
+
#
|
598
|
+
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
|
599
|
+
# specified by the +:batch_size+ option).
|
600
|
+
#
|
601
|
+
# Person.all.find_each do |person|
|
602
|
+
# person.do_awesome_stuff
|
603
|
+
# end
|
604
|
+
#
|
605
|
+
# Person.where("age > 21").find_each do |person|
|
606
|
+
# person.party_all_night!
|
607
|
+
# end
|
608
|
+
#
|
609
|
+
# You can also pass the +:start+ option to specify
|
610
|
+
# an offset to control the starting point.
|
611
|
+
def find_each(options = {})
|
612
|
+
find_in_batches(options) do |records|
|
613
|
+
records.each { |record| yield record }
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
# Yields each batch of records that was found by the find +options+ as
|
618
|
+
# an array. The size of each batch is set by the +:batch_size+
|
619
|
+
# option; the default is 1000.
|
620
|
+
#
|
621
|
+
# You can control the starting point for the batch processing by
|
622
|
+
# supplying the +:start+ option. This is especially useful if you
|
623
|
+
# want multiple workers dealing with the same processing queue. You can
|
624
|
+
# make worker 1 handle all the records between id 0 and 10,000 and
|
625
|
+
# worker 2 handle from 10,000 and beyond (by setting the +:start+
|
626
|
+
# option on that worker).
|
627
|
+
#
|
628
|
+
# It's not possible to set the order. That is automatically set to
|
629
|
+
# ascending on the primary key ("id ASC") to make the batch ordering
|
630
|
+
# work. This also mean that this method only works with integer-based
|
631
|
+
# primary keys. You can't set the limit either, that's used to control
|
632
|
+
# the batch sizes.
|
633
|
+
#
|
634
|
+
# Person.where("age > 21").find_in_batches do |group|
|
635
|
+
# sleep(50) # Make sure it doesn't get too crowded in there!
|
636
|
+
# group.each { |person| person.party_all_night! }
|
637
|
+
# end
|
638
|
+
#
|
639
|
+
# # Let's process the next 2000 records
|
640
|
+
# Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
|
641
|
+
# group.each { |person| person.party_all_night! }
|
642
|
+
# end
|
643
|
+
def find_in_batches(options={})
|
644
|
+
options.assert_valid_keys(:start, :batch_size)
|
645
|
+
|
646
|
+
raise "Not yet implemented" # todo
|
647
|
+
|
648
|
+
start = options.delete(:start) || 0
|
649
|
+
batch_size = options.delete(:batch_size) || 1000
|
650
|
+
|
651
|
+
while records.any?
|
652
|
+
records_size = records.size
|
653
|
+
primary_key_offset = records.last.id
|
654
|
+
|
655
|
+
yield records
|
656
|
+
|
657
|
+
break if records_size < batch_size
|
658
|
+
|
659
|
+
if primary_key_offset
|
660
|
+
records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
|
661
|
+
else
|
662
|
+
raise "Primary key not included in the custom select clause"
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
|
668
|
+
# The resulting object is returned whether the object was saved successfully to the database or not.
|
669
|
+
#
|
670
|
+
# ==== Parameters
|
671
|
+
#
|
672
|
+
# * +id+ - This should be the id or an array of ids to be updated.
|
673
|
+
# * +attributes+ - This should be a hash of attributes or an array of hashes.
|
674
|
+
#
|
675
|
+
# ==== Examples
|
676
|
+
#
|
677
|
+
# # Updates one record
|
678
|
+
# Person.update(15, user_name: 'Samuel', group: 'expert')
|
679
|
+
#
|
680
|
+
# # Updates multiple records
|
681
|
+
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
|
682
|
+
# Person.update(people.keys, people.values)
|
683
|
+
def update(id, attributes)
|
684
|
+
if id.is_a?(Array)
|
685
|
+
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
|
686
|
+
else
|
687
|
+
object = find(id)
|
688
|
+
object.update_attributes(attributes)
|
689
|
+
object
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
## todo: scoping
|
694
|
+
## Scope all queries to the current scope.
|
695
|
+
##
|
696
|
+
## Comment.where(:post_id => 1).scoping do
|
697
|
+
## Comment.first # SELECT * FROM comments WHERE post_id = 1
|
698
|
+
## end
|
699
|
+
##
|
700
|
+
## Please check unscoped if you want to remove all previous scopes (including
|
701
|
+
## the default_scope) during the execution of a block.
|
702
|
+
#def scoping
|
703
|
+
# previous, @model_class.current_scope = @model_class.current_scope, self
|
704
|
+
# yield
|
705
|
+
#ensure
|
706
|
+
# klass.current_scope = previous
|
707
|
+
#end
|
708
|
+
|
709
|
+
|
710
|
+
# #where accepts conditions in one of several formats.
|
711
|
+
#
|
712
|
+
# === string
|
713
|
+
#
|
714
|
+
# A single string, without additional arguments, is used in the where clause of the query.
|
715
|
+
#
|
716
|
+
# Client.where("orders_count = '2'")
|
717
|
+
# # SELECT * where orders_count = '2';
|
718
|
+
#
|
719
|
+
# Note that building your own string from user input may expose your application
|
720
|
+
# to injection attacks if not done properly. As an alternative, it is recommended
|
721
|
+
# to use one of the following methods.
|
722
|
+
#
|
723
|
+
# === array
|
724
|
+
#
|
725
|
+
# If an array is passed, then the first element of the array is treated as a template, and
|
726
|
+
# the remaining elements are inserted into the template to generate the condition.
|
727
|
+
# Active Record takes care of building the query to avoid injection attacks, and will
|
728
|
+
# convert from the ruby type to the database type where needed. Elements are inserted
|
729
|
+
# into the string in the order in which they appear.
|
730
|
+
#
|
731
|
+
# User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
|
732
|
+
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
|
733
|
+
#
|
734
|
+
# Alternatively, you can use named placeholders in the template, and pass a hash as the
|
735
|
+
# second element of the array. The names in the template are replaced with the corresponding
|
736
|
+
# values from the hash.
|
737
|
+
#
|
738
|
+
# User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
|
739
|
+
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
|
740
|
+
#
|
741
|
+
# This can make for more readable code in complex queries.
|
742
|
+
#
|
743
|
+
# Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
|
744
|
+
# than the previous methods; you are responsible for ensuring that the values in the template
|
745
|
+
# are properly quoted. The values are passed to the connector for quoting, but the caller
|
746
|
+
# is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
|
747
|
+
# the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
|
748
|
+
#
|
749
|
+
# User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
|
750
|
+
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
|
751
|
+
#
|
752
|
+
# If #where is called with multiple arguments, these are treated as if they were passed as
|
753
|
+
# the elements of a single array.
|
754
|
+
#
|
755
|
+
# User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
|
756
|
+
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
|
757
|
+
#
|
758
|
+
# When using strings to specify conditions, you can use any operator available from
|
759
|
+
# the database. While this provides the most flexibility, you can also unintentionally introduce
|
760
|
+
# dependencies on the underlying database. If your code is intended for general consumption,
|
761
|
+
# test with multiple database backends.
|
762
|
+
#
|
763
|
+
# === hash
|
764
|
+
#
|
765
|
+
# #where will also accept a hash condition, in which the keys are fields and the values
|
766
|
+
# are values to be searched for.
|
767
|
+
#
|
768
|
+
# Fields can be symbols or strings. Values can be single values, arrays, or ranges.
|
769
|
+
#
|
770
|
+
# User.where({ name: "Joe", email: "joe@example.com" })
|
771
|
+
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com'
|
772
|
+
#
|
773
|
+
# User.where({ name: ["Alice", "Bob"]})
|
774
|
+
# # SELECT * WHERE name IN ('Alice', 'Bob')
|
775
|
+
#
|
776
|
+
# User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
|
777
|
+
# # SELECT * WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
|
778
|
+
#
|
779
|
+
# In the case of a belongs_to relationship, an association key can be used
|
780
|
+
# to specify the model if an ActiveRecord object is used as the value.
|
781
|
+
#
|
782
|
+
# author = Author.find(1)
|
783
|
+
#
|
784
|
+
# # The following queries will be equivalent:
|
785
|
+
# Post.where(:author => author)
|
786
|
+
# Post.where(:author_id => author)
|
787
|
+
#
|
788
|
+
# === empty condition
|
789
|
+
#
|
790
|
+
# If the condition returns true for blank?, then where is a no-op and returns the current relation.
|
791
|
+
#
|
792
|
+
def where(opts, *rest)
|
793
|
+
return self if opts.blank?
|
794
|
+
case opts
|
795
|
+
when Hash
|
796
|
+
@options[:hash] = opts # keep around for first_or_create stuff...
|
797
|
+
opts.each do |k,v|
|
798
|
+
# todo: can we support IN and BETWEEN syntax as documented above?
|
799
|
+
v = "'#{v}'" if v.is_a? String
|
800
|
+
query_conditions << "#{k} = #{v}"
|
801
|
+
end
|
802
|
+
when String
|
803
|
+
query_conditions << opts
|
804
|
+
when Array
|
805
|
+
query = opts.shift.gsub '?', "'%s'"
|
806
|
+
query = query % opts
|
807
|
+
query_conditions << query
|
808
|
+
end
|
809
|
+
self
|
810
|
+
end
|
811
|
+
|
812
|
+
|
813
|
+
protected
|
814
|
+
|
815
|
+
|
816
|
+
def limit_value
|
817
|
+
@options[:limit]
|
818
|
+
end
|
819
|
+
|
820
|
+
def query_conditions
|
821
|
+
@options[:conditions] ||= []
|
822
|
+
end
|
823
|
+
|
824
|
+
def loaded?
|
825
|
+
!!@records
|
826
|
+
end
|
827
|
+
|
828
|
+
def reversed?
|
829
|
+
!!@options[:reversed]
|
830
|
+
end
|
831
|
+
|
832
|
+
def find_one(id_or_name=nil)
|
833
|
+
begin
|
834
|
+
entity = @model_class.resource[id_or_name].query(nil, limit: 1).entity
|
835
|
+
@model_class.model_name.constantize.new(entity.data) if entity
|
836
|
+
rescue RestClient::ResourceNotFound
|
837
|
+
nil
|
838
|
+
end
|
839
|
+
end
|
840
|
+
|
841
|
+
def find_one!(id_or_name=nil)
|
842
|
+
find_one(id_or_name) or raise RecordNotFound
|
843
|
+
end
|
844
|
+
|
845
|
+
# Server-side options:
|
846
|
+
# Xql string Query in the query language
|
847
|
+
# type string Entity type to return
|
848
|
+
# Xreversed string Return results in reverse order
|
849
|
+
# connection string Connection type (e.g., "likes")
|
850
|
+
# start string First entity's UUID to return
|
851
|
+
# cursor string Encoded representation of the query position for paging
|
852
|
+
# Xlimit integer Number of results to return
|
853
|
+
# permission string Permission type
|
854
|
+
# Xfilter string Condition on which to filter
|
855
|
+
def query_options
|
856
|
+
# todo: support more options?
|
857
|
+
options = {}
|
858
|
+
options.merge!({:limit => limit_value.to_json}) if limit_value
|
859
|
+
options.merge!({:skip => @options[:skip].to_json}) if @options[:skip]
|
860
|
+
options.merge!({:reversed => reversed?.to_json}) if reversed?
|
861
|
+
options.merge!({:order => @options[:order]}) if @options[:order]
|
862
|
+
options
|
863
|
+
end
|
864
|
+
|
865
|
+
def create_query
|
866
|
+
select = @options[:select] || '*'
|
867
|
+
where = ('where ' + query_conditions.join(' and ')) unless query_conditions.blank?
|
868
|
+
"select #{select} #{where}"
|
869
|
+
end
|
870
|
+
|
871
|
+
def run_query
|
872
|
+
@model_class.resource.query(create_query, query_options)
|
873
|
+
end
|
874
|
+
|
875
|
+
def run_update(attributes)
|
876
|
+
@model_class.resource.update_query(attributes, create_query, query_options)
|
877
|
+
end
|
878
|
+
|
879
|
+
def load
|
880
|
+
return if loaded?
|
881
|
+
begin
|
882
|
+
response = run_query
|
883
|
+
@records = response.entities.collect {|r| @model_class.model_name.constantize.new(r.data)}
|
884
|
+
rescue RestClient::ResourceNotFound
|
885
|
+
@records = []
|
886
|
+
end
|
887
|
+
end
|
888
|
+
end
|
889
|
+
end
|
890
|
+
end
|