usergrid_ironhorse 0.0.2
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/.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
|