activerecord 3.0.0.beta3 → 3.0.0.beta4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +27 -0
- data/lib/active_record.rb +5 -1
- data/lib/active_record/aggregations.rb +1 -0
- data/lib/active_record/association_preload.rb +1 -1
- data/lib/active_record/associations.rb +88 -33
- data/lib/active_record/associations/association_collection.rb +12 -11
- data/lib/active_record/associations/association_proxy.rb +1 -1
- data/lib/active_record/attribute_methods.rb +10 -1
- data/lib/active_record/attribute_methods/dirty.rb +50 -50
- data/lib/active_record/attribute_methods/primary_key.rb +1 -1
- data/lib/active_record/autosave_association.rb +20 -5
- data/lib/active_record/base.rb +28 -343
- data/lib/active_record/callbacks.rb +23 -34
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +11 -18
- data/lib/active_record/connection_adapters/abstract/quoting.rb +3 -7
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +61 -7
- data/lib/active_record/connection_adapters/abstract_adapter.rb +3 -1
- data/lib/active_record/connection_adapters/mysql_adapter.rb +22 -50
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +31 -143
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +6 -2
- data/lib/active_record/counter_cache.rb +105 -0
- data/lib/active_record/fixtures.rb +16 -15
- data/lib/active_record/locale/en.yml +2 -2
- data/lib/active_record/locking/optimistic.rb +37 -16
- data/lib/active_record/migration.rb +7 -3
- data/lib/active_record/named_scope.rb +1 -5
- data/lib/active_record/nested_attributes.rb +13 -2
- data/lib/active_record/observer.rb +19 -7
- data/lib/active_record/persistence.rb +230 -0
- data/lib/active_record/railtie.rb +17 -23
- data/lib/active_record/railties/databases.rake +3 -2
- data/lib/active_record/relation.rb +5 -2
- data/lib/active_record/relation/batches.rb +6 -1
- data/lib/active_record/relation/calculations.rb +12 -9
- data/lib/active_record/relation/finder_methods.rb +14 -10
- data/lib/active_record/relation/query_methods.rb +62 -44
- data/lib/active_record/relation/spawn_methods.rb +6 -1
- data/lib/active_record/schema_dumper.rb +3 -0
- data/lib/active_record/serializers/xml_serializer.rb +30 -56
- data/lib/active_record/session_store.rb +1 -1
- data/lib/active_record/timestamp.rb +22 -26
- data/lib/active_record/transactions.rb +168 -50
- data/lib/active_record/validations.rb +33 -49
- data/lib/active_record/version.rb +1 -1
- data/lib/rails/generators/active_record.rb +6 -9
- metadata +27 -10
@@ -11,6 +11,7 @@ require 'active_record/connection_adapters/abstract/quoting'
|
|
11
11
|
require 'active_record/connection_adapters/abstract/connection_pool'
|
12
12
|
require 'active_record/connection_adapters/abstract/connection_specification'
|
13
13
|
require 'active_record/connection_adapters/abstract/query_cache'
|
14
|
+
require 'active_record/connection_adapters/abstract/database_limits'
|
14
15
|
|
15
16
|
module ActiveRecord
|
16
17
|
module ConnectionAdapters # :nodoc:
|
@@ -29,6 +30,7 @@ module ActiveRecord
|
|
29
30
|
# notably, the instance methods provided by SchemaStatement are very useful.
|
30
31
|
class AbstractAdapter
|
31
32
|
include Quoting, DatabaseStatements, SchemaStatements
|
33
|
+
include DatabaseLimits
|
32
34
|
include QueryCache
|
33
35
|
include ActiveSupport::Callbacks
|
34
36
|
|
@@ -197,7 +199,7 @@ module ActiveRecord
|
|
197
199
|
def log(sql, name)
|
198
200
|
name ||= "SQL"
|
199
201
|
result = nil
|
200
|
-
ActiveSupport::Notifications.instrument("active_record
|
202
|
+
ActiveSupport::Notifications.instrument("sql.active_record",
|
201
203
|
:sql => sql, :name => name, :connection_id => self.object_id) do
|
202
204
|
@runtime += Benchmark.ms { result = yield }
|
203
205
|
end
|
@@ -3,48 +3,6 @@ require 'active_support/core_ext/kernel/requires'
|
|
3
3
|
require 'active_support/core_ext/object/blank'
|
4
4
|
require 'set'
|
5
5
|
|
6
|
-
module MysqlCompat #:nodoc:
|
7
|
-
# add all_hashes method to standard mysql-c bindings or pure ruby version
|
8
|
-
def self.define_all_hashes_method!
|
9
|
-
raise 'Mysql not loaded' unless defined?(::Mysql)
|
10
|
-
|
11
|
-
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
|
12
|
-
return if target.instance_methods.include?('all_hashes') ||
|
13
|
-
target.instance_methods.include?(:all_hashes)
|
14
|
-
|
15
|
-
# Ruby driver has a version string and returns null values in each_hash
|
16
|
-
# C driver >= 2.7 returns null values in each_hash
|
17
|
-
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
|
18
|
-
target.class_eval <<-'end_eval'
|
19
|
-
def all_hashes # def all_hashes
|
20
|
-
rows = [] # rows = []
|
21
|
-
each_hash { |row| rows << row } # each_hash { |row| rows << row }
|
22
|
-
rows # rows
|
23
|
-
end # end
|
24
|
-
end_eval
|
25
|
-
|
26
|
-
# adapters before 2.7 don't have a version constant
|
27
|
-
# and don't return null values in each_hash
|
28
|
-
else
|
29
|
-
target.class_eval <<-'end_eval'
|
30
|
-
def all_hashes # def all_hashes
|
31
|
-
rows = [] # rows = []
|
32
|
-
all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
|
33
|
-
fields[f.name] = nil; fields # fields[f.name] = nil; fields
|
34
|
-
} # }
|
35
|
-
each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
|
36
|
-
rows # rows
|
37
|
-
end # end
|
38
|
-
end_eval
|
39
|
-
end
|
40
|
-
|
41
|
-
unless target.instance_methods.include?('all_hashes') ||
|
42
|
-
target.instance_methods.include?(:all_hashes)
|
43
|
-
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
6
|
module ActiveRecord
|
49
7
|
class Base
|
50
8
|
# Establishes a connection to the database that's used by all Active Record objects.
|
@@ -57,17 +15,17 @@ module ActiveRecord
|
|
57
15
|
password = config[:password].to_s
|
58
16
|
database = config[:database]
|
59
17
|
|
60
|
-
# Require the MySQL driver and define Mysql::Result.all_hashes
|
61
18
|
unless defined? Mysql
|
62
19
|
begin
|
63
|
-
|
20
|
+
require 'mysql'
|
64
21
|
rescue LoadError
|
65
|
-
|
66
|
-
raise
|
22
|
+
raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql', '2.8.1'"
|
67
23
|
end
|
68
|
-
end
|
69
24
|
|
70
|
-
|
25
|
+
unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash)
|
26
|
+
raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'"
|
27
|
+
end
|
28
|
+
end
|
71
29
|
|
72
30
|
mysql = Mysql.init
|
73
31
|
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
@@ -461,10 +419,11 @@ module ActiveRecord
|
|
461
419
|
if current_index != row[2]
|
462
420
|
next if row[2] == "PRIMARY" # skip the primary key
|
463
421
|
current_index = row[2]
|
464
|
-
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
|
422
|
+
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
|
465
423
|
end
|
466
424
|
|
467
425
|
indexes.last.columns << row[4]
|
426
|
+
indexes.last.lengths << row[7]
|
468
427
|
end
|
469
428
|
result.free
|
470
429
|
indexes
|
@@ -594,6 +553,18 @@ module ActiveRecord
|
|
594
553
|
end
|
595
554
|
|
596
555
|
protected
|
556
|
+
def quoted_columns_for_index(column_names, options = {})
|
557
|
+
length = options[:length] if options.is_a?(Hash)
|
558
|
+
|
559
|
+
quoted_column_names = case length
|
560
|
+
when Hash
|
561
|
+
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
|
562
|
+
when Fixnum
|
563
|
+
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
|
564
|
+
else
|
565
|
+
column_names.map {|name| quote_column_name(name) }
|
566
|
+
end
|
567
|
+
end
|
597
568
|
|
598
569
|
def translate_exception(exception, message)
|
599
570
|
return super unless exception.respond_to?(:errno)
|
@@ -643,7 +614,8 @@ module ActiveRecord
|
|
643
614
|
def select(sql, name = nil)
|
644
615
|
@connection.query_with_result = true
|
645
616
|
result = execute(sql, name)
|
646
|
-
rows =
|
617
|
+
rows = []
|
618
|
+
result.each_hash { |row| rows << row }
|
647
619
|
result.free
|
648
620
|
rows
|
649
621
|
end
|
@@ -2,26 +2,12 @@ require 'active_record/connection_adapters/abstract_adapter'
|
|
2
2
|
require 'active_support/core_ext/kernel/requires'
|
3
3
|
require 'active_support/core_ext/object/blank'
|
4
4
|
|
5
|
-
begin
|
6
|
-
require_library_or_gem 'pg'
|
7
|
-
rescue LoadError => e
|
8
|
-
begin
|
9
|
-
require_library_or_gem 'postgres'
|
10
|
-
class PGresult
|
11
|
-
alias_method :nfields, :num_fields unless self.method_defined?(:nfields)
|
12
|
-
alias_method :ntuples, :num_tuples unless self.method_defined?(:ntuples)
|
13
|
-
alias_method :ftype, :type unless self.method_defined?(:ftype)
|
14
|
-
alias_method :cmd_tuples, :cmdtuples unless self.method_defined?(:cmd_tuples)
|
15
|
-
end
|
16
|
-
rescue LoadError
|
17
|
-
raise e
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
5
|
module ActiveRecord
|
22
6
|
class Base
|
23
7
|
# Establishes a connection to the database that's used by all Active Record objects
|
24
8
|
def self.postgresql_connection(config) # :nodoc:
|
9
|
+
require 'pg'
|
10
|
+
|
25
11
|
config = config.symbolize_keys
|
26
12
|
host = config[:host]
|
27
13
|
port = config[:port] || 5432
|
@@ -277,20 +263,12 @@ module ActiveRecord
|
|
277
263
|
true
|
278
264
|
end
|
279
265
|
|
280
|
-
#
|
281
|
-
def
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
self.client_min_messages = 'panic'
|
287
|
-
|
288
|
-
# postgres-pr does not raise an exception when client_min_messages is set higher
|
289
|
-
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
|
290
|
-
# PGresult instead.
|
291
|
-
has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
|
292
|
-
self.client_min_messages = client_min_messages_old
|
293
|
-
has_support
|
266
|
+
# Enable standard-conforming strings if available.
|
267
|
+
def set_standard_conforming_strings
|
268
|
+
old, self.client_min_messages = client_min_messages, 'panic'
|
269
|
+
execute('SET standard_conforming_strings = on') rescue nil
|
270
|
+
ensure
|
271
|
+
self.client_min_messages = old
|
294
272
|
end
|
295
273
|
|
296
274
|
def supports_insert_with_returning?
|
@@ -314,85 +292,23 @@ module ActiveRecord
|
|
314
292
|
# QUOTING ==================================================
|
315
293
|
|
316
294
|
# Escapes binary strings for bytea input to the database.
|
317
|
-
def escape_bytea(
|
318
|
-
|
319
|
-
self.class.instance_eval do
|
320
|
-
define_method(:escape_bytea) do |value|
|
321
|
-
@connection.escape_bytea(value) if value
|
322
|
-
end
|
323
|
-
end
|
324
|
-
elsif PGconn.respond_to?(:escape_bytea)
|
325
|
-
self.class.instance_eval do
|
326
|
-
define_method(:escape_bytea) do |value|
|
327
|
-
PGconn.escape_bytea(value) if value
|
328
|
-
end
|
329
|
-
end
|
330
|
-
else
|
331
|
-
self.class.instance_eval do
|
332
|
-
define_method(:escape_bytea) do |value|
|
333
|
-
if value
|
334
|
-
result = ''
|
335
|
-
value.each_byte { |c| result << sprintf('\\\\%03o', c) }
|
336
|
-
result
|
337
|
-
end
|
338
|
-
end
|
339
|
-
end
|
340
|
-
end
|
341
|
-
escape_bytea(original_value)
|
295
|
+
def escape_bytea(value)
|
296
|
+
@connection.escape_bytea(value) if value
|
342
297
|
end
|
343
298
|
|
344
299
|
# Unescapes bytea output from a database to the binary string it represents.
|
345
300
|
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
|
346
301
|
# on escaped binary output from database drive.
|
347
|
-
def unescape_bytea(
|
348
|
-
|
349
|
-
# or an unescaped Active Record attribute that was just written.
|
350
|
-
if PGconn.respond_to?(:unescape_bytea)
|
351
|
-
self.class.instance_eval do
|
352
|
-
define_method(:unescape_bytea) do |value|
|
353
|
-
if value =~ /\\\d{3}/
|
354
|
-
PGconn.unescape_bytea(value)
|
355
|
-
else
|
356
|
-
value
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
|
-
else
|
361
|
-
self.class.instance_eval do
|
362
|
-
define_method(:unescape_bytea) do |value|
|
363
|
-
if value =~ /\\\d{3}/
|
364
|
-
result = ''
|
365
|
-
i, max = 0, value.size
|
366
|
-
while i < max
|
367
|
-
char = value[i]
|
368
|
-
if char == ?\\
|
369
|
-
if value[i+1] == ?\\
|
370
|
-
char = ?\\
|
371
|
-
i += 1
|
372
|
-
else
|
373
|
-
char = value[i+1..i+3].oct
|
374
|
-
i += 3
|
375
|
-
end
|
376
|
-
end
|
377
|
-
result << char
|
378
|
-
i += 1
|
379
|
-
end
|
380
|
-
result
|
381
|
-
else
|
382
|
-
value
|
383
|
-
end
|
384
|
-
end
|
385
|
-
end
|
386
|
-
end
|
387
|
-
unescape_bytea(original_value)
|
302
|
+
def unescape_bytea(value)
|
303
|
+
@connection.unescape_bytea(value) if value
|
388
304
|
end
|
389
305
|
|
390
306
|
# Quotes PostgreSQL-specific data types for SQL input.
|
391
307
|
def quote(value, column = nil) #:nodoc:
|
392
308
|
if value.kind_of?(String) && column && column.type == :binary
|
393
|
-
"
|
309
|
+
"'#{escape_bytea(value)}'"
|
394
310
|
elsif value.kind_of?(String) && column && column.sql_type == 'xml'
|
395
|
-
"xml
|
311
|
+
"xml '#{quote_string(value)}'"
|
396
312
|
elsif value.kind_of?(Numeric) && column && column.sql_type == 'money'
|
397
313
|
# Not truly string input, so doesn't require (or allow) escape string syntax.
|
398
314
|
"'#{value.to_s}'"
|
@@ -408,28 +324,9 @@ module ActiveRecord
|
|
408
324
|
end
|
409
325
|
end
|
410
326
|
|
411
|
-
# Quotes strings for use in SQL input
|
412
|
-
def quote_string(
|
413
|
-
|
414
|
-
self.class.instance_eval do
|
415
|
-
define_method(:quote_string) do |s|
|
416
|
-
@connection.escape(s)
|
417
|
-
end
|
418
|
-
end
|
419
|
-
elsif PGconn.respond_to?(:escape)
|
420
|
-
self.class.instance_eval do
|
421
|
-
define_method(:quote_string) do |s|
|
422
|
-
PGconn.escape(s)
|
423
|
-
end
|
424
|
-
end
|
425
|
-
else
|
426
|
-
# There are some incorrectly compiled postgres drivers out there
|
427
|
-
# that don't define PGconn.escape.
|
428
|
-
self.class.instance_eval do
|
429
|
-
remove_method(:quote_string)
|
430
|
-
end
|
431
|
-
end
|
432
|
-
quote_string(original_value)
|
327
|
+
# Quotes strings for use in SQL input.
|
328
|
+
def quote_string(s) #:nodoc:
|
329
|
+
@connection.escape(s)
|
433
330
|
end
|
434
331
|
|
435
332
|
# Checks the following cases:
|
@@ -593,12 +490,8 @@ module ActiveRecord
|
|
593
490
|
execute "ROLLBACK"
|
594
491
|
end
|
595
492
|
|
596
|
-
|
597
|
-
|
598
|
-
# while the ruby-postgres driver does not.
|
599
|
-
def outside_transaction?
|
600
|
-
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
601
|
-
end
|
493
|
+
def outside_transaction?
|
494
|
+
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
602
495
|
end
|
603
496
|
|
604
497
|
def create_savepoint
|
@@ -926,9 +819,12 @@ module ActiveRecord
|
|
926
819
|
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
927
820
|
end
|
928
821
|
|
929
|
-
|
930
|
-
|
931
|
-
|
822
|
+
def remove_index!(table_name, index_name) #:nodoc:
|
823
|
+
execute "DROP INDEX #{quote_table_name(index_name)}"
|
824
|
+
end
|
825
|
+
|
826
|
+
def index_name_length
|
827
|
+
63
|
932
828
|
end
|
933
829
|
|
934
830
|
# Maps logical Rails types to PostgreSQL-specific data types.
|
@@ -1005,22 +901,11 @@ module ActiveRecord
|
|
1005
901
|
# Ignore async_exec and async_query when using postgres-pr.
|
1006
902
|
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
|
1007
903
|
|
1008
|
-
# Use escape string syntax if available. We cannot do this lazily when encountering
|
1009
|
-
# the first string, because that could then break any transactions in progress.
|
1010
|
-
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
|
1011
|
-
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
|
1012
|
-
# support escape string syntax. Don't override the inherited quoted_string_prefix.
|
1013
|
-
if supports_standard_conforming_strings?
|
1014
|
-
self.class.instance_eval do
|
1015
|
-
define_method(:quoted_string_prefix) { 'E' }
|
1016
|
-
end
|
1017
|
-
end
|
1018
|
-
|
1019
904
|
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
1020
905
|
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
1021
906
|
# should know about this but can't detect it there, so deal with it here.
|
1022
|
-
PostgreSQLColumn.money_precision =
|
1023
|
-
|
907
|
+
PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
|
908
|
+
|
1024
909
|
configure_connection
|
1025
910
|
end
|
1026
911
|
|
@@ -1036,7 +921,10 @@ module ActiveRecord
|
|
1036
921
|
end
|
1037
922
|
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
1038
923
|
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
1039
|
-
|
924
|
+
|
925
|
+
# Use standard-conforming strings if available so we don't have to do the E'...' dance.
|
926
|
+
set_standard_conforming_strings
|
927
|
+
|
1040
928
|
# If using ActiveRecord's time zone support configure the connection to return
|
1041
929
|
# TIMESTAMP WITH ZONE types in UTC.
|
1042
930
|
execute("SET time zone 'UTC'") if ActiveRecord::Base.default_timezone == :utc
|
@@ -34,6 +34,10 @@ module ActiveRecord
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def binary_to_string(value)
|
37
|
+
if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT
|
38
|
+
value = value.force_encoding(Encoding::ASCII_8BIT)
|
39
|
+
end
|
40
|
+
|
37
41
|
value.gsub(/%00|%25/n) do |b|
|
38
42
|
case b
|
39
43
|
when "%00" then "\0"
|
@@ -217,8 +221,8 @@ module ActiveRecord
|
|
217
221
|
column ? column['name'] : nil
|
218
222
|
end
|
219
223
|
|
220
|
-
def remove_index(table_name,
|
221
|
-
execute "DROP INDEX #{quote_column_name(index_name
|
224
|
+
def remove_index!(table_name, index_name) #:nodoc:
|
225
|
+
execute "DROP INDEX #{quote_column_name(index_name)}"
|
222
226
|
end
|
223
227
|
|
224
228
|
def rename_table(name, new_name)
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module CounterCache
|
3
|
+
# Resets one or more counter caches to their correct value using an SQL
|
4
|
+
# count query. This is useful when adding new counter caches, or if the
|
5
|
+
# counter has been corrupted or modified directly by SQL.
|
6
|
+
#
|
7
|
+
# ==== Parameters
|
8
|
+
#
|
9
|
+
# * +id+ - The id of the object you wish to reset a counter on.
|
10
|
+
# * +counters+ - One or more counter names to reset
|
11
|
+
#
|
12
|
+
# ==== Examples
|
13
|
+
#
|
14
|
+
# # For Post with id #1 records reset the comments_count
|
15
|
+
# Post.reset_counters(1, :comments)
|
16
|
+
def reset_counters(id, *counters)
|
17
|
+
object = find(id)
|
18
|
+
counters.each do |association|
|
19
|
+
child_class = reflect_on_association(association.to_sym).klass
|
20
|
+
belongs_name = self.name.demodulize.underscore.to_sym
|
21
|
+
counter_name = child_class.reflect_on_association(belongs_name).counter_cache_column
|
22
|
+
|
23
|
+
self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({
|
24
|
+
arel_table[counter_name] => object.send(association).count
|
25
|
+
})
|
26
|
+
end
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
|
30
|
+
# A generic "counter updater" implementation, intended primarily to be
|
31
|
+
# used by increment_counter and decrement_counter, but which may also
|
32
|
+
# be useful on its own. It simply does a direct SQL update for the record
|
33
|
+
# with the given ID, altering the given hash of counters by the amount
|
34
|
+
# given by the corresponding value:
|
35
|
+
#
|
36
|
+
# ==== Parameters
|
37
|
+
#
|
38
|
+
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
|
39
|
+
# * +counters+ - An Array of Hashes containing the names of the fields
|
40
|
+
# to update as keys and the amount to update the field by as values.
|
41
|
+
#
|
42
|
+
# ==== Examples
|
43
|
+
#
|
44
|
+
# # For the Post with id of 5, decrement the comment_count by 1, and
|
45
|
+
# # increment the action_count by 1
|
46
|
+
# Post.update_counters 5, :comment_count => -1, :action_count => 1
|
47
|
+
# # Executes the following SQL:
|
48
|
+
# # UPDATE posts
|
49
|
+
# # SET comment_count = comment_count - 1,
|
50
|
+
# # action_count = action_count + 1
|
51
|
+
# # WHERE id = 5
|
52
|
+
#
|
53
|
+
# # For the Posts with id of 10 and 15, increment the comment_count by 1
|
54
|
+
# Post.update_counters [10, 15], :comment_count => 1
|
55
|
+
# # Executes the following SQL:
|
56
|
+
# # UPDATE posts
|
57
|
+
# # SET comment_count = comment_count + 1,
|
58
|
+
# # WHERE id IN (10, 15)
|
59
|
+
def update_counters(id, counters)
|
60
|
+
updates = counters.map do |counter_name, value|
|
61
|
+
operator = value < 0 ? '-' : '+'
|
62
|
+
quoted_column = connection.quote_column_name(counter_name)
|
63
|
+
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
|
64
|
+
end
|
65
|
+
|
66
|
+
update_all(updates.join(', '), primary_key => id )
|
67
|
+
end
|
68
|
+
|
69
|
+
# Increment a number field by one, usually representing a count.
|
70
|
+
#
|
71
|
+
# This is used for caching aggregate values, so that they don't need to be computed every time.
|
72
|
+
# For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
|
73
|
+
# shown it would have to run an SQL query to find how many posts and comments there are.
|
74
|
+
#
|
75
|
+
# ==== Parameters
|
76
|
+
#
|
77
|
+
# * +counter_name+ - The name of the field that should be incremented.
|
78
|
+
# * +id+ - The id of the object that should be incremented.
|
79
|
+
#
|
80
|
+
# ==== Examples
|
81
|
+
#
|
82
|
+
# # Increment the post_count column for the record with an id of 5
|
83
|
+
# DiscussionBoard.increment_counter(:post_count, 5)
|
84
|
+
def increment_counter(counter_name, id)
|
85
|
+
update_counters(id, counter_name => 1)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Decrement a number field by one, usually representing a count.
|
89
|
+
#
|
90
|
+
# This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
|
91
|
+
#
|
92
|
+
# ==== Parameters
|
93
|
+
#
|
94
|
+
# * +counter_name+ - The name of the field that should be decremented.
|
95
|
+
# * +id+ - The id of the object that should be decremented.
|
96
|
+
#
|
97
|
+
# ==== Examples
|
98
|
+
#
|
99
|
+
# # Decrement the post_count column for the record with an id of 5
|
100
|
+
# DiscussionBoard.decrement_counter(:post_count, 5)
|
101
|
+
def decrement_counter(counter_name, id)
|
102
|
+
update_counters(id, counter_name => -1)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|