activerecord 4.0.0.beta1 → 4.0.0.rc1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +573 -30
- data/README.rdoc +3 -3
- data/lib/active_record.rb +8 -2
- data/lib/active_record/associations.rb +16 -9
- data/lib/active_record/associations/association.rb +8 -6
- data/lib/active_record/associations/association_scope.rb +2 -1
- data/lib/active_record/associations/belongs_to_association.rb +2 -2
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
- data/lib/active_record/associations/builder/belongs_to.rb +37 -5
- data/lib/active_record/associations/collection_association.rb +38 -14
- data/lib/active_record/associations/collection_proxy.rb +18 -15
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +3 -3
- data/lib/active_record/associations/has_many_association.rb +4 -3
- data/lib/active_record/associations/has_many_through_association.rb +1 -1
- data/lib/active_record/associations/has_one_association.rb +1 -1
- data/lib/active_record/associations/join_dependency.rb +29 -8
- data/lib/active_record/associations/join_dependency/join_association.rb +26 -6
- data/lib/active_record/associations/join_dependency/join_base.rb +2 -2
- data/lib/active_record/associations/join_dependency/join_part.rb +6 -6
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +1 -1
- data/lib/active_record/associations/preloader/has_many_through.rb +6 -2
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/attribute_assignment.rb +5 -5
- data/lib/active_record/attribute_methods.rb +20 -5
- data/lib/active_record/attribute_methods/dirty.rb +5 -1
- data/lib/active_record/attribute_methods/primary_key.rb +1 -1
- data/lib/active_record/attribute_methods/serialization.rb +9 -2
- data/lib/active_record/autosave_association.rb +19 -5
- data/lib/active_record/base.rb +3 -3
- data/lib/active_record/callbacks.rb +1 -1
- data/lib/active_record/coders/yaml_column.rb +8 -13
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +3 -9
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/quoting.rb +2 -8
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +60 -61
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +13 -2
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +291 -153
- data/lib/active_record/connection_adapters/abstract/transaction.rb +1 -1
- data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -1
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +55 -29
- data/lib/active_record/connection_adapters/column.rb +4 -4
- data/lib/active_record/connection_adapters/connection_specification.rb +2 -2
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -3
- data/lib/active_record/connection_adapters/mysql_adapter.rb +5 -5
- data/lib/active_record/connection_adapters/postgresql/cast.rb +22 -2
- data/lib/active_record/connection_adapters/postgresql/oid.rb +25 -6
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +26 -13
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -9
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +53 -24
- data/lib/active_record/connection_adapters/schema_cache.rb +35 -7
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +13 -5
- data/lib/active_record/connection_handling.rb +7 -7
- data/lib/active_record/core.rb +43 -8
- data/lib/active_record/counter_cache.rb +2 -1
- data/lib/active_record/errors.rb +11 -10
- data/lib/active_record/explain.rb +9 -7
- data/lib/active_record/explain_registry.rb +30 -0
- data/lib/active_record/explain_subscriber.rb +3 -2
- data/lib/active_record/fixture_set/file.rb +1 -2
- data/lib/active_record/fixtures.rb +13 -7
- data/lib/active_record/inheritance.rb +12 -4
- data/lib/active_record/integration.rb +3 -3
- data/lib/active_record/locking/optimistic.rb +2 -2
- data/lib/active_record/log_subscriber.rb +2 -2
- data/lib/active_record/migration.rb +69 -21
- data/lib/active_record/model_schema.rb +1 -1
- data/lib/active_record/nested_attributes.rb +98 -46
- data/lib/active_record/persistence.rb +3 -3
- data/lib/active_record/querying.rb +1 -1
- data/lib/active_record/railtie.rb +18 -4
- data/lib/active_record/railties/console_sandbox.rb +3 -2
- data/lib/active_record/railties/controller_runtime.rb +2 -1
- data/lib/active_record/railties/databases.rake +38 -80
- data/lib/active_record/reflection.rb +36 -3
- data/lib/active_record/relation.rb +18 -8
- data/lib/active_record/relation/calculations.rb +10 -5
- data/lib/active_record/relation/delegation.rb +3 -5
- data/lib/active_record/relation/finder_methods.rb +27 -14
- data/lib/active_record/relation/merger.rb +30 -2
- data/lib/active_record/relation/predicate_builder.rb +1 -6
- data/lib/active_record/relation/query_methods.rb +113 -16
- data/lib/active_record/runtime_registry.rb +17 -0
- data/lib/active_record/schema_dumper.rb +5 -1
- data/lib/active_record/schema_migration.rb +8 -5
- data/lib/active_record/scoping.rb +56 -2
- data/lib/active_record/scoping/default.rb +12 -11
- data/lib/active_record/scoping/named.rb +7 -3
- data/lib/active_record/serialization.rb +1 -1
- data/lib/active_record/statement_cache.rb +26 -0
- data/lib/active_record/tasks/database_tasks.rb +55 -10
- data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
- data/lib/active_record/tasks/mysql_database_tasks.rb +7 -2
- data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
- data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
- data/lib/active_record/timestamp.rb +6 -0
- data/lib/active_record/transactions.rb +7 -3
- data/lib/active_record/validations.rb +1 -2
- data/lib/active_record/validations/uniqueness.rb +7 -3
- data/lib/active_record/version.rb +7 -6
- data/lib/rails/generators/active_record/migration/migration_generator.rb +9 -2
- data/lib/rails/generators/active_record/{model/templates/migration.rb → migration/templates/create_table_migration.rb} +4 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +1 -1
- data/lib/rails/generators/active_record/model/templates/model.rb +4 -1
- metadata +17 -12
- data/examples/associations.png +0 -0
@@ -15,6 +15,9 @@ module ActiveRecord
|
|
15
15
|
# and if the inheritance column is attr accessible, it initializes an
|
16
16
|
# instance of the given subclass instead of the base class
|
17
17
|
def new(*args, &block)
|
18
|
+
if abstract_class? || self == Base
|
19
|
+
raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
|
20
|
+
end
|
18
21
|
if (attrs = args.first).is_a?(Hash)
|
19
22
|
if subclass = subclass_from_attrs(attrs)
|
20
23
|
return subclass.new(*args, &block)
|
@@ -167,11 +170,16 @@ module ActiveRecord
|
|
167
170
|
# this will ignore the inheritance column and return nil
|
168
171
|
def subclass_from_attrs(attrs)
|
169
172
|
subclass_name = attrs.with_indifferent_access[inheritance_column]
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
+
|
174
|
+
if subclass_name.present? && subclass_name != self.name
|
175
|
+
subclass = subclass_name.safe_constantize
|
176
|
+
|
177
|
+
unless descendants.include?(subclass)
|
178
|
+
raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
|
179
|
+
end
|
180
|
+
|
181
|
+
subclass
|
173
182
|
end
|
174
|
-
subclass
|
175
183
|
end
|
176
184
|
end
|
177
185
|
|
@@ -21,7 +21,7 @@ module ActiveRecord
|
|
21
21
|
# <tt>resources :users</tt> route. Normally, +user_path+ will
|
22
22
|
# construct a path with the user object's 'id' in it:
|
23
23
|
#
|
24
|
-
# user = User.
|
24
|
+
# user = User.find_by(name: 'Phusion')
|
25
25
|
# user_path(user) # => "/users/1"
|
26
26
|
#
|
27
27
|
# You can override +to_param+ in your model to make +user_path+ construct
|
@@ -33,7 +33,7 @@ module ActiveRecord
|
|
33
33
|
# end
|
34
34
|
# end
|
35
35
|
#
|
36
|
-
# user = User.
|
36
|
+
# user = User.find_by(name: 'Phusion')
|
37
37
|
# user_path(user) # => "/users/Phusion"
|
38
38
|
def to_param
|
39
39
|
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
|
@@ -49,7 +49,7 @@ module ActiveRecord
|
|
49
49
|
case
|
50
50
|
when new_record?
|
51
51
|
"#{self.class.model_name.cache_key}/new"
|
52
|
-
when timestamp =
|
52
|
+
when timestamp = max_updated_column_timestamp
|
53
53
|
timestamp = timestamp.utc.to_s(cache_timestamp_format)
|
54
54
|
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
|
55
55
|
else
|
@@ -86,7 +86,7 @@ module ActiveRecord
|
|
86
86
|
)
|
87
87
|
).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
|
88
88
|
|
89
|
-
affected_rows = connection.update stmt
|
89
|
+
affected_rows = self.class.connection.update stmt
|
90
90
|
|
91
91
|
unless affected_rows == 1
|
92
92
|
raise ActiveRecord::StaleObjectError.new(self, "update")
|
@@ -117,7 +117,7 @@ module ActiveRecord
|
|
117
117
|
if locking_enabled?
|
118
118
|
column_name = self.class.locking_column
|
119
119
|
column = self.class.columns_hash[column_name]
|
120
|
-
substitute = connection.substitute_at(column, relation.bind_values.length)
|
120
|
+
substitute = self.class.connection.substitute_at(column, relation.bind_values.length)
|
121
121
|
|
122
122
|
relation = relation.where(self.class.arel_table[column_name].eq(substitute))
|
123
123
|
relation.bind_values << [column, self[column_name].to_i]
|
@@ -3,11 +3,11 @@ module ActiveRecord
|
|
3
3
|
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
|
4
4
|
|
5
5
|
def self.runtime=(value)
|
6
|
-
|
6
|
+
ActiveRecord::RuntimeRegistry.sql_runtime = value
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.runtime
|
10
|
-
|
10
|
+
ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.reset_runtime
|
@@ -102,7 +102,7 @@ module ActiveRecord
|
|
102
102
|
# table definition.
|
103
103
|
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
|
104
104
|
# * <tt>change_table(name, options)</tt>: Allows to make column alterations to
|
105
|
-
# the table called +name+. It makes the table object
|
105
|
+
# the table called +name+. It makes the table object available to a block that
|
106
106
|
# can then add/remove columns, indexes or foreign keys to it.
|
107
107
|
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
|
108
108
|
# to +new_name+.
|
@@ -330,6 +330,24 @@ module ActiveRecord
|
|
330
330
|
#
|
331
331
|
# For a list of commands that are reversible, please see
|
332
332
|
# <tt>ActiveRecord::Migration::CommandRecorder</tt>.
|
333
|
+
#
|
334
|
+
# == Transactional Migrations
|
335
|
+
#
|
336
|
+
# If the database adapter supports DDL transactions, all migrations will
|
337
|
+
# automatically be wrapped in a transaction. There are queries that you
|
338
|
+
# can't execute inside a transaction though, and for these situations
|
339
|
+
# you can turn the automatic transactions off.
|
340
|
+
#
|
341
|
+
# class ChangeEnum < ActiveRecord::Migration
|
342
|
+
# disable_ddl_transaction!
|
343
|
+
#
|
344
|
+
# def up
|
345
|
+
# execute "ALTER TYPE model_size ADD VALUE 'new_value'"
|
346
|
+
# end
|
347
|
+
# end
|
348
|
+
#
|
349
|
+
# Remember that you can still open your own transactions, even if you
|
350
|
+
# are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
|
333
351
|
class Migration
|
334
352
|
autoload :CommandRecorder, 'active_record/migration/command_recorder'
|
335
353
|
|
@@ -351,6 +369,7 @@ module ActiveRecord
|
|
351
369
|
|
352
370
|
class << self
|
353
371
|
attr_accessor :delegate # :nodoc:
|
372
|
+
attr_accessor :disable_ddl_transaction # :nodoc:
|
354
373
|
end
|
355
374
|
|
356
375
|
def self.check_pending!
|
@@ -365,8 +384,16 @@ module ActiveRecord
|
|
365
384
|
new.migrate direction
|
366
385
|
end
|
367
386
|
|
368
|
-
|
387
|
+
# Disable DDL transactions for this migration.
|
388
|
+
def self.disable_ddl_transaction!
|
389
|
+
@disable_ddl_transaction = true
|
390
|
+
end
|
391
|
+
|
392
|
+
def disable_ddl_transaction # :nodoc:
|
393
|
+
self.class.disable_ddl_transaction
|
394
|
+
end
|
369
395
|
|
396
|
+
cattr_accessor :verbose
|
370
397
|
attr_accessor :name, :version
|
371
398
|
|
372
399
|
def initialize(name = self.class.name, version = nil)
|
@@ -375,8 +402,8 @@ module ActiveRecord
|
|
375
402
|
@connection = nil
|
376
403
|
end
|
377
404
|
|
405
|
+
self.verbose = true
|
378
406
|
# instantiate the delegate object after initialize is defined
|
379
|
-
self.verbose = true
|
380
407
|
self.delegate = new
|
381
408
|
|
382
409
|
# Reverses the migration commands for the given block and
|
@@ -439,7 +466,7 @@ module ActiveRecord
|
|
439
466
|
@connection.respond_to?(:reverting) && @connection.reverting
|
440
467
|
end
|
441
468
|
|
442
|
-
class ReversibleBlockHelper < Struct.new(:reverting)
|
469
|
+
class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
|
443
470
|
def up
|
444
471
|
yield unless reverting
|
445
472
|
end
|
@@ -607,8 +634,17 @@ module ActiveRecord
|
|
607
634
|
source_migrations = ActiveRecord::Migrator.migrations(path)
|
608
635
|
|
609
636
|
source_migrations.each do |migration|
|
610
|
-
source = File.
|
611
|
-
|
637
|
+
source = File.binread(migration.filename)
|
638
|
+
inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
|
639
|
+
if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source
|
640
|
+
# If we have a magic comment in the original migration,
|
641
|
+
# insert our comment after the first newline(end of the magic comment line)
|
642
|
+
# so the magic keep working.
|
643
|
+
# Note that magic comments must be at the first line(except sh-bang).
|
644
|
+
source[/\n/] = "\n#{inserted_comment}"
|
645
|
+
else
|
646
|
+
source = "#{inserted_comment}#{source}"
|
647
|
+
end
|
612
648
|
|
613
649
|
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
|
614
650
|
if options[:on_skip] && duplicate.scope != scope.to_s
|
@@ -622,7 +658,7 @@ module ActiveRecord
|
|
622
658
|
old_path, migration.filename = migration.filename, new_path
|
623
659
|
last = migration
|
624
660
|
|
625
|
-
File.
|
661
|
+
File.binwrite(migration.filename, source)
|
626
662
|
copied << migration
|
627
663
|
options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
|
628
664
|
destination_migrations << migration
|
@@ -663,7 +699,7 @@ module ActiveRecord
|
|
663
699
|
File.basename(filename)
|
664
700
|
end
|
665
701
|
|
666
|
-
delegate :migrate, :announce, :write, :to
|
702
|
+
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
|
667
703
|
|
668
704
|
private
|
669
705
|
|
@@ -822,7 +858,7 @@ module ActiveRecord
|
|
822
858
|
end
|
823
859
|
|
824
860
|
def current_version
|
825
|
-
migrated.
|
861
|
+
migrated.max || 0
|
826
862
|
end
|
827
863
|
|
828
864
|
def current_migration
|
@@ -831,11 +867,15 @@ module ActiveRecord
|
|
831
867
|
alias :current :current_migration
|
832
868
|
|
833
869
|
def run
|
834
|
-
|
835
|
-
raise UnknownMigrationVersionError.new(@target_version) if
|
836
|
-
unless (up? && migrated.include?(
|
837
|
-
|
838
|
-
|
870
|
+
migration = migrations.detect { |m| m.version == @target_version }
|
871
|
+
raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
|
872
|
+
unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
|
873
|
+
begin
|
874
|
+
execute_migration_in_transaction(migration, @direction)
|
875
|
+
rescue => e
|
876
|
+
canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
|
877
|
+
raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
|
878
|
+
end
|
839
879
|
end
|
840
880
|
end
|
841
881
|
|
@@ -856,12 +896,9 @@ module ActiveRecord
|
|
856
896
|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
|
857
897
|
|
858
898
|
begin
|
859
|
-
|
860
|
-
migration.migrate(@direction)
|
861
|
-
record_version_state_after_migrating(migration.version)
|
862
|
-
end
|
899
|
+
execute_migration_in_transaction(migration, @direction)
|
863
900
|
rescue => e
|
864
|
-
canceled_msg =
|
901
|
+
canceled_msg = use_transaction?(migration) ? "this and " : ""
|
865
902
|
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
|
866
903
|
end
|
867
904
|
end
|
@@ -896,6 +933,13 @@ module ActiveRecord
|
|
896
933
|
migrated.include?(migration.version.to_i)
|
897
934
|
end
|
898
935
|
|
936
|
+
def execute_migration_in_transaction(migration, direction)
|
937
|
+
ddl_transaction(migration) do
|
938
|
+
migration.migrate(direction)
|
939
|
+
record_version_state_after_migrating(migration.version)
|
940
|
+
end
|
941
|
+
end
|
942
|
+
|
899
943
|
def target
|
900
944
|
migrations.detect { |m| m.version == @target_version }
|
901
945
|
end
|
@@ -935,12 +979,16 @@ module ActiveRecord
|
|
935
979
|
end
|
936
980
|
|
937
981
|
# Wrap the migration in a transaction only if supported by the adapter.
|
938
|
-
def ddl_transaction
|
939
|
-
if
|
982
|
+
def ddl_transaction(migration)
|
983
|
+
if use_transaction?(migration)
|
940
984
|
Base.transaction { yield }
|
941
985
|
else
|
942
986
|
yield
|
943
987
|
end
|
944
988
|
end
|
989
|
+
|
990
|
+
def use_transaction?(migration)
|
991
|
+
!migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
|
992
|
+
end
|
945
993
|
end
|
946
994
|
end
|
@@ -205,7 +205,7 @@ module ActiveRecord
|
|
205
205
|
|
206
206
|
# Returns an array of column objects for the table associated with this class.
|
207
207
|
def columns
|
208
|
-
@columns ||= connection.schema_cache.columns
|
208
|
+
@columns ||= connection.schema_cache.columns(table_name).map do |col|
|
209
209
|
col = col.dup
|
210
210
|
col.primary = (col.name == primary_key)
|
211
211
|
col
|
@@ -90,8 +90,9 @@ module ActiveRecord
|
|
90
90
|
# accepts_nested_attributes_for :posts
|
91
91
|
# end
|
92
92
|
#
|
93
|
-
# You can now set or update attributes on
|
94
|
-
#
|
93
|
+
# You can now set or update attributes on the associated posts through
|
94
|
+
# an attribute hash for a member: include the key +:posts_attributes+
|
95
|
+
# with an array of hashes of post attributes as a value.
|
95
96
|
#
|
96
97
|
# For each hash that does _not_ have an <tt>id</tt> key a new record will
|
97
98
|
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
|
@@ -114,10 +115,10 @@ module ActiveRecord
|
|
114
115
|
# hashes if they fail to pass your criteria. For example, the previous
|
115
116
|
# example could be rewritten as:
|
116
117
|
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
118
|
+
# class Member < ActiveRecord::Base
|
119
|
+
# has_many :posts
|
120
|
+
# accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
|
121
|
+
# end
|
121
122
|
#
|
122
123
|
# params = { member: {
|
123
124
|
# name: 'joe', posts_attributes: [
|
@@ -134,19 +135,19 @@ module ActiveRecord
|
|
134
135
|
#
|
135
136
|
# Alternatively, :reject_if also accepts a symbol for using methods:
|
136
137
|
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
#
|
138
|
+
# class Member < ActiveRecord::Base
|
139
|
+
# has_many :posts
|
140
|
+
# accepts_nested_attributes_for :posts, reject_if: :new_record?
|
141
|
+
# end
|
141
142
|
#
|
142
|
-
#
|
143
|
-
#
|
144
|
-
#
|
143
|
+
# class Member < ActiveRecord::Base
|
144
|
+
# has_many :posts
|
145
|
+
# accepts_nested_attributes_for :posts, reject_if: :reject_posts
|
145
146
|
#
|
146
|
-
#
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
147
|
+
# def reject_posts(attributed)
|
148
|
+
# attributed['title'].blank?
|
149
|
+
# end
|
150
|
+
# end
|
150
151
|
#
|
151
152
|
# If the hash contains an <tt>id</tt> key that matches an already
|
152
153
|
# associated record, the matching record will be modified:
|
@@ -183,6 +184,29 @@ module ActiveRecord
|
|
183
184
|
# member.save
|
184
185
|
# member.reload.posts.length # => 1
|
185
186
|
#
|
187
|
+
# Nested attributes for an associated collection can also be passed in
|
188
|
+
# the form of a hash of hashes instead of an array of hashes:
|
189
|
+
#
|
190
|
+
# Member.create(name: 'joe',
|
191
|
+
# posts_attributes: { first: { title: 'Foo' },
|
192
|
+
# second: { title: 'Bar' } })
|
193
|
+
#
|
194
|
+
# has the same effect as
|
195
|
+
#
|
196
|
+
# Member.create(name: 'joe',
|
197
|
+
# posts_attributes: [ { title: 'Foo' },
|
198
|
+
# { title: 'Bar' } ])
|
199
|
+
#
|
200
|
+
# The keys of the hash which is the value for +:posts_attributes+ are
|
201
|
+
# ignored in this case.
|
202
|
+
# However, it is not allowed to use +'id'+ or +:id+ for one of
|
203
|
+
# such keys, otherwise the hash will be wrapped in an array and
|
204
|
+
# interpreted as an attribute hash for a single post.
|
205
|
+
#
|
206
|
+
# Passing attributes for an associated collection in the form of a hash
|
207
|
+
# of hashes can be used with hashes generated from HTTP/HTML parameters,
|
208
|
+
# where there maybe no natural way to submit an array of hashes.
|
209
|
+
#
|
186
210
|
# === Saving
|
187
211
|
#
|
188
212
|
# All changes to models, including the destruction of those marked for
|
@@ -269,23 +293,36 @@ module ActiveRecord
|
|
269
293
|
self.nested_attributes_options = nested_attributes_options
|
270
294
|
|
271
295
|
type = (reflection.collection? ? :collection : :one_to_one)
|
272
|
-
|
273
|
-
# def pirate_attributes=(attributes)
|
274
|
-
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
|
275
|
-
# end
|
276
|
-
generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
|
277
|
-
if method_defined?(:#{association_name}_attributes=)
|
278
|
-
remove_method(:#{association_name}_attributes=)
|
279
|
-
end
|
280
|
-
def #{association_name}_attributes=(attributes)
|
281
|
-
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
282
|
-
end
|
283
|
-
eoruby
|
296
|
+
generate_association_writer(association_name, type)
|
284
297
|
else
|
285
298
|
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
286
299
|
end
|
287
300
|
end
|
288
301
|
end
|
302
|
+
|
303
|
+
private
|
304
|
+
|
305
|
+
# Generates a writer method for this association. Serves as a point for
|
306
|
+
# accessing the objects in the association. For example, this method
|
307
|
+
# could generate the following:
|
308
|
+
#
|
309
|
+
# def pirate_attributes=(attributes)
|
310
|
+
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
|
311
|
+
# end
|
312
|
+
#
|
313
|
+
# This redirects the attempts to write objects in an association through
|
314
|
+
# the helper methods defined below. Makes it seem like the nested
|
315
|
+
# associations are just regular associations.
|
316
|
+
def generate_association_writer(association_name, type)
|
317
|
+
generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
|
318
|
+
if method_defined?(:#{association_name}_attributes=)
|
319
|
+
remove_method(:#{association_name}_attributes=)
|
320
|
+
end
|
321
|
+
def #{association_name}_attributes=(attributes)
|
322
|
+
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
323
|
+
end
|
324
|
+
eoruby
|
325
|
+
end
|
289
326
|
end
|
290
327
|
|
291
328
|
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
|
@@ -325,7 +362,7 @@ module ActiveRecord
|
|
325
362
|
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
|
326
363
|
|
327
364
|
elsif attributes['id'].present?
|
328
|
-
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
365
|
+
raise_nested_attributes_record_not_found!(association_name, attributes['id'])
|
329
366
|
|
330
367
|
elsif !reject_new_record?(association_name, attributes)
|
331
368
|
method = "build_#{association_name}"
|
@@ -371,20 +408,7 @@ module ActiveRecord
|
|
371
408
|
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
372
409
|
end
|
373
410
|
|
374
|
-
|
375
|
-
limit = case limit
|
376
|
-
when Symbol
|
377
|
-
send(limit)
|
378
|
-
when Proc
|
379
|
-
limit.call
|
380
|
-
else
|
381
|
-
limit
|
382
|
-
end
|
383
|
-
|
384
|
-
if limit && attributes_collection.size > limit
|
385
|
-
raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
|
386
|
-
end
|
387
|
-
end
|
411
|
+
check_record_limit!(options[:limit], attributes_collection)
|
388
412
|
|
389
413
|
if attributes_collection.is_a? Hash
|
390
414
|
keys = attributes_collection.keys
|
@@ -428,7 +452,30 @@ module ActiveRecord
|
|
428
452
|
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
429
453
|
end
|
430
454
|
else
|
431
|
-
raise_nested_attributes_record_not_found(association_name, attributes['id'])
|
455
|
+
raise_nested_attributes_record_not_found!(association_name, attributes['id'])
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Takes in a limit and checks if the attributes_collection has too many
|
461
|
+
# records. The method will take limits in the form of symbols, procs, and
|
462
|
+
# number-like objects (anything that can be compared with an integer).
|
463
|
+
#
|
464
|
+
# Will raise an TooManyRecords error if the attributes_collection is
|
465
|
+
# larger than the limit.
|
466
|
+
def check_record_limit!(limit, attributes_collection)
|
467
|
+
if limit
|
468
|
+
limit = case limit
|
469
|
+
when Symbol
|
470
|
+
send(limit)
|
471
|
+
when Proc
|
472
|
+
limit.call
|
473
|
+
else
|
474
|
+
limit
|
475
|
+
end
|
476
|
+
|
477
|
+
if limit && attributes_collection.size > limit
|
478
|
+
raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
|
432
479
|
end
|
433
480
|
end
|
434
481
|
end
|
@@ -452,6 +499,11 @@ module ActiveRecord
|
|
452
499
|
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
|
453
500
|
end
|
454
501
|
|
502
|
+
# Determines if a record with the particular +attributes+ should be
|
503
|
+
# rejected by calling the reject_if Symbol or Proc (if defined).
|
504
|
+
# The reject_if option is defined by +accepts_nested_attributes_for+.
|
505
|
+
#
|
506
|
+
# Returns false if there is a +destroy_flag+ on the attributes.
|
455
507
|
def call_reject_if(association_name, attributes)
|
456
508
|
return false if has_destroy_flag?(attributes)
|
457
509
|
case callback = self.nested_attributes_options[association_name][:reject_if]
|
@@ -462,7 +514,7 @@ module ActiveRecord
|
|
462
514
|
end
|
463
515
|
end
|
464
516
|
|
465
|
-
def raise_nested_attributes_record_not_found(association_name, record_id)
|
517
|
+
def raise_nested_attributes_record_not_found!(association_name, record_id)
|
466
518
|
raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
|
467
519
|
end
|
468
520
|
end
|