activerecord 3.1.9 → 3.2.12

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.
Files changed (112) hide show
  1. checksums.yaml +6 -6
  2. data/CHANGELOG.md +317 -336
  3. data/README.rdoc +3 -3
  4. data/examples/performance.rb +20 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +3 -6
  7. data/lib/active_record/associations/association.rb +3 -42
  8. data/lib/active_record/associations/association_scope.rb +3 -15
  9. data/lib/active_record/associations/builder/association.rb +6 -4
  10. data/lib/active_record/associations/builder/belongs_to.rb +3 -3
  11. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +4 -4
  13. data/lib/active_record/associations/builder/has_one.rb +5 -6
  14. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  15. data/lib/active_record/associations/collection_association.rb +64 -31
  16. data/lib/active_record/associations/collection_proxy.rb +2 -37
  17. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
  18. data/lib/active_record/associations/has_many_association.rb +5 -1
  19. data/lib/active_record/associations/has_many_through_association.rb +28 -9
  20. data/lib/active_record/associations/has_one_association.rb +15 -13
  21. data/lib/active_record/associations/join_dependency.rb +2 -2
  22. data/lib/active_record/associations/preloader.rb +14 -10
  23. data/lib/active_record/associations/through_association.rb +7 -3
  24. data/lib/active_record/associations.rb +92 -76
  25. data/lib/active_record/attribute_assignment.rb +221 -0
  26. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  27. data/lib/active_record/attribute_methods/dirty.rb +21 -11
  28. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  29. data/lib/active_record/attribute_methods/read.rb +73 -83
  30. data/lib/active_record/attribute_methods/serialization.rb +102 -0
  31. data/lib/active_record/attribute_methods/time_zone_conversion.rb +23 -17
  32. data/lib/active_record/attribute_methods/write.rb +31 -6
  33. data/lib/active_record/attribute_methods.rb +231 -30
  34. data/lib/active_record/autosave_association.rb +43 -22
  35. data/lib/active_record/base.rb +227 -1708
  36. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
  37. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
  38. data/lib/active_record/connection_adapters/abstract/database_statements.rb +6 -33
  39. data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
  40. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -6
  41. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -26
  42. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
  43. data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
  44. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +674 -0
  45. data/lib/active_record/connection_adapters/column.rb +37 -11
  46. data/lib/active_record/connection_adapters/mysql2_adapter.rb +129 -581
  47. data/lib/active_record/connection_adapters/mysql_adapter.rb +137 -696
  48. data/lib/active_record/connection_adapters/postgresql_adapter.rb +184 -86
  49. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  50. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  51. data/lib/active_record/connection_adapters/sqlite_adapter.rb +55 -32
  52. data/lib/active_record/counter_cache.rb +9 -4
  53. data/lib/active_record/dynamic_finder_match.rb +12 -0
  54. data/lib/active_record/dynamic_matchers.rb +84 -0
  55. data/lib/active_record/errors.rb +11 -1
  56. data/lib/active_record/explain.rb +85 -0
  57. data/lib/active_record/explain_subscriber.rb +25 -0
  58. data/lib/active_record/fixtures/file.rb +65 -0
  59. data/lib/active_record/fixtures.rb +56 -85
  60. data/lib/active_record/identity_map.rb +3 -4
  61. data/lib/active_record/inheritance.rb +174 -0
  62. data/lib/active_record/integration.rb +49 -0
  63. data/lib/active_record/locking/optimistic.rb +30 -25
  64. data/lib/active_record/locking/pessimistic.rb +23 -1
  65. data/lib/active_record/log_subscriber.rb +3 -3
  66. data/lib/active_record/migration/command_recorder.rb +8 -8
  67. data/lib/active_record/migration.rb +68 -35
  68. data/lib/active_record/model_schema.rb +366 -0
  69. data/lib/active_record/nested_attributes.rb +3 -2
  70. data/lib/active_record/persistence.rb +57 -11
  71. data/lib/active_record/querying.rb +58 -0
  72. data/lib/active_record/railtie.rb +31 -29
  73. data/lib/active_record/railties/controller_runtime.rb +3 -1
  74. data/lib/active_record/railties/databases.rake +191 -110
  75. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  76. data/lib/active_record/readonly_attributes.rb +26 -0
  77. data/lib/active_record/reflection.rb +7 -15
  78. data/lib/active_record/relation/batches.rb +5 -2
  79. data/lib/active_record/relation/calculations.rb +47 -15
  80. data/lib/active_record/relation/delegation.rb +49 -0
  81. data/lib/active_record/relation/finder_methods.rb +9 -7
  82. data/lib/active_record/relation/predicate_builder.rb +18 -7
  83. data/lib/active_record/relation/query_methods.rb +75 -9
  84. data/lib/active_record/relation/spawn_methods.rb +11 -2
  85. data/lib/active_record/relation.rb +78 -32
  86. data/lib/active_record/result.rb +1 -1
  87. data/lib/active_record/sanitization.rb +194 -0
  88. data/lib/active_record/schema_dumper.rb +12 -5
  89. data/lib/active_record/scoping/default.rb +142 -0
  90. data/lib/active_record/scoping/named.rb +202 -0
  91. data/lib/active_record/scoping.rb +152 -0
  92. data/lib/active_record/serialization.rb +1 -43
  93. data/lib/active_record/serializers/xml_serializer.rb +4 -45
  94. data/lib/active_record/session_store.rb +17 -15
  95. data/lib/active_record/store.rb +52 -0
  96. data/lib/active_record/test_case.rb +11 -7
  97. data/lib/active_record/timestamp.rb +17 -3
  98. data/lib/active_record/transactions.rb +27 -6
  99. data/lib/active_record/translation.rb +22 -0
  100. data/lib/active_record/validations/associated.rb +5 -4
  101. data/lib/active_record/validations/uniqueness.rb +7 -7
  102. data/lib/active_record/validations.rb +1 -1
  103. data/lib/active_record/version.rb +2 -2
  104. data/lib/active_record.rb +38 -3
  105. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  106. data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
  107. data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
  108. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  109. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  110. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  111. metadata +30 -10
  112. data/lib/active_record/named_scope.rb +0 -200
@@ -0,0 +1,102 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module Serialization
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # Returns a hash of all the attributes that have been specified for serialization as
8
+ # keys and their class restriction as values.
9
+ class_attribute :serialized_attributes
10
+ self.serialized_attributes = {}
11
+ end
12
+
13
+ class Attribute < Struct.new(:coder, :value, :state)
14
+ def unserialized_value
15
+ state == :serialized ? unserialize : value
16
+ end
17
+
18
+ def serialized_value
19
+ state == :unserialized ? serialize : value
20
+ end
21
+
22
+ def unserialize
23
+ self.state = :unserialized
24
+ self.value = coder.load(value)
25
+ end
26
+
27
+ def serialize
28
+ self.state = :serialized
29
+ self.value = coder.dump(value)
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
35
+ # then specify the name of that attribute using this method and it will be handled automatically.
36
+ # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
37
+ # class on retrieval or SerializationTypeMismatch will be raised.
38
+ #
39
+ # ==== Parameters
40
+ #
41
+ # * +attr_name+ - The field name that should be serialized.
42
+ # * +class_name+ - Optional, class name that the object type should be equal to.
43
+ #
44
+ # ==== Example
45
+ # # Serialize a preferences attribute
46
+ # class User < ActiveRecord::Base
47
+ # serialize :preferences
48
+ # end
49
+ def serialize(attr_name, class_name = Object)
50
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
51
+ class_name
52
+ else
53
+ Coders::YAMLColumn.new(class_name)
54
+ end
55
+
56
+ # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
57
+ # has its own hash of own serialized attributes
58
+ self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
59
+ end
60
+
61
+ def initialize_attributes(attributes, options = {}) #:nodoc:
62
+ serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
63
+ super(attributes, options)
64
+
65
+ serialized_attributes.each do |key, coder|
66
+ if attributes.key?(key)
67
+ attributes[key] = Attribute.new(coder, attributes[key], serialized)
68
+ end
69
+ end
70
+
71
+ attributes
72
+ end
73
+
74
+ private
75
+
76
+ def attribute_cast_code(attr_name)
77
+ if serialized_attributes.include?(attr_name)
78
+ "v.unserialized_value"
79
+ else
80
+ super
81
+ end
82
+ end
83
+ end
84
+
85
+ def type_cast_attribute_for_write(column, value)
86
+ if column && coder = self.class.serialized_attributes[column.name]
87
+ Attribute.new(coder, value, :unserialized)
88
+ else
89
+ super
90
+ end
91
+ end
92
+
93
+ def read_attribute_before_type_cast(attr_name)
94
+ if serialized_attributes.include?(attr_name)
95
+ super.unserialized_value
96
+ else
97
+ super
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -16,21 +16,16 @@ module ActiveRecord
16
16
 
17
17
  module ClassMethods
18
18
  protected
19
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
20
- # This enhanced read method automatically converts the UTC time stored in the database to the time
19
+ # The enhanced read method automatically converts the UTC time stored in the database to the time
21
20
  # zone stored in Time.zone.
22
- def define_method_attribute(attr_name)
23
- if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
24
- method_body, line = <<-EOV, __LINE__ + 1
25
- def _#{attr_name}
26
- cached = @attributes_cache['#{attr_name}']
27
- return cached if cached
28
- time = _read_attribute('#{attr_name}')
29
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
30
- end
31
- alias #{attr_name} _#{attr_name}
32
- EOV
33
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
21
+ def attribute_cast_code(attr_name)
22
+ column = columns_hash[attr_name]
23
+
24
+ if create_time_zone_conversion_attribute?(attr_name, column)
25
+ typecast = "v = #{super}"
26
+ time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v"
27
+
28
+ "((#{typecast}) && (#{time_zone_conversion}))"
34
29
  else
35
30
  super
36
31
  end
@@ -46,9 +41,14 @@ module ActiveRecord
46
41
  unless time.acts_like?(:time)
47
42
  time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
48
43
  end
49
- time = time.in_time_zone rescue nil if time
50
- write_attribute(:#{attr_name}, original_time)
51
- @attributes_cache["#{attr_name}"] = time
44
+ zoned_time = time && time.in_time_zone rescue nil
45
+ rounded_time = round_usec(zoned_time)
46
+ rounded_value = round_usec(read_attribute("#{attr_name}"))
47
+ if (rounded_value != rounded_time) || (!rounded_value && original_time)
48
+ write_attribute("#{attr_name}", original_time)
49
+ #{attr_name}_will_change!
50
+ @attributes_cache["#{attr_name}"] = zoned_time
51
+ end
52
52
  end
53
53
  EOV
54
54
  generated_attribute_methods.module_eval(method_body, __FILE__, line)
@@ -62,6 +62,12 @@ module ActiveRecord
62
62
  time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp])
63
63
  end
64
64
  end
65
+
66
+ private
67
+ def round_usec(value)
68
+ return unless value
69
+ value.change(:usec => 0)
70
+ end
65
71
  end
66
72
  end
67
73
  end
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  module ClassMethods
11
11
  protected
12
12
  def define_method_attribute=(attr_name)
13
- if attr_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
13
+ if attr_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
14
14
  generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
15
15
  else
16
16
  generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
@@ -24,13 +24,18 @@ module ActiveRecord
24
24
  # for fixnum and float columns are turned into +nil+.
25
25
  def write_attribute(attr_name, value)
26
26
  attr_name = attr_name.to_s
27
- attr_name = self.class.primary_key if attr_name == 'id'
27
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
28
28
  @attributes_cache.delete(attr_name)
29
- if (column = column_for_attribute(attr_name)) && column.number?
30
- @attributes[attr_name] = convert_number_column_value(value)
31
- else
32
- @attributes[attr_name] = value
29
+ column = column_for_attribute(attr_name)
30
+
31
+ unless column || @attributes.has_key?(attr_name)
32
+ ActiveSupport::Deprecation.warn(
33
+ "You're trying to create an attribute `#{attr_name}'. Writing arbitrary " \
34
+ "attributes on a model is deprecated. Please just use `attr_writer` etc."
35
+ )
33
36
  end
37
+
38
+ @attributes[attr_name] = type_cast_attribute_for_write(column, value)
34
39
  end
35
40
  alias_method :raw_write_attribute, :write_attribute
36
41
 
@@ -39,6 +44,26 @@ module ActiveRecord
39
44
  def attribute=(attribute_name, value)
40
45
  write_attribute(attribute_name, value)
41
46
  end
47
+
48
+ def type_cast_attribute_for_write(column, value)
49
+ if column && column.number?
50
+ convert_number_column_value(value)
51
+ else
52
+ value
53
+ end
54
+ end
55
+
56
+ def convert_number_column_value(value)
57
+ if value == false
58
+ 0
59
+ elsif value == true
60
+ 1
61
+ elsif value.is_a?(String) && value.blank?
62
+ nil
63
+ else
64
+ value
65
+ end
66
+ end
42
67
  end
43
68
  end
44
69
  end
@@ -1,4 +1,5 @@
1
1
  require 'active_support/core_ext/enumerable'
2
+ require 'active_support/deprecation'
2
3
 
3
4
  module ActiveRecord
4
5
  # = Active Record Attribute Methods
@@ -6,69 +7,269 @@ module ActiveRecord
6
7
  extend ActiveSupport::Concern
7
8
  include ActiveModel::AttributeMethods
8
9
 
10
+ included do
11
+ include Read
12
+ include Write
13
+ include BeforeTypeCast
14
+ include Query
15
+ include PrimaryKey
16
+ include TimeZoneConversion
17
+ include Dirty
18
+ include Serialization
19
+ include DeprecatedUnderscoreRead
20
+
21
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
22
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
23
+ # (Alias for the protected read_attribute method).
24
+ def [](attr_name)
25
+ read_attribute(attr_name)
26
+ end
27
+
28
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
29
+ # (Alias for the protected write_attribute method).
30
+ def []=(attr_name, value)
31
+ write_attribute(attr_name, value)
32
+ end
33
+ end
34
+
9
35
  module ClassMethods
10
36
  # Generates all the attribute related methods for columns in the database
11
37
  # accessors, mutators and query methods.
12
38
  def define_attribute_methods
13
- return if attribute_methods_generated?
14
- super(column_names)
15
- @attribute_methods_generated = true
39
+ unless defined?(@attribute_methods_mutex)
40
+ msg = "It looks like something (probably a gem/plugin) is overriding the " \
41
+ "ActiveRecord::Base.inherited method. It is important that this hook executes so " \
42
+ "that your models are set up correctly. A workaround has been added to stop this " \
43
+ "causing an error in 3.2, but future versions will simply not work if the hook is " \
44
+ "overridden. If you are using Kaminari, please upgrade as it is known to have had " \
45
+ "this problem.\n\n"
46
+ msg << "The following may help track down the problem:"
47
+
48
+ meth = method(:inherited)
49
+ if meth.respond_to?(:source_location)
50
+ msg << " #{meth.source_location.inspect}"
51
+ else
52
+ msg << " #{meth.inspect}"
53
+ end
54
+ msg << "\n\n"
55
+
56
+ ActiveSupport::Deprecation.warn(msg)
57
+
58
+ @attribute_methods_mutex = Mutex.new
59
+ end
60
+
61
+ # Use a mutex; we don't want two thread simaltaneously trying to define
62
+ # attribute methods.
63
+ @attribute_methods_mutex.synchronize do
64
+ return if attribute_methods_generated?
65
+ superclass.define_attribute_methods unless self == base_class
66
+ super(column_names)
67
+ column_names.each { |name| define_external_attribute_method(name) }
68
+ @attribute_methods_generated = true
69
+ end
16
70
  end
17
71
 
18
72
  def attribute_methods_generated?
19
73
  @attribute_methods_generated ||= false
20
74
  end
21
75
 
22
- def undefine_attribute_methods(*args)
76
+ # We will define the methods as instance methods, but will call them as singleton
77
+ # methods. This allows us to use method_defined? to check if the method exists,
78
+ # which is fast and won't give any false positives from the ancestors (because
79
+ # there are no ancestors).
80
+ def generated_external_attribute_methods
81
+ @generated_external_attribute_methods ||= Module.new { extend self }
82
+ end
83
+
84
+ def undefine_attribute_methods
23
85
  super
24
86
  @attribute_methods_generated = false
25
87
  end
26
88
 
27
- # Checks whether the method is defined in the model or any of its subclasses
28
- # that also derive from Active Record. Raises DangerousAttributeError if the
29
- # method is defined by Active Record though.
30
89
  def instance_method_already_implemented?(method_name)
31
- method_name = method_name.to_s
32
- index = ancestors.index(ActiveRecord::Base) || ancestors.length
33
- @_defined_class_methods ||= ancestors.first(index).map { |m|
34
- m.instance_methods(false) | m.private_instance_methods(false)
35
- }.flatten.map {|m| m.to_s }.to_set
90
+ if dangerous_attribute_method?(method_name)
91
+ raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
92
+ end
93
+
94
+ if superclass == Base
95
+ super
96
+ else
97
+ # If B < A and A defines its own attribute method, then we don't want to overwrite that.
98
+ defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
99
+ defined && !ActiveRecord::Base.method_defined?(method_name) || super
100
+ end
101
+ end
36
102
 
37
- @@_defined_activerecord_methods ||= defined_activerecord_methods
38
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
39
- @_defined_class_methods.include?(method_name)
103
+ # A method name is 'dangerous' if it is already defined by Active Record, but
104
+ # not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
105
+ def dangerous_attribute_method?(name)
106
+ method_defined_within?(name, Base)
40
107
  end
41
108
 
42
- def defined_activerecord_methods
43
- active_record = ActiveRecord::Base
44
- super_klass = ActiveRecord::Base.superclass
45
- methods = (active_record.instance_methods - super_klass.instance_methods) +
46
- (active_record.private_instance_methods - super_klass.private_instance_methods)
47
- methods.map {|m| m.to_s }.to_set
109
+ def method_defined_within?(name, klass, sup = klass.superclass)
110
+ if klass.method_defined?(name) || klass.private_method_defined?(name)
111
+ if sup.method_defined?(name) || sup.private_method_defined?(name)
112
+ klass.instance_method(name).owner != sup.instance_method(name).owner
113
+ else
114
+ true
115
+ end
116
+ else
117
+ false
118
+ end
119
+ end
120
+
121
+ def attribute_method?(attribute)
122
+ super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
123
+ end
124
+
125
+ # Returns an array of column names as strings if it's not
126
+ # an abstract class and table exists.
127
+ # Otherwise it returns an empty array.
128
+ def attribute_names
129
+ @attribute_names ||= if !abstract_class? && table_exists?
130
+ column_names
131
+ else
132
+ []
133
+ end
48
134
  end
49
135
  end
50
136
 
51
- def method_missing(method_id, *args, &block)
52
- # If we haven't generated any methods yet, generate them, then
53
- # see if we've created the method we're looking for.
54
- if !self.class.attribute_methods_generated?
137
+ # If we haven't generated any methods yet, generate them, then
138
+ # see if we've created the method we're looking for.
139
+ def method_missing(method, *args, &block)
140
+ unless self.class.attribute_methods_generated?
55
141
  self.class.define_attribute_methods
56
- method_name = method_id.to_s
57
- guard_private_attribute_method!(method_name, args)
58
- send(method_id, *args, &block)
142
+
143
+ if respond_to_without_attributes?(method)
144
+ send(method, *args, &block)
145
+ else
146
+ super
147
+ end
59
148
  else
60
149
  super
61
150
  end
62
151
  end
63
152
 
153
+ def attribute_missing(match, *args, &block)
154
+ if self.class.columns_hash[match.attr_name]
155
+ ActiveSupport::Deprecation.warn(
156
+ "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
157
+ "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
158
+ "is a column of the table. If this error has happened through normal usage of Active " \
159
+ "Record (rather than through your own code or external libraries), please report it as " \
160
+ "a bug."
161
+ )
162
+ end
163
+
164
+ super
165
+ end
166
+
64
167
  def respond_to?(name, include_private = false)
65
168
  self.class.define_attribute_methods unless self.class.attribute_methods_generated?
66
169
  super
67
170
  end
68
171
 
172
+ # Returns true if the given attribute is in the attributes hash
173
+ def has_attribute?(attr_name)
174
+ @attributes.has_key?(attr_name.to_s)
175
+ end
176
+
177
+ # Returns an array of names for the attributes available on this object.
178
+ def attribute_names
179
+ @attributes.keys
180
+ end
181
+
182
+ # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
183
+ def attributes
184
+ attrs = {}
185
+ attribute_names.each { |name| attrs[name] = read_attribute(name) }
186
+ attrs
187
+ end
188
+
189
+ # Returns an <tt>#inspect</tt>-like string for the value of the
190
+ # attribute +attr_name+. String attributes are truncated upto 50
191
+ # characters, and Date and Time attributes are returned in the
192
+ # <tt>:db</tt> format. Other attributes return the value of
193
+ # <tt>#inspect</tt> without modification.
194
+ #
195
+ # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
196
+ #
197
+ # person.attribute_for_inspect(:name)
198
+ # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
199
+ #
200
+ # person.attribute_for_inspect(:created_at)
201
+ # # => '"2009-01-12 04:48:57"'
202
+ def attribute_for_inspect(attr_name)
203
+ value = read_attribute(attr_name)
204
+
205
+ if value.is_a?(String) && value.length > 50
206
+ "#{value[0..50]}...".inspect
207
+ elsif value.is_a?(Date) || value.is_a?(Time)
208
+ %("#{value.to_s(:db)}")
209
+ else
210
+ value.inspect
211
+ end
212
+ end
213
+
214
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
215
+ # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
216
+ def attribute_present?(attribute)
217
+ value = read_attribute(attribute)
218
+ !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
219
+ end
220
+
221
+ # Returns the column object for the named attribute.
222
+ def column_for_attribute(name)
223
+ self.class.columns_hash[name.to_s]
224
+ end
225
+
69
226
  protected
70
- def attribute_method?(attr_name)
71
- attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
227
+
228
+ def clone_attributes(reader_method = :read_attribute, attributes = {})
229
+ attribute_names.each do |name|
230
+ attributes[name] = clone_attribute_value(reader_method, name)
72
231
  end
232
+ attributes
233
+ end
234
+
235
+ def clone_attribute_value(reader_method, attribute_name)
236
+ value = send(reader_method, attribute_name)
237
+ value.duplicable? ? value.clone : value
238
+ rescue TypeError, NoMethodError
239
+ value
240
+ end
241
+
242
+ # Returns a copy of the attributes hash where all the values have been safely quoted for use in
243
+ # an Arel insert/update method.
244
+ def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
245
+ attrs = {}
246
+ klass = self.class
247
+ arel_table = klass.arel_table
248
+
249
+ attribute_names.each do |name|
250
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
251
+
252
+ if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
253
+
254
+ value = if klass.serialized_attributes.include?(name)
255
+ @attributes[name].serialized_value
256
+ else
257
+ # FIXME: we need @attributes to be used consistently.
258
+ # If the values stored in @attributes were already type
259
+ # casted, this code could be simplified
260
+ read_attribute(name)
261
+ end
262
+
263
+ attrs[arel_table[name]] = value
264
+ end
265
+ end
266
+ end
267
+
268
+ attrs
269
+ end
270
+
271
+ def attribute_method?(attr_name)
272
+ attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
273
+ end
73
274
  end
74
275
  end
@@ -21,6 +21,21 @@ module ActiveRecord
21
21
  # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
22
22
  # When the <tt>:autosave</tt> option is not present new associations are saved.
23
23
  #
24
+ # == Validation
25
+ #
26
+ # Children records are validated unless <tt>:validate</tt> is +false+.
27
+ #
28
+ # == Callbacks
29
+ #
30
+ # Association with autosave option defines several callbacks on your
31
+ # model (before_save, after_create, after_update). Please note that
32
+ # callbacks are executed in the order they were defined in
33
+ # model. You should avoid modyfing the association content, before
34
+ # autosave callbacks are executed. Placing your callbacks after
35
+ # associations is usually a good practice.
36
+ #
37
+ # == Examples
38
+ #
24
39
  # === One-to-one Example
25
40
  #
26
41
  # class Post
@@ -109,10 +124,7 @@ module ActiveRecord
109
124
  # Now it _is_ removed from the database:
110
125
  #
111
126
  # Comment.find_by_id(id).nil? # => true
112
- #
113
- # === Validation
114
- #
115
- # Children records are validated unless <tt>:validate</tt> is +false+.
127
+
116
128
  module AutosaveAssociation
117
129
  extend ActiveSupport::Concern
118
130
 
@@ -161,7 +173,7 @@ module ActiveRecord
161
173
  #
162
174
  # For performance reasons, we don't check whether to validate at runtime.
163
175
  # However the validation and callback methods are lazy and those methods
164
- # get created when they are invoked for the very first time. However,
176
+ # get created when they are invoked for the very first time. However,
165
177
  # this can change, for instance, when using nested attributes, which is
166
178
  # called _after_ the association has been defined. Since we don't want
167
179
  # the callbacks to get defined multiple times, there are guards that
@@ -264,7 +276,7 @@ module ActiveRecord
264
276
  # turned on for the association.
265
277
  def validate_single_association(reflection)
266
278
  association = association_instance_get(reflection.name)
267
- record = association && association.target
279
+ record = association && association.reader
268
280
  association_valid?(reflection, record) if record
269
281
  end
270
282
 
@@ -320,25 +332,31 @@ module ActiveRecord
320
332
 
321
333
  if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
322
334
  begin
323
- records.each do |record|
324
- next if record.destroyed?
335
+ records_to_destroy = []
336
+
337
+ records.each do |record|
338
+ next if record.destroyed?
339
+
340
+ saved = true
341
+
342
+ if autosave && record.marked_for_destruction?
343
+ records_to_destroy << record
344
+ elsif autosave != false && (@new_record_before_save || record.new_record?)
345
+ if autosave
346
+ saved = association.insert_record(record, false)
347
+ else
348
+ association.insert_record(record) unless reflection.nested?
349
+ end
350
+ elsif autosave
351
+ saved = record.save(:validate => false)
352
+ end
325
353
 
326
- saved = true
354
+ raise ActiveRecord::Rollback unless saved
355
+ end
327
356
 
328
- if autosave && record.marked_for_destruction?
357
+ records_to_destroy.each do |record|
329
358
  association.proxy.destroy(record)
330
- elsif autosave != false && (@new_record_before_save || record.new_record?)
331
- if autosave
332
- saved = association.insert_record(record, false)
333
- else
334
- association.insert_record(record)
335
- end
336
- elsif autosave
337
- saved = record.save(:validate => false)
338
359
  end
339
-
340
- raise ActiveRecord::Rollback unless saved
341
- end
342
360
  rescue
343
361
  records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled?
344
362
  raise
@@ -370,7 +388,10 @@ module ActiveRecord
370
388
  else
371
389
  key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
372
390
  if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
373
- record[reflection.foreign_key] = key
391
+ unless reflection.through_reflection
392
+ record[reflection.foreign_key] = key
393
+ end
394
+
374
395
  saved = record.save(:validate => !autosave)
375
396
  raise ActiveRecord::Rollback if !saved && autosave
376
397
  saved