activerecord 3.0.0 → 4.0.0

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 (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +35 -44
  5. data/examples/performance.rb +110 -100
  6. data/lib/active_record/aggregations.rb +59 -75
  7. data/lib/active_record/associations/alias_tracker.rb +76 -0
  8. data/lib/active_record/associations/association.rb +248 -0
  9. data/lib/active_record/associations/association_scope.rb +135 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +60 -59
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +16 -59
  12. data/lib/active_record/associations/builder/association.rb +108 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  16. data/lib/active_record/associations/builder/has_many.rb +15 -0
  17. data/lib/active_record/associations/builder/has_one.rb +25 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +608 -0
  20. data/lib/active_record/associations/collection_proxy.rb +986 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +40 -112
  22. data/lib/active_record/associations/has_many_association.rb +83 -76
  23. data/lib/active_record/associations/has_many_through_association.rb +147 -66
  24. data/lib/active_record/associations/has_one_association.rb +67 -108
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  27. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  28. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  29. data/lib/active_record/associations/join_dependency.rb +235 -0
  30. data/lib/active_record/associations/join_helper.rb +45 -0
  31. data/lib/active_record/associations/preloader/association.rb +121 -0
  32. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  33. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  34. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  35. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  36. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  37. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  38. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  39. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  40. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  41. data/lib/active_record/associations/preloader.rb +178 -0
  42. data/lib/active_record/associations/singular_association.rb +64 -0
  43. data/lib/active_record/associations/through_association.rb +87 -0
  44. data/lib/active_record/associations.rb +512 -1224
  45. data/lib/active_record/attribute_assignment.rb +201 -0
  46. data/lib/active_record/attribute_methods/before_type_cast.rb +49 -12
  47. data/lib/active_record/attribute_methods/dirty.rb +51 -28
  48. data/lib/active_record/attribute_methods/primary_key.rb +94 -22
  49. data/lib/active_record/attribute_methods/query.rb +5 -4
  50. data/lib/active_record/attribute_methods/read.rb +63 -72
  51. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  52. data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -41
  53. data/lib/active_record/attribute_methods/write.rb +39 -13
  54. data/lib/active_record/attribute_methods.rb +362 -29
  55. data/lib/active_record/autosave_association.rb +132 -75
  56. data/lib/active_record/base.rb +83 -1627
  57. data/lib/active_record/callbacks.rb +69 -47
  58. data/lib/active_record/coders/yaml_column.rb +38 -0
  59. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +411 -138
  60. data/lib/active_record/connection_adapters/abstract/database_limits.rb +21 -11
  61. data/lib/active_record/connection_adapters/abstract/database_statements.rb +234 -173
  62. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -22
  63. data/lib/active_record/connection_adapters/abstract/quoting.rb +82 -25
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +176 -414
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +562 -232
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +281 -53
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  70. data/lib/active_record/connection_adapters/column.rb +318 -0
  71. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  72. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  73. data/lib/active_record/connection_adapters/mysql_adapter.rb +365 -450
  74. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  75. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  76. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  77. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  80. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  81. data/lib/active_record/connection_adapters/postgresql_adapter.rb +672 -752
  82. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  83. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +588 -17
  84. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  85. data/lib/active_record/connection_handling.rb +98 -0
  86. data/lib/active_record/core.rb +463 -0
  87. data/lib/active_record/counter_cache.rb +108 -101
  88. data/lib/active_record/dynamic_matchers.rb +131 -0
  89. data/lib/active_record/errors.rb +54 -13
  90. data/lib/active_record/explain.rb +38 -0
  91. data/lib/active_record/explain_registry.rb +30 -0
  92. data/lib/active_record/explain_subscriber.rb +29 -0
  93. data/lib/active_record/fixture_set/file.rb +55 -0
  94. data/lib/active_record/fixtures.rb +703 -785
  95. data/lib/active_record/inheritance.rb +200 -0
  96. data/lib/active_record/integration.rb +60 -0
  97. data/lib/active_record/locale/en.yml +8 -1
  98. data/lib/active_record/locking/optimistic.rb +69 -60
  99. data/lib/active_record/locking/pessimistic.rb +34 -12
  100. data/lib/active_record/log_subscriber.rb +40 -6
  101. data/lib/active_record/migration/command_recorder.rb +164 -0
  102. data/lib/active_record/migration/join_table.rb +15 -0
  103. data/lib/active_record/migration.rb +614 -216
  104. data/lib/active_record/model_schema.rb +345 -0
  105. data/lib/active_record/nested_attributes.rb +248 -119
  106. data/lib/active_record/null_relation.rb +65 -0
  107. data/lib/active_record/persistence.rb +275 -57
  108. data/lib/active_record/query_cache.rb +29 -9
  109. data/lib/active_record/querying.rb +62 -0
  110. data/lib/active_record/railtie.rb +135 -21
  111. data/lib/active_record/railties/console_sandbox.rb +5 -0
  112. data/lib/active_record/railties/controller_runtime.rb +17 -5
  113. data/lib/active_record/railties/databases.rake +249 -359
  114. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  115. data/lib/active_record/readonly_attributes.rb +30 -0
  116. data/lib/active_record/reflection.rb +283 -103
  117. data/lib/active_record/relation/batches.rb +38 -34
  118. data/lib/active_record/relation/calculations.rb +252 -139
  119. data/lib/active_record/relation/delegation.rb +125 -0
  120. data/lib/active_record/relation/finder_methods.rb +182 -188
  121. data/lib/active_record/relation/merger.rb +161 -0
  122. data/lib/active_record/relation/predicate_builder.rb +86 -21
  123. data/lib/active_record/relation/query_methods.rb +917 -134
  124. data/lib/active_record/relation/spawn_methods.rb +53 -92
  125. data/lib/active_record/relation.rb +405 -143
  126. data/lib/active_record/result.rb +67 -0
  127. data/lib/active_record/runtime_registry.rb +17 -0
  128. data/lib/active_record/sanitization.rb +168 -0
  129. data/lib/active_record/schema.rb +20 -14
  130. data/lib/active_record/schema_dumper.rb +55 -46
  131. data/lib/active_record/schema_migration.rb +39 -0
  132. data/lib/active_record/scoping/default.rb +146 -0
  133. data/lib/active_record/scoping/named.rb +175 -0
  134. data/lib/active_record/scoping.rb +82 -0
  135. data/lib/active_record/serialization.rb +8 -46
  136. data/lib/active_record/serializers/xml_serializer.rb +21 -68
  137. data/lib/active_record/statement_cache.rb +26 -0
  138. data/lib/active_record/store.rb +156 -0
  139. data/lib/active_record/tasks/database_tasks.rb +203 -0
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  141. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  142. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  143. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  144. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  145. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  146. data/lib/active_record/test_case.rb +57 -28
  147. data/lib/active_record/timestamp.rb +49 -18
  148. data/lib/active_record/transactions.rb +106 -63
  149. data/lib/active_record/translation.rb +22 -0
  150. data/lib/active_record/validations/associated.rb +25 -24
  151. data/lib/active_record/validations/presence.rb +65 -0
  152. data/lib/active_record/validations/uniqueness.rb +123 -83
  153. data/lib/active_record/validations.rb +29 -29
  154. data/lib/active_record/version.rb +7 -5
  155. data/lib/active_record.rb +83 -34
  156. data/lib/rails/generators/active_record/migration/migration_generator.rb +46 -9
  157. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  158. data/lib/rails/generators/active_record/migration/templates/migration.rb +30 -8
  159. data/lib/rails/generators/active_record/model/model_generator.rb +15 -5
  160. data/lib/rails/generators/active_record/model/templates/model.rb +7 -2
  161. data/lib/rails/generators/active_record/model/templates/module.rb +3 -1
  162. data/lib/rails/generators/active_record.rb +4 -8
  163. metadata +163 -121
  164. data/CHANGELOG +0 -6023
  165. data/examples/associations.png +0 -0
  166. data/lib/active_record/association_preload.rb +0 -403
  167. data/lib/active_record/associations/association_collection.rb +0 -562
  168. data/lib/active_record/associations/association_proxy.rb +0 -295
  169. data/lib/active_record/associations/through_association_scope.rb +0 -154
  170. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -113
  171. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -401
  172. data/lib/active_record/dynamic_finder_match.rb +0 -53
  173. data/lib/active_record/dynamic_scope_match.rb +0 -32
  174. data/lib/active_record/named_scope.rb +0 -138
  175. data/lib/active_record/observer.rb +0 -140
  176. data/lib/active_record/session_store.rb +0 -340
  177. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -16
  178. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  179. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -2
  180. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -24
  181. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -1,136 +1,64 @@
1
1
  module ActiveRecord
2
2
  # = Active Record Has And Belongs To Many Association
3
3
  module Associations
4
- class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
5
- def create(attributes = {})
6
- create_record(attributes) { |record| insert_record(record) }
7
- end
4
+ class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
5
+ attr_reader :join_table
8
6
 
9
- def create!(attributes = {})
10
- create_record(attributes) { |record| insert_record(record, true) }
7
+ def initialize(owner, reflection)
8
+ @join_table = Arel::Table.new(reflection.join_table)
9
+ super
11
10
  end
12
11
 
13
- def columns
14
- @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
15
- end
12
+ def insert_record(record, validate = true, raise = false)
13
+ if record.new_record?
14
+ if raise
15
+ record.save!(:validate => validate)
16
+ else
17
+ return unless record.save(:validate => validate)
18
+ end
19
+ end
16
20
 
17
- def reset_column_information
18
- @reflection.reset_column_information
19
- end
21
+ if options[:insert_sql]
22
+ owner.connection.insert(interpolate(options[:insert_sql], record))
23
+ else
24
+ stmt = join_table.compile_insert(
25
+ join_table[reflection.foreign_key] => owner.id,
26
+ join_table[reflection.association_foreign_key] => record.id
27
+ )
28
+
29
+ owner.class.connection.insert stmt
30
+ end
20
31
 
21
- def has_primary_key?
22
- @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
32
+ record
23
33
  end
24
34
 
25
- protected
26
- def construct_find_options!(options)
27
- options[:joins] = Arel::SqlLiteral.new @join_sql
28
- options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
29
- options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*'))
30
- end
35
+ private
31
36
 
32
37
  def count_records
33
38
  load_target.size
34
39
  end
35
40
 
36
- def insert_record(record, force = true, validate = true)
37
- if record.new_record?
38
- if force
39
- record.save!
40
- else
41
- return false unless record.save(:validate => validate)
42
- end
43
- end
44
-
45
- if @reflection.options[:insert_sql]
46
- @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
41
+ def delete_records(records, method)
42
+ if sql = options[:delete_sql]
43
+ records = load_target if records == :all
44
+ records.each { |record| owner.class.connection.delete(interpolate(sql, record)) }
47
45
  else
48
- relation = Arel::Table.new(@reflection.options[:join_table])
49
- timestamps = record_timestamp_columns(record)
50
- timezone = record.send(:current_time_from_proper_timezone) if timestamps.any?
51
-
52
- attributes = columns.inject({}) do |attrs, column|
53
- name = column.name
54
- case name.to_s
55
- when @reflection.primary_key_name.to_s
56
- attrs[relation[name]] = @owner.id
57
- when @reflection.association_foreign_key.to_s
58
- attrs[relation[name]] = record.id
59
- when *timestamps
60
- attrs[relation[name]] = timezone
61
- else
62
- if record.has_attribute?(name)
63
- value = @owner.send(:quote_value, record[name], column)
64
- attrs[relation[name]] = value unless value.nil?
65
- end
66
- end
67
- attrs
46
+ relation = join_table
47
+ condition = relation[reflection.foreign_key].eq(owner.id)
48
+
49
+ unless records == :all
50
+ condition = condition.and(
51
+ relation[reflection.association_foreign_key]
52
+ .in(records.map { |x| x.id }.compact)
53
+ )
68
54
  end
69
55
 
70
- relation.insert(attributes)
71
- end
72
-
73
- return true
74
- end
75
-
76
- def delete_records(records)
77
- if sql = @reflection.options[:delete_sql]
78
- records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
79
- else
80
- relation = Arel::Table.new(@reflection.options[:join_table])
81
- relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
82
- and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }))
83
- ).delete
84
- end
85
- end
86
-
87
- def construct_sql
88
- if @reflection.options[:finder_sql]
89
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
90
- else
91
- @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
92
- @finder_sql << " AND (#{conditions})" if conditions
93
- end
94
-
95
- @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
96
-
97
- construct_counter_sql
98
- end
99
-
100
- def construct_scope
101
- { :find => { :conditions => @finder_sql,
102
- :joins => @join_sql,
103
- :readonly => false,
104
- :order => @reflection.options[:order],
105
- :include => @reflection.options[:include],
106
- :limit => @reflection.options[:limit] } }
107
- end
108
-
109
- # Join tables with additional columns on top of the two foreign keys must be considered
110
- # ambiguous unless a select clause has been explicitly defined. Otherwise you can get
111
- # broken records back, if, for example, the join column also has an id column. This will
112
- # then overwrite the id column of the records coming back.
113
- def finding_with_ambiguous_select?(select_clause)
114
- !select_clause && columns.size != 2
115
- end
116
-
117
- private
118
- def create_record(attributes, &block)
119
- # Can't use Base.create because the foreign key may be a protected attribute.
120
- ensure_owner_is_not_new
121
- if attributes.is_a?(Array)
122
- attributes.collect { |attr| create(attr) }
123
- else
124
- build_record(attributes, &block)
56
+ owner.class.connection.delete(relation.where(condition).compile_delete)
125
57
  end
126
58
  end
127
59
 
128
- def record_timestamp_columns(record)
129
- if record.record_timestamps
130
- record.send(:all_timestamp_attributes).map { |x| x.to_s }
131
- else
132
- []
133
- end
60
+ def invertible_for?(record)
61
+ false
134
62
  end
135
63
  end
136
64
  end
@@ -5,25 +5,48 @@ module ActiveRecord
5
5
  #
6
6
  # If the association has a <tt>:through</tt> option further specialization
7
7
  # is provided by its child HasManyThroughAssociation.
8
- class HasManyAssociation < AssociationCollection #:nodoc:
9
- def initialize(owner, reflection)
10
- @finder_sql = nil
11
- super
12
- end
13
- protected
14
- def owner_quoted_id
15
- if @reflection.options[:primary_key]
16
- quote_value(@owner.send(@reflection.options[:primary_key]))
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+
10
+ def handle_dependency
11
+ case options[:dependent]
12
+ when :restrict, :restrict_with_exception
13
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
14
+
15
+ when :restrict_with_error
16
+ unless empty?
17
+ record = klass.human_attribute_name(reflection.name).downcase
18
+ owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
19
+ false
20
+ end
21
+
22
+ else
23
+ if options[:dependent] == :destroy
24
+ # No point in executing the counter update since we're going to destroy the parent anyway
25
+ load_target.each { |t| t.destroyed_by_association = reflection }
26
+ destroy_all
17
27
  else
18
- @owner.quoted_id
28
+ delete_all
19
29
  end
20
30
  end
31
+ end
32
+
33
+ def insert_record(record, validate = true, raise = false)
34
+ set_owner_attributes(record)
35
+
36
+ if raise
37
+ record.save!(:validate => validate)
38
+ else
39
+ record.save(:validate => validate)
40
+ end
41
+ end
42
+
43
+ private
21
44
 
22
45
  # Returns the number of records in this collection.
23
46
  #
24
47
  # If the association has a counter cache it gets that value. Otherwise
25
48
  # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
26
- # there's one. Some configuration options like :group make it impossible
49
+ # there's one. Some configuration options like :group make it impossible
27
50
  # to do an SQL count, in those cases the array count will be used.
28
51
  #
29
52
  # That does not depend on whether the collection has already been loaded
@@ -34,94 +57,78 @@ module ActiveRecord
34
57
  # the loaded flag is set to true as well.
35
58
  def count_records
36
59
  count = if has_cached_counter?
37
- @owner.send(:read_attribute, cached_counter_attribute_name)
38
- elsif @reflection.options[:counter_sql]
39
- @reflection.klass.count_by_sql(@counter_sql)
60
+ owner.send(:read_attribute, cached_counter_attribute_name)
61
+ elsif options[:counter_sql] || options[:finder_sql]
62
+ reflection.klass.count_by_sql(custom_counter_sql)
40
63
  else
41
- @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
64
+ scope.count
42
65
  end
43
66
 
44
67
  # If there's nothing in the database and @target has no new records
45
68
  # we are certain the current target is an empty array. This is a
46
69
  # documented side-effect of the method that may avoid an extra SELECT.
47
- @target ||= [] and loaded if count == 0
48
-
49
- if @reflection.options[:limit]
50
- count = [ @reflection.options[:limit], count ].min
51
- end
70
+ @target ||= [] and loaded! if count == 0
52
71
 
53
- return count
72
+ [association_scope.limit_value, count].compact.min
54
73
  end
55
74
 
56
- def has_cached_counter?
57
- @owner.attribute_present?(cached_counter_attribute_name)
75
+ def has_cached_counter?(reflection = reflection)
76
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
58
77
  end
59
78
 
60
- def cached_counter_attribute_name
61
- "#{@reflection.name}_count"
79
+ def cached_counter_attribute_name(reflection = reflection)
80
+ options[:counter_cache] || "#{reflection.name}_count"
62
81
  end
63
82
 
64
- def insert_record(record, force = false, validate = true)
65
- set_belongs_to_association_for(record)
66
- force ? record.save! : record.save(:validate => validate)
67
- end
68
-
69
- # Deletes the records according to the <tt>:dependent</tt> option.
70
- def delete_records(records)
71
- case @reflection.options[:dependent]
72
- when :destroy
73
- records.each { |r| r.destroy }
74
- when :delete_all
75
- @reflection.klass.delete(records.map { |record| record.id })
76
- else
77
- relation = Arel::Table.new(@reflection.table_name)
78
- relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
79
- and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id }))
80
- ).update(relation[@reflection.primary_key_name] => nil)
81
-
82
- @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
83
+ def update_counter(difference, reflection = reflection)
84
+ if has_cached_counter?(reflection)
85
+ counter = cached_counter_attribute_name(reflection)
86
+ owner.class.update_counters(owner.id, counter => difference)
87
+ owner[counter] += difference
88
+ owner.changed_attributes.delete(counter) # eww
83
89
  end
84
90
  end
85
91
 
86
- def target_obsolete?
87
- false
92
+ # This shit is nasty. We need to avoid the following situation:
93
+ #
94
+ # * An associated record is deleted via record.destroy
95
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
96
+ # :counter_cache options which points back at our owner. So they update the
97
+ # counter cache.
98
+ # * In which case, we must make sure to *not* update the counter cache, or else
99
+ # it will be decremented twice.
100
+ #
101
+ # Hence this method.
102
+ def inverse_updates_counter_cache?(reflection = reflection)
103
+ counter_name = cached_counter_attribute_name(reflection)
104
+ reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
105
+ inverse_reflection.counter_cache_column == counter_name
106
+ }
88
107
  end
89
108
 
90
- def construct_sql
91
- case
92
- when @reflection.options[:finder_sql]
93
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
94
-
95
- when @reflection.options[:as]
96
- @finder_sql =
97
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
98
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
99
- @finder_sql << " AND (#{conditions})" if conditions
109
+ # Deletes the records according to the <tt>:dependent</tt> option.
110
+ def delete_records(records, method)
111
+ if method == :destroy
112
+ records.each { |r| r.destroy }
113
+ update_counter(-records.length) unless inverse_updates_counter_cache?
114
+ else
115
+ if records == :all
116
+ scope = self.scope
117
+ else
118
+ keys = records.map { |r| r[reflection.association_primary_key] }
119
+ scope = self.scope.where(reflection.association_primary_key => keys)
120
+ end
100
121
 
122
+ if method == :delete_all
123
+ update_counter(-scope.delete_all)
101
124
  else
102
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
103
- @finder_sql << " AND (#{conditions})" if conditions
125
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
126
+ end
104
127
  end
105
-
106
- construct_counter_sql
107
- end
108
-
109
- def construct_scope
110
- create_scoping = {}
111
- set_belongs_to_association_for(create_scoping)
112
- {
113
- :find => { :conditions => @finder_sql,
114
- :readonly => false,
115
- :order => @reflection.options[:order],
116
- :limit => @reflection.options[:limit],
117
- :include => @reflection.options[:include]},
118
- :create => create_scoping
119
- }
120
128
  end
121
129
 
122
- def we_can_set_the_inverse_on_this?(record)
123
- inverse = @reflection.inverse_of
124
- return !inverse.nil?
130
+ def foreign_key_present?
131
+ owner.attribute_present?(reflection.association_primary_key)
125
132
  end
126
133
  end
127
134
  end
@@ -1,27 +1,15 @@
1
- require "active_record/associations/through_association_scope"
2
- require 'active_support/core_ext/object/blank'
3
1
 
4
2
  module ActiveRecord
5
3
  # = Active Record Has Many Through Association
6
4
  module Associations
7
5
  class HasManyThroughAssociation < HasManyAssociation #:nodoc:
8
- include ThroughAssociationScope
6
+ include ThroughAssociation
9
7
 
10
- alias_method :new, :build
8
+ def initialize(owner, reflection)
9
+ super
11
10
 
12
- def create!(attrs = nil)
13
- create_record(attrs, true)
14
- end
15
-
16
- def create(attrs = nil)
17
- create_record(attrs, false)
18
- end
19
-
20
- def destroy(*records)
21
- transaction do
22
- delete_records(flatten_deeper(records))
23
- super
24
- end
11
+ @through_records = {}
12
+ @through_association = nil
25
13
  end
26
14
 
27
15
  # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
@@ -29,86 +17,179 @@ module ActiveRecord
29
17
  # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
30
18
  # SELECT query if you use #length.
31
19
  def size
32
- return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
33
- return @target.size if loaded?
34
- return count
20
+ if has_cached_counter?
21
+ owner.send(:read_attribute, cached_counter_attribute_name)
22
+ elsif loaded?
23
+ target.size
24
+ else
25
+ count
26
+ end
27
+ end
28
+
29
+ def concat(*records)
30
+ unless owner.new_record?
31
+ records.flatten.each do |record|
32
+ raise_on_type_mismatch!(record)
33
+ record.save! if record.new_record?
34
+ end
35
+ end
36
+
37
+ super
35
38
  end
36
39
 
37
- protected
38
- def create_record(attrs, force = true)
39
- ensure_owner_is_not_new
40
+ def concat_records(records)
41
+ ensure_not_nested
40
42
 
41
- transaction do
42
- object = @reflection.klass.new(attrs)
43
- add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
44
- object
43
+ records = super
44
+
45
+ if owner.new_record? && records
46
+ records.flatten.each do |record|
47
+ build_through_record(record)
45
48
  end
46
49
  end
47
50
 
48
- def target_reflection_has_associated_record?
49
- if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
50
- false
51
+ records
52
+ end
53
+
54
+ def insert_record(record, validate = true, raise = false)
55
+ ensure_not_nested
56
+
57
+ if record.new_record?
58
+ if raise
59
+ record.save!(:validate => validate)
51
60
  else
52
- true
61
+ return unless record.save(:validate => validate)
53
62
  end
54
63
  end
55
64
 
56
- def construct_find_options!(options)
57
- options[:joins] = construct_joins(options[:joins])
58
- options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
65
+ save_through_record(record)
66
+ update_counter(1)
67
+ record
68
+ end
69
+
70
+ private
71
+
72
+ def through_association
73
+ @through_association ||= owner.association(through_reflection.name)
59
74
  end
60
75
 
61
- def insert_record(record, force = true, validate = true)
62
- if record.new_record?
63
- if force
64
- record.save!
65
- else
66
- return false unless record.save(:validate => validate)
67
- end
76
+ # We temporarily cache through record that has been build, because if we build a
77
+ # through record in build_record and then subsequently call insert_record, then we
78
+ # want to use the exact same object.
79
+ #
80
+ # However, after insert_record has been called, we clear the cache entry because
81
+ # we want it to be possible to have multiple instances of the same record in an
82
+ # association
83
+ def build_through_record(record)
84
+ @through_records[record.object_id] ||= begin
85
+ ensure_mutable
86
+
87
+ through_record = through_association.build
88
+ through_record.send("#{source_reflection.name}=", record)
89
+ through_record
68
90
  end
91
+ end
69
92
 
70
- through_association = @owner.send(@reflection.through_reflection.name)
71
- through_record = through_association.create!(construct_join_attributes(record))
72
- through_association.proxy_target << through_record
93
+ def save_through_record(record)
94
+ build_through_record(record).save!
95
+ ensure
96
+ @through_records.delete(record.object_id)
73
97
  end
74
98
 
75
- # TODO - add dependent option support
76
- def delete_records(records)
77
- klass = @reflection.through_reflection.klass
78
- records.each do |associate|
79
- klass.delete_all(construct_join_attributes(associate))
99
+ def build_record(attributes)
100
+ ensure_not_nested
101
+
102
+ record = super(attributes)
103
+
104
+ inverse = source_reflection.inverse_of
105
+ if inverse
106
+ if inverse.macro == :has_many
107
+ record.send(inverse.name) << build_through_record(record)
108
+ elsif inverse.macro == :has_one
109
+ record.send("#{inverse.name}=", build_through_record(record))
110
+ end
80
111
  end
112
+
113
+ record
81
114
  end
82
115
 
83
- def find_target
84
- return [] unless target_reflection_has_associated_record?
85
- with_scope(construct_scope) { @reflection.klass.find(:all) }
116
+ def target_reflection_has_associated_record?
117
+ !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
118
+ end
119
+
120
+ def update_through_counter?(method)
121
+ case method
122
+ when :destroy
123
+ !inverse_updates_counter_cache?(through_reflection)
124
+ when :nullify
125
+ false
126
+ else
127
+ true
128
+ end
86
129
  end
87
130
 
88
- def construct_sql
89
- case
90
- when @reflection.options[:finder_sql]
91
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
131
+ def delete_records(records, method)
132
+ ensure_not_nested
92
133
 
93
- @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
94
- @finder_sql << " AND (#{conditions})" if conditions
95
- else
96
- @finder_sql = construct_conditions
134
+ # This is unoptimised; it will load all the target records
135
+ # even when we just want to delete everything.
136
+ records = load_target if records == :all
137
+
138
+ scope = through_association.scope
139
+ scope.where! construct_join_attributes(*records)
140
+
141
+ case method
142
+ when :destroy
143
+ count = scope.destroy_all.length
144
+ when :nullify
145
+ count = scope.update_all(source_reflection.foreign_key => nil)
146
+ else
147
+ count = scope.delete_all
97
148
  end
98
149
 
99
- construct_counter_sql
150
+ delete_through_records(records)
151
+
152
+ if source_reflection.options[:counter_cache]
153
+ counter = source_reflection.counter_cache_column
154
+ klass.decrement_counter counter, records.map(&:id)
155
+ end
156
+
157
+ if through_reflection.macro == :has_many && update_through_counter?(method)
158
+ update_counter(-count, through_reflection)
159
+ end
160
+
161
+ update_counter(-count)
100
162
  end
101
163
 
102
- def has_cached_counter?
103
- @owner.attribute_present?(cached_counter_attribute_name)
164
+ def through_records_for(record)
165
+ attributes = construct_join_attributes(record)
166
+ candidates = Array.wrap(through_association.target)
167
+ candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
104
168
  end
105
169
 
106
- def cached_counter_attribute_name
107
- "#{@reflection.name}_count"
170
+ def delete_through_records(records)
171
+ records.each do |record|
172
+ through_records = through_records_for(record)
173
+
174
+ if through_reflection.macro == :has_many
175
+ through_records.each { |r| through_association.target.delete(r) }
176
+ else
177
+ if through_records.include?(through_association.target)
178
+ through_association.target = nil
179
+ end
180
+ end
181
+
182
+ @through_records.delete(record.object_id)
183
+ end
184
+ end
185
+
186
+ def find_target
187
+ return [] unless target_reflection_has_associated_record?
188
+ scope.to_a
108
189
  end
109
190
 
110
191
  # NOTE - not sure that we can actually cope with inverses here
111
- def we_can_set_the_inverse_on_this?(record)
192
+ def invertible_for?(record)
112
193
  false
113
194
  end
114
195
  end