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
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
|
require 'active_support/core_ext/object/blank'
module ActiveRecord
# = Active Record Relation
class Relation
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
# These are explicitly delegated to improve performance (avoids method_missing)
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :to => :klass
attr_reader :table, :klass, :loaded
attr_accessor :extensions, :default_scoped
alias :loaded? :loaded
alias :default_scoped? :default_scoped
def initialize(klass, table)
@klass, @table = klass, table
@implicit_readonly = nil
@loaded = false
@default_scoped = false
SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
(ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
@extensions = []
end
def insert(values)
primary_key_value = nil
if primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == primary_key
}]
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
primary_key_value = connection.next_sequence_value(klass.sequence_name)
values[klass.arel_table[klass.primary_key]] = primary_key_value
end
end
im = arel.create_insert
im.into @table
conn = @klass.connection
substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
binds = substitutes.map do |arel_attr, value|
[@klass.columns_hash[arel_attr.name], value]
end
substitutes.each_with_index do |tuple, i|
tuple[1] = conn.substitute_at(binds[i][0], i)
end
if values.empty? # empty insert
im.values = Arel.sql(connection.empty_insert_statement_value)
else
im.insert substitutes
end
conn.insert(
im.to_sql,
'SQL',
primary_key,
primary_key_value,
nil,
binds)
end
def new(*args, &block)
scoping { @klass.new(*args, &block) }
end
def initialize_copy(other)
reset
end
alias build new
def create(*args, &block)
scoping { @klass.create(*args, &block) }
end
def create!(*args, &block)
scoping { @klass.create!(*args, &block) }
end
def respond_to?(method, include_private = false)
arel.respond_to?(method, include_private) ||
Array.method_defined?(method) ||
@klass.respond_to?(method, include_private) ||
super
end
def to_a
return @records if loaded?
@records = if @readonly_value.nil? && !@klass.locking_enabled?
eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
else
IdentityMap.without do
eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
end
end
preload = @preload_values
preload += @includes_values unless eager_loading?
preload.each do |associations|
ActiveRecord::Associations::Preloader.new(@records, associations).run
end
# @readonly_value is true only if set explicitly. @implicit_readonly is true if there
# are JOINS and no explicit SELECT.
readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
@records.each { |record| record.readonly! } if readonly
@loaded = true
@records
end
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
# Returns size of the records.
def size
loaded? ? @records.length : count
end
# Returns true if there are no records.
def empty?
return @records.empty? if loaded?
c = count
c.respond_to?(:zero?) ? c.zero? : c.empty?
end
def any?
if block_given?
to_a.any? { |*block_args| yield(*block_args) }
else
!empty?
end
end
def many?
if block_given?
to_a.many? { |*block_args| yield(*block_args) }
else
@limit_value ? to_a.many? : size > 1
end
end
# Scope all queries to the current scope.
#
# ==== Example
#
# Comment.where(:post_id => 1).scoping do
# Comment.first # SELECT * FROM comments WHERE post_id = 1
# end
#
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
@klass.send(:with_scope, self, :overwrite) { yield }
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
# database. It does not instantiate the involved models and it does not trigger Active Record callbacks
# or validations.
#
# ==== Parameters
#
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
# * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement.
# See conditions in the intro.
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
#
# # Update all customers with the given attributes
# Customer.update_all :wants_email => true
#
# # Update all books with 'Rails' in their title
# Book.update_all "author = 'David'", "title LIKE '%Rails%'"
#
# # Update all avatars migrated more than a week ago
# Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago]
#
# # Update all books that match conditions, but limit it to 5 ordered by date
# Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
#
# # Conditions from the current relation also works
# Book.where('title LIKE ?', '%Rails%').update_all(:author => 'David')
#
# # The same idea applies to limit and order
# Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David')
def update_all(updates, conditions = nil, options = {})
IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled?
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
limit = nil
order = []
# Apply limit and order only if they're both present
if @limit_value.present? == @order_values.present?
limit = arel.limit
order = arel.orders
end
stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
stmt.take limit if limit
stmt.order(*order)
stmt.key = table[primary_key]
@klass.connection.update stmt.to_sql
end
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
# The resulting object is returned whether the object was saved successfully to the database or not.
#
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
# * +attributes+ - This should be a hash of attributes or an array of hashes.
#
# ==== Examples
#
# # Updates one record
# Person.update(15, :user_name => 'Samuel', :group => 'expert')
#
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
def update(id, attributes)
if id.is_a?(Array)
idx = -1
id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
else
object = find(id)
object.update_attributes(attributes)
object
end
end
# Destroys the records matching +conditions+ by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
# executed (including <tt>:dependent</tt> association options and
# +before_destroy+/+after_destroy+ Observer methods). Returns the
# collection of objects that were destroyed; each will be frozen, to
# reflect that no changes should be made (since they can't be
# persisted).
#
# Note: Instantiation, callback execution, and deletion of each
# record can be time consuming when you're removing many records at
# once. It generates at least one SQL +DELETE+ query per record (or
# possibly more, to enforce your callbacks). If you want to delete many
# rows quickly, without concern for their associations or callbacks, use
# +delete_all+ instead.
#
# ==== Parameters
#
# * +conditions+ - A string, array, or hash that specifies which records
# to destroy. If omitted, all records are destroyed. See the
# Conditions section in the introduction to ActiveRecord::Base for
# more information.
#
# ==== Examples
#
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(:status => "inactive")
# Person.where(:age => 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
where(conditions).destroy_all
else
to_a.each {|object| object.destroy }.tap { reset }
end
end
# Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
# therefore all callbacks and filters are fired off before the object is deleted. This method is
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
#
# This essentially finds the object (or multiple objects) with the given id, creates a new object
# from the attributes, and then calls destroy on it.
#
# ==== Parameters
#
# * +id+ - Can be either an Integer or an Array of Integers.
#
# ==== Examples
#
# # Destroy a single object
# Todo.destroy(1)
#
# # Destroy multiple objects
# todos = [1,2,3]
# Todo.destroy(todos)
def destroy(id)
if id.is_a?(Array)
id.map { |one_id| destroy(one_id) }
else
find(id).destroy
end
end
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
# calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
# goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
# though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
# the number of rows affected.
#
# ==== Parameters
#
# * +conditions+ - Conditions are specified the same way as with +find+ method.
#
# ==== Example
#
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
# Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
def delete_all(conditions = nil)
IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled?
if conditions
where(conditions).delete_all
else
statement = arel.compile_delete
affected = @klass.connection.delete(
statement.to_sql, 'SQL', bind_values)
reset
affected
end
end
# Deletes the row with a primary key matching the +id+ argument, using a
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
# Record objects are not instantiated, so the object's callbacks are not
# executed, including any <tt>:dependent</tt> association options or
# Observer methods.
#
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
#
# Note: Although it is often much faster than the alternative,
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
# your application that ensures referential integrity or performs other
# essential jobs.
#
# ==== Examples
#
# # Delete a single row
# Todo.delete(1)
#
# # Delete multiple rows
# Todo.delete([2,3,4])
def delete(id_or_array)
IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled?
where(primary_key => id_or_array).delete_all
end
def reload
reset
to_a # force reload
self
end
def reset
@first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
@should_eager_load = @join_dependency = nil
@records = []
self
end
def to_sql
@to_sql ||= arel.to_sql
end
def where_values_hash
equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
Hash[equalities.map { |where| [where.left.name, where.right] }]
end
def scope_for_create
@scope_for_create ||= where_values_hash.merge(@create_with_value || {})
end
def eager_loading?
@should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?))
end
def ==(other)
case other
when Relation
other.to_sql == to_sql
when Array
to_a == other
end
end
def inspect
to_a.inspect
end
def with_default_scope #:nodoc:
if default_scoped?
default_scope = @klass.send(:build_default_scope)
default_scope ? default_scope.merge(self) : self
else
self
end
end
protected
def method_missing(method, *args, &block)
if Array.method_defined?(method)
to_a.send(method, *args, &block)
elsif @klass.respond_to?(method)
scoping { @klass.send(method, *args, &block) }
elsif arel.respond_to?(method)
arel.send(method, *args, &block)
else
super
end
end
private
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
tables_in_string(join.left)
else
[join.left.table_name, join.left.table_alias]
end
end
joined_tables += [table.name, table.table_alias]
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
(tables_in_string(to_sql) - joined_tables).any?
end
def tables_in_string(string)
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
end
end
end
|