activerecord 3.1.12 → 3.2.22.1

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.

Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +804 -338
  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 +13 -45
  8. data/lib/active_record/associations/association_scope.rb +3 -15
  9. data/lib/active_record/associations/belongs_to_association.rb +1 -1
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
  11. data/lib/active_record/associations/builder/association.rb +6 -4
  12. data/lib/active_record/associations/builder/belongs_to.rb +7 -4
  13. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +4 -4
  15. data/lib/active_record/associations/builder/has_one.rb +5 -6
  16. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  17. data/lib/active_record/associations/collection_association.rb +65 -32
  18. data/lib/active_record/associations/collection_proxy.rb +8 -41
  19. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
  20. data/lib/active_record/associations/has_many_association.rb +11 -7
  21. data/lib/active_record/associations/has_many_through_association.rb +19 -9
  22. data/lib/active_record/associations/has_one_association.rb +23 -13
  23. data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
  24. data/lib/active_record/associations/join_dependency.rb +3 -3
  25. data/lib/active_record/associations/preloader/through_association.rb +3 -3
  26. data/lib/active_record/associations/preloader.rb +14 -10
  27. data/lib/active_record/associations/through_association.rb +8 -4
  28. data/lib/active_record/associations.rb +92 -76
  29. data/lib/active_record/attribute_assignment.rb +221 -0
  30. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  31. data/lib/active_record/attribute_methods/dirty.rb +21 -11
  32. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  33. data/lib/active_record/attribute_methods/read.rb +73 -83
  34. data/lib/active_record/attribute_methods/serialization.rb +120 -0
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  36. data/lib/active_record/attribute_methods/write.rb +32 -6
  37. data/lib/active_record/attribute_methods.rb +231 -30
  38. data/lib/active_record/autosave_association.rb +44 -26
  39. data/lib/active_record/base.rb +227 -1708
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
  41. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +7 -34
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  45. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +39 -28
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
  49. data/lib/active_record/connection_adapters/column.rb +37 -11
  50. data/lib/active_record/connection_adapters/mysql2_adapter.rb +133 -581
  51. data/lib/active_record/connection_adapters/mysql_adapter.rb +136 -693
  52. data/lib/active_record/connection_adapters/postgresql_adapter.rb +209 -97
  53. data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
  54. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  55. data/lib/active_record/connection_adapters/sqlite_adapter.rb +62 -35
  56. data/lib/active_record/counter_cache.rb +9 -4
  57. data/lib/active_record/dynamic_finder_match.rb +12 -0
  58. data/lib/active_record/dynamic_matchers.rb +84 -0
  59. data/lib/active_record/errors.rb +11 -1
  60. data/lib/active_record/explain.rb +86 -0
  61. data/lib/active_record/explain_subscriber.rb +25 -0
  62. data/lib/active_record/fixtures/file.rb +65 -0
  63. data/lib/active_record/fixtures.rb +57 -86
  64. data/lib/active_record/identity_map.rb +3 -4
  65. data/lib/active_record/inheritance.rb +174 -0
  66. data/lib/active_record/integration.rb +60 -0
  67. data/lib/active_record/locking/optimistic.rb +33 -26
  68. data/lib/active_record/locking/pessimistic.rb +23 -1
  69. data/lib/active_record/log_subscriber.rb +8 -4
  70. data/lib/active_record/migration/command_recorder.rb +8 -8
  71. data/lib/active_record/migration.rb +68 -35
  72. data/lib/active_record/model_schema.rb +368 -0
  73. data/lib/active_record/nested_attributes.rb +60 -24
  74. data/lib/active_record/persistence.rb +57 -11
  75. data/lib/active_record/query_cache.rb +6 -6
  76. data/lib/active_record/querying.rb +58 -0
  77. data/lib/active_record/railtie.rb +37 -29
  78. data/lib/active_record/railties/controller_runtime.rb +3 -1
  79. data/lib/active_record/railties/databases.rake +213 -117
  80. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  81. data/lib/active_record/readonly_attributes.rb +26 -0
  82. data/lib/active_record/reflection.rb +7 -15
  83. data/lib/active_record/relation/batches.rb +7 -4
  84. data/lib/active_record/relation/calculations.rb +55 -16
  85. data/lib/active_record/relation/delegation.rb +49 -0
  86. data/lib/active_record/relation/finder_methods.rb +16 -11
  87. data/lib/active_record/relation/predicate_builder.rb +8 -6
  88. data/lib/active_record/relation/query_methods.rb +75 -9
  89. data/lib/active_record/relation/spawn_methods.rb +48 -7
  90. data/lib/active_record/relation.rb +78 -32
  91. data/lib/active_record/result.rb +10 -4
  92. data/lib/active_record/sanitization.rb +194 -0
  93. data/lib/active_record/schema_dumper.rb +12 -5
  94. data/lib/active_record/scoping/default.rb +142 -0
  95. data/lib/active_record/scoping/named.rb +200 -0
  96. data/lib/active_record/scoping.rb +152 -0
  97. data/lib/active_record/serialization.rb +1 -43
  98. data/lib/active_record/serializers/xml_serializer.rb +4 -45
  99. data/lib/active_record/session_store.rb +18 -16
  100. data/lib/active_record/store.rb +52 -0
  101. data/lib/active_record/test_case.rb +11 -7
  102. data/lib/active_record/timestamp.rb +17 -3
  103. data/lib/active_record/transactions.rb +27 -6
  104. data/lib/active_record/translation.rb +22 -0
  105. data/lib/active_record/validations/associated.rb +5 -4
  106. data/lib/active_record/validations/uniqueness.rb +8 -8
  107. data/lib/active_record/validations.rb +1 -1
  108. data/lib/active_record/version.rb +3 -3
  109. data/lib/active_record.rb +38 -3
  110. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  111. data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
  112. data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
  113. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  114. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  115. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  116. metadata +49 -28
  117. data/lib/active_record/named_scope.rb +0 -200
@@ -0,0 +1,120 @@
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 _field_changed?(attr, old, value)
94
+ if self.class.serialized_attributes.include?(attr)
95
+ old != value
96
+ else
97
+ super
98
+ end
99
+ end
100
+
101
+ def read_attribute_before_type_cast(attr_name)
102
+ if serialized_attributes.include?(attr_name)
103
+ super.unserialized_value
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ def attributes_before_type_cast
110
+ super.dup.tap do |attributes|
111
+ self.class.serialized_attributes.each_key do |key|
112
+ if attributes.key?(key)
113
+ attributes[key] = attributes[key].unserialized_value
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ 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
@@ -42,12 +37,15 @@ module ActiveRecord
42
37
  if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
43
38
  method_body, line = <<-EOV, __LINE__ + 1
44
39
  def #{attr_name}=(original_time)
40
+ original_time = nil if original_time.blank?
45
41
  time = original_time
46
42
  unless time.acts_like?(:time)
47
43
  time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
48
44
  end
49
45
  time = time.in_time_zone rescue nil if time
46
+ previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name})
50
47
  write_attribute(:#{attr_name}, original_time)
48
+ #{attr_name}_will_change! if previous_time != time
51
49
  @attributes_cache["#{attr_name}"] = time
52
50
  end
53
51
  EOV
@@ -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,27 @@ 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
+ case value
58
+ when FalseClass
59
+ 0
60
+ when TrueClass
61
+ 1
62
+ when String
63
+ value.presence
64
+ else
65
+ value
66
+ end
67
+ end
42
68
  end
43
69
  end
44
70
  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,34 +332,37 @@ 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?
325
-
326
- saved = true
327
-
328
- if autosave && record.marked_for_destruction?
329
- 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)
335
+ if autosave
336
+ records_to_destroy = records.select(&:marked_for_destruction?)
337
+ records_to_destroy.each { |record| association.proxy.destroy(record) }
338
+ records -= records_to_destroy
338
339
  end
339
340
 
340
- raise ActiveRecord::Rollback unless saved
341
- end
341
+ records.each do |record|
342
+ next if record.destroyed?
343
+
344
+ saved = true
345
+
346
+ if autosave != false && (@new_record_before_save || record.new_record?)
347
+ if autosave
348
+ saved = association.insert_record(record, false)
349
+ else
350
+ association.insert_record(record) unless reflection.nested?
351
+ end
352
+ elsif autosave
353
+ saved = record.save(:validate => false)
354
+ end
355
+
356
+ raise ActiveRecord::Rollback unless saved
357
+ end
342
358
  rescue
343
359
  records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled?
344
360
  raise
345
361
  end
346
-
347
362
  end
348
363
 
349
364
  # reconstruct the scope now that we know the owner's id
350
- association.send(:reset_scope) if association.respond_to?(:reset_scope)
365
+ association.reset_scope if association.respond_to?(:reset_scope)
351
366
  end
352
367
  end
353
368
 
@@ -370,7 +385,10 @@ module ActiveRecord
370
385
  else
371
386
  key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
372
387
  if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
373
- record[reflection.foreign_key] = key
388
+ unless reflection.through_reflection
389
+ record[reflection.foreign_key] = key
390
+ end
391
+
374
392
  saved = record.save(:validate => !autosave)
375
393
  raise ActiveRecord::Rollback if !saved && autosave
376
394
  saved