activerecord 3.1.10 → 4.2.11

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.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (237) hide show
  1. checksums.yaml +6 -6
  2. data/CHANGELOG.md +1837 -338
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +39 -43
  5. data/examples/performance.rb +51 -20
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +57 -43
  8. data/lib/active_record/association_relation.rb +35 -0
  9. data/lib/active_record/associations/alias_tracker.rb +47 -39
  10. data/lib/active_record/associations/association.rb +71 -85
  11. data/lib/active_record/associations/association_scope.rb +138 -89
  12. data/lib/active_record/associations/belongs_to_association.rb +65 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +9 -3
  14. data/lib/active_record/associations/builder/association.rb +125 -29
  15. data/lib/active_record/associations/builder/belongs_to.rb +91 -60
  16. data/lib/active_record/associations/builder/collection_association.rb +69 -49
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +113 -42
  18. data/lib/active_record/associations/builder/has_many.rb +8 -64
  19. data/lib/active_record/associations/builder/has_one.rb +12 -52
  20. data/lib/active_record/associations/builder/singular_association.rb +22 -29
  21. data/lib/active_record/associations/collection_association.rb +294 -187
  22. data/lib/active_record/associations/collection_proxy.rb +961 -94
  23. data/lib/active_record/associations/foreign_association.rb +11 -0
  24. data/lib/active_record/associations/has_many_association.rb +118 -23
  25. data/lib/active_record/associations/has_many_through_association.rb +115 -45
  26. data/lib/active_record/associations/has_one_association.rb +57 -24
  27. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  28. data/lib/active_record/associations/join_dependency/join_association.rb +76 -102
  29. data/lib/active_record/associations/join_dependency/join_base.rb +7 -9
  30. data/lib/active_record/associations/join_dependency/join_part.rb +30 -37
  31. data/lib/active_record/associations/join_dependency.rb +230 -156
  32. data/lib/active_record/associations/preloader/association.rb +96 -55
  33. data/lib/active_record/associations/preloader/collection_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/has_many_through.rb +7 -3
  35. data/lib/active_record/associations/preloader/has_one.rb +1 -1
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +61 -32
  38. data/lib/active_record/associations/preloader.rb +113 -87
  39. data/lib/active_record/associations/singular_association.rb +29 -13
  40. data/lib/active_record/associations/through_association.rb +37 -19
  41. data/lib/active_record/associations.rb +505 -371
  42. data/lib/active_record/attribute.rb +163 -0
  43. data/lib/active_record/attribute_assignment.rb +212 -0
  44. data/lib/active_record/attribute_decorators.rb +66 -0
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +52 -7
  46. data/lib/active_record/attribute_methods/dirty.rb +141 -51
  47. data/lib/active_record/attribute_methods/primary_key.rb +87 -36
  48. data/lib/active_record/attribute_methods/query.rb +5 -4
  49. data/lib/active_record/attribute_methods/read.rb +74 -117
  50. data/lib/active_record/attribute_methods/serialization.rb +70 -0
  51. data/lib/active_record/attribute_methods/time_zone_conversion.rb +49 -47
  52. data/lib/active_record/attribute_methods/write.rb +60 -21
  53. data/lib/active_record/attribute_methods.rb +409 -48
  54. data/lib/active_record/attribute_set/builder.rb +106 -0
  55. data/lib/active_record/attribute_set.rb +81 -0
  56. data/lib/active_record/attributes.rb +147 -0
  57. data/lib/active_record/autosave_association.rb +279 -232
  58. data/lib/active_record/base.rb +84 -1969
  59. data/lib/active_record/callbacks.rb +66 -28
  60. data/lib/active_record/coders/json.rb +13 -0
  61. data/lib/active_record/coders/yaml_column.rb +18 -21
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +422 -243
  63. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  64. data/lib/active_record/connection_adapters/abstract/database_statements.rb +170 -194
  65. data/lib/active_record/connection_adapters/abstract/query_cache.rb +32 -19
  66. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -57
  67. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +125 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +273 -170
  70. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +50 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +731 -254
  72. data/lib/active_record/connection_adapters/abstract/transaction.rb +215 -0
  73. data/lib/active_record/connection_adapters/abstract_adapter.rb +339 -95
  74. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +946 -0
  75. data/lib/active_record/connection_adapters/column.rb +33 -221
  76. data/lib/active_record/connection_adapters/connection_specification.rb +275 -0
  77. data/lib/active_record/connection_adapters/mysql2_adapter.rb +140 -602
  78. data/lib/active_record/connection_adapters/mysql_adapter.rb +254 -756
  79. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +93 -0
  80. data/lib/active_record/connection_adapters/postgresql/column.rb +20 -0
  81. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +232 -0
  82. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +100 -0
  83. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  84. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +15 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +46 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +11 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +36 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +19 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/float.rb +21 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +13 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/integer.rb +11 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +35 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +43 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +79 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +19 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/time.rb +11 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +109 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid.rb +36 -0
  108. data/lib/active_record/connection_adapters/postgresql/quoting.rb +108 -0
  109. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  110. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +152 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +596 -0
  112. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  113. data/lib/active_record/connection_adapters/postgresql_adapter.rb +445 -902
  114. data/lib/active_record/connection_adapters/schema_cache.rb +94 -0
  115. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +578 -25
  116. data/lib/active_record/connection_handling.rb +132 -0
  117. data/lib/active_record/core.rb +579 -0
  118. data/lib/active_record/counter_cache.rb +159 -102
  119. data/lib/active_record/dynamic_matchers.rb +140 -0
  120. data/lib/active_record/enum.rb +197 -0
  121. data/lib/active_record/errors.rb +102 -34
  122. data/lib/active_record/explain.rb +38 -0
  123. data/lib/active_record/explain_registry.rb +30 -0
  124. data/lib/active_record/explain_subscriber.rb +29 -0
  125. data/lib/active_record/fixture_set/file.rb +56 -0
  126. data/lib/active_record/fixtures.rb +318 -260
  127. data/lib/active_record/gem_version.rb +15 -0
  128. data/lib/active_record/inheritance.rb +247 -0
  129. data/lib/active_record/integration.rb +113 -0
  130. data/lib/active_record/legacy_yaml_adapter.rb +30 -0
  131. data/lib/active_record/locale/en.yml +8 -1
  132. data/lib/active_record/locking/optimistic.rb +80 -52
  133. data/lib/active_record/locking/pessimistic.rb +27 -5
  134. data/lib/active_record/log_subscriber.rb +25 -18
  135. data/lib/active_record/migration/command_recorder.rb +130 -38
  136. data/lib/active_record/migration/join_table.rb +15 -0
  137. data/lib/active_record/migration.rb +532 -201
  138. data/lib/active_record/model_schema.rb +342 -0
  139. data/lib/active_record/nested_attributes.rb +229 -139
  140. data/lib/active_record/no_touching.rb +52 -0
  141. data/lib/active_record/null_relation.rb +81 -0
  142. data/lib/active_record/persistence.rb +304 -99
  143. data/lib/active_record/query_cache.rb +25 -43
  144. data/lib/active_record/querying.rb +68 -0
  145. data/lib/active_record/railtie.rb +86 -45
  146. data/lib/active_record/railties/console_sandbox.rb +3 -4
  147. data/lib/active_record/railties/controller_runtime.rb +7 -4
  148. data/lib/active_record/railties/databases.rake +198 -377
  149. data/lib/active_record/railties/jdbcmysql_error.rb +2 -2
  150. data/lib/active_record/readonly_attributes.rb +23 -0
  151. data/lib/active_record/reflection.rb +516 -165
  152. data/lib/active_record/relation/batches.rb +96 -45
  153. data/lib/active_record/relation/calculations.rb +221 -144
  154. data/lib/active_record/relation/delegation.rb +140 -0
  155. data/lib/active_record/relation/finder_methods.rb +362 -243
  156. data/lib/active_record/relation/merger.rb +193 -0
  157. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  158. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  159. data/lib/active_record/relation/predicate_builder.rb +135 -41
  160. data/lib/active_record/relation/query_methods.rb +982 -155
  161. data/lib/active_record/relation/spawn_methods.rb +50 -110
  162. data/lib/active_record/relation.rb +371 -180
  163. data/lib/active_record/result.rb +109 -12
  164. data/lib/active_record/runtime_registry.rb +22 -0
  165. data/lib/active_record/sanitization.rb +191 -0
  166. data/lib/active_record/schema.rb +19 -13
  167. data/lib/active_record/schema_dumper.rb +111 -61
  168. data/lib/active_record/schema_migration.rb +53 -0
  169. data/lib/active_record/scoping/default.rb +135 -0
  170. data/lib/active_record/scoping/named.rb +164 -0
  171. data/lib/active_record/scoping.rb +87 -0
  172. data/lib/active_record/serialization.rb +7 -45
  173. data/lib/active_record/serializers/xml_serializer.rb +14 -65
  174. data/lib/active_record/statement_cache.rb +111 -0
  175. data/lib/active_record/store.rb +205 -0
  176. data/lib/active_record/tasks/database_tasks.rb +299 -0
  177. data/lib/active_record/tasks/mysql_database_tasks.rb +159 -0
  178. data/lib/active_record/tasks/postgresql_database_tasks.rb +101 -0
  179. data/lib/active_record/tasks/sqlite_database_tasks.rb +55 -0
  180. data/lib/active_record/timestamp.rb +35 -14
  181. data/lib/active_record/transactions.rb +141 -74
  182. data/lib/active_record/translation.rb +22 -0
  183. data/lib/active_record/type/big_integer.rb +13 -0
  184. data/lib/active_record/type/binary.rb +50 -0
  185. data/lib/active_record/type/boolean.rb +31 -0
  186. data/lib/active_record/type/date.rb +50 -0
  187. data/lib/active_record/type/date_time.rb +54 -0
  188. data/lib/active_record/type/decimal.rb +64 -0
  189. data/lib/active_record/type/decimal_without_scale.rb +11 -0
  190. data/lib/active_record/type/decorator.rb +14 -0
  191. data/lib/active_record/type/float.rb +19 -0
  192. data/lib/active_record/type/hash_lookup_type_map.rb +23 -0
  193. data/lib/active_record/type/integer.rb +59 -0
  194. data/lib/active_record/type/mutable.rb +16 -0
  195. data/lib/active_record/type/numeric.rb +36 -0
  196. data/lib/active_record/type/serialized.rb +62 -0
  197. data/lib/active_record/type/string.rb +40 -0
  198. data/lib/active_record/type/text.rb +11 -0
  199. data/lib/active_record/type/time.rb +26 -0
  200. data/lib/active_record/type/time_value.rb +38 -0
  201. data/lib/active_record/type/type_map.rb +64 -0
  202. data/lib/active_record/type/unsigned_integer.rb +15 -0
  203. data/lib/active_record/type/value.rb +110 -0
  204. data/lib/active_record/type.rb +23 -0
  205. data/lib/active_record/validations/associated.rb +27 -18
  206. data/lib/active_record/validations/presence.rb +67 -0
  207. data/lib/active_record/validations/uniqueness.rb +125 -66
  208. data/lib/active_record/validations.rb +37 -30
  209. data/lib/active_record/version.rb +5 -7
  210. data/lib/active_record.rb +80 -25
  211. data/lib/rails/generators/active_record/migration/migration_generator.rb +54 -9
  212. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  213. data/lib/rails/generators/active_record/migration/templates/migration.rb +25 -11
  214. data/lib/rails/generators/active_record/migration.rb +11 -8
  215. data/lib/rails/generators/active_record/model/model_generator.rb +17 -4
  216. data/lib/rails/generators/active_record/model/templates/model.rb +5 -2
  217. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  218. data/lib/rails/generators/active_record.rb +3 -11
  219. metadata +132 -53
  220. data/examples/associations.png +0 -0
  221. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -62
  222. data/lib/active_record/associations/join_helper.rb +0 -55
  223. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  224. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -135
  225. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -556
  226. data/lib/active_record/dynamic_finder_match.rb +0 -56
  227. data/lib/active_record/dynamic_scope_match.rb +0 -23
  228. data/lib/active_record/identity_map.rb +0 -163
  229. data/lib/active_record/named_scope.rb +0 -200
  230. data/lib/active_record/observer.rb +0 -121
  231. data/lib/active_record/session_store.rb +0 -358
  232. data/lib/active_record/test_case.rb +0 -69
  233. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -17
  234. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  235. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  236. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  237. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -4,10 +4,14 @@ module ActiveRecord
4
4
  class HasManyThrough < CollectionAssociation #:nodoc:
5
5
  include ThroughAssociation
6
6
 
7
- def associated_records_by_owner
8
- super.each do |owner, records|
9
- records.uniq! if options[:uniq]
7
+ def associated_records_by_owner(preloader)
8
+ records_by_owner = super
9
+
10
+ if reflection_scope.distinct_value
11
+ records_by_owner.each_value { |records| records.uniq! }
10
12
  end
13
+
14
+ records_by_owner
11
15
  end
12
16
  end
13
17
  end
@@ -14,7 +14,7 @@ module ActiveRecord
14
14
  private
15
15
 
16
16
  def build_scope
17
- super.order(preload_options[:order] || options[:order])
17
+ super.order(preload_scope.values[:order] || reflection_scope.values[:order])
18
18
  end
19
19
 
20
20
  end
@@ -5,13 +5,13 @@ module ActiveRecord
5
5
 
6
6
  private
7
7
 
8
- def preload
9
- associated_records_by_owner.each do |owner, associated_records|
8
+ def preload(preloader)
9
+ associated_records_by_owner(preloader).each do |owner, associated_records|
10
10
  record = associated_records.first
11
11
 
12
12
  association = owner.association(reflection.name)
13
13
  association.target = record
14
- association.set_inverse_instance(record)
14
+ association.set_inverse_instance(record) if record
15
15
  end
16
16
  end
17
17
 
@@ -2,7 +2,6 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  module ThroughAssociation #:nodoc:
5
-
6
5
  def through_reflection
7
6
  reflection.through_reflection
8
7
  end
@@ -11,55 +10,85 @@ module ActiveRecord
11
10
  reflection.source_reflection
12
11
  end
13
12
 
14
- def associated_records_by_owner
15
- through_records = through_records_by_owner
13
+ def associated_records_by_owner(preloader)
14
+ preloader.preload(owners,
15
+ through_reflection.name,
16
+ through_scope)
16
17
 
17
- ActiveRecord::Associations::Preloader.new(
18
- through_records.values.flatten,
19
- source_reflection.name, options
20
- ).run
18
+ through_records = owners.map do |owner|
19
+ association = owner.association through_reflection.name
21
20
 
22
- through_records.each do |owner, records|
23
- records.map! { |r| r.send(source_reflection.name) }.flatten!
24
- records.compact!
21
+ [owner, Array(association.reader)]
25
22
  end
26
- end
27
23
 
28
- private
24
+ reset_association owners, through_reflection.name
25
+
26
+ middle_records = through_records.flat_map { |(_,rec)| rec }
27
+
28
+ preloaders = preloader.preload(middle_records,
29
+ source_reflection.name,
30
+ reflection_scope)
29
31
 
30
- def through_records_by_owner
31
- ActiveRecord::Associations::Preloader.new(
32
- owners, through_reflection.name,
33
- through_options
34
- ).run
32
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
33
+
34
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
35
+ pl.owners.each { |middle|
36
+ h[middle] = pl
37
+ }
38
+ end
39
+
40
+ record_offset = {}
41
+ @preloaded_records.each_with_index do |record,i|
42
+ record_offset[record] = i
43
+ end
35
44
 
36
- Hash[owners.map do |owner|
37
- through_records = Array.wrap(owner.send(through_reflection.name))
45
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
46
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
38
47
 
39
- # Dont cache the association - we would only be caching a subset
40
- if reflection.options[:source_type] && through_reflection.collection?
41
- owner.association(through_reflection.name).reset
48
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
49
+ rhs_records = middles.flat_map { |r|
50
+ association = r.association source_reflection.name
51
+
52
+ association.reader
53
+ }.compact
54
+
55
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
42
56
  end
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+ def reset_association(owners, association_name)
63
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
64
+ (reflection.options[:source_type] && through_reflection.collection?)
43
65
 
44
- [owner, through_records]
45
- end]
66
+ # Don't cache the association - we would only be caching a subset
67
+ if should_reset
68
+ owners.each { |owner|
69
+ owner.association(association_name).reset
70
+ }
71
+ end
46
72
  end
47
73
 
48
- def through_options
49
- through_options = {}
74
+
75
+ def through_scope
76
+ scope = through_reflection.klass.unscoped
50
77
 
51
78
  if options[:source_type]
52
- through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
79
+ scope.where! reflection.foreign_type => options[:source_type]
53
80
  else
54
- if options[:conditions]
55
- through_options[:include] = options[:include] || options[:source]
56
- through_options[:conditions] = options[:conditions]
81
+ unless reflection_scope.where_values.empty?
82
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
83
+ scope.where_values = reflection_scope.values[:where]
84
+ scope.bind_values = reflection_scope.bind_values
57
85
  end
58
86
 
59
- through_options[:order] = options[:order]
87
+ scope.references! reflection_scope.values[:references]
88
+ scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
60
89
  end
61
90
 
62
- through_options
91
+ scope
63
92
  end
64
93
  end
65
94
  end
@@ -2,47 +2,57 @@ module ActiveRecord
2
2
  module Associations
3
3
  # Implements the details of eager loading of Active Record associations.
4
4
  #
5
- # Note that 'eager loading' and 'preloading' are actually the same thing.
6
- # However, there are two different eager loading strategies.
5
+ # Suppose that you have the following two Active Record models:
7
6
  #
8
- # The first one is by using table joins. This was only strategy available
9
- # prior to Rails 2.1. Suppose that you have an Author model with columns
10
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
11
- # this strategy, Active Record would try to retrieve all data for an author
12
- # and all of its books via a single query:
7
+ # class Author < ActiveRecord::Base
8
+ # # columns: name, age
9
+ # has_many :books
10
+ # end
13
11
  #
14
- # SELECT * FROM authors
15
- # LEFT OUTER JOIN books ON authors.id = books.id
16
- # WHERE authors.name = 'Ken Akamatsu'
12
+ # class Book < ActiveRecord::Base
13
+ # # columns: title, sales, author_id
14
+ # end
17
15
  #
18
- # However, this could result in many rows that contain redundant data. After
19
- # having received the first row, we already have enough data to instantiate
20
- # the Author object. In all subsequent rows, only the data for the joined
21
- # 'books' table is useful; the joined 'authors' data is just redundant, and
22
- # processing this redundant data takes memory and CPU time. The problem
23
- # quickly becomes worse and worse as the level of eager loading increases
24
- # (i.e. if Active Record is to eager load the associations' associations as
25
- # well).
16
+ # When you load an author with all associated books Active Record will make
17
+ # multiple queries like this:
18
+ #
19
+ # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a
20
+ #
21
+ # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
22
+ # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
23
+ #
24
+ # Active Record saves the ids of the records from the first query to use in
25
+ # the second. Depending on the number of associations involved there can be
26
+ # arbitrarily many SQL queries made.
27
+ #
28
+ # However, if there is a WHERE clause that spans across tables Active
29
+ # Record will fall back to a slightly more resource-intensive single query:
30
+ #
31
+ # Author.includes(:books).where(books: {title: 'Illiad'}).to_a
32
+ # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
33
+ # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
34
+ # FROM `authors`
35
+ # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
36
+ # WHERE `books`.`title` = 'Illiad'
37
+ #
38
+ # This could result in many rows that contain redundant data and it performs poorly at scale
39
+ # and is therefore only used when necessary.
26
40
  #
27
- # The second strategy is to use multiple database queries, one for each
28
- # level of association. Since Rails 2.1, this is the default strategy. In
29
- # situations where a table join is necessary (e.g. when the +:conditions+
30
- # option references an association's column), it will fallback to the table
31
- # join strategy.
32
41
  class Preloader #:nodoc:
33
- autoload :Association, 'active_record/associations/preloader/association'
34
- autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
35
- autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
36
- autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
42
+ extend ActiveSupport::Autoload
37
43
 
38
- autoload :HasMany, 'active_record/associations/preloader/has_many'
39
- autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
40
- autoload :HasOne, 'active_record/associations/preloader/has_one'
41
- autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
42
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
43
- autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
44
+ eager_autoload do
45
+ autoload :Association, 'active_record/associations/preloader/association'
46
+ autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
47
+ autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
48
+ autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
44
49
 
45
- attr_reader :records, :associations, :options, :model
50
+ autoload :HasMany, 'active_record/associations/preloader/has_many'
51
+ autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
52
+ autoload :HasOne, 'active_record/associations/preloader/has_one'
53
+ autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
54
+ autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
55
+ end
46
56
 
47
57
  # Eager loads the named associations for the given Active Record record(s).
48
58
  #
@@ -68,7 +78,7 @@ module ActiveRecord
68
78
  # books.
69
79
  # - a Hash which specifies multiple association names, as well as
70
80
  # association names for the to-be-preloaded association objects. For
71
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
81
+ # example, specifying <tt>{ author: :avatar }</tt> will preload a
72
82
  # book's author, as well as that author's avatar.
73
83
  #
74
84
  # +:associations+ has the same format as the +:include+ option for
@@ -76,43 +86,50 @@ module ActiveRecord
76
86
  #
77
87
  # :books
78
88
  # [ :books, :author ]
79
- # { :author => :avatar }
80
- # [ :books, { :author => :avatar } ]
81
- #
82
- # +options+ contains options that will be passed to ActiveRecord::Base#find
83
- # (which is called under the hood for preloading records). But it is passed
84
- # only one level deep in the +associations+ argument, i.e. it's not passed
85
- # to the child associations when +associations+ is a Hash.
86
- def initialize(records, associations, options = {})
87
- @records = Array.wrap(records).compact.uniq
88
- @associations = Array.wrap(associations)
89
- @options = options
90
- end
89
+ # { author: :avatar }
90
+ # [ :books, { author: :avatar } ]
91
91
 
92
- def run
93
- unless records.empty?
94
- associations.each { |association| preload(association) }
92
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
93
+
94
+ def preload(records, associations, preload_scope = nil)
95
+ records = Array.wrap(records).compact.uniq
96
+ associations = Array.wrap(associations)
97
+ preload_scope = preload_scope || NULL_RELATION
98
+
99
+ if records.empty?
100
+ []
101
+ else
102
+ associations.flat_map { |association|
103
+ preloaders_on association, records, preload_scope
104
+ }
95
105
  end
96
106
  end
97
107
 
98
108
  private
99
109
 
100
- def preload(association)
110
+ def preloaders_on(association, records, scope)
101
111
  case association
102
112
  when Hash
103
- preload_hash(association)
104
- when String, Symbol
105
- preload_one(association.to_sym)
113
+ preloaders_for_hash(association, records, scope)
114
+ when Symbol
115
+ preloaders_for_one(association, records, scope)
116
+ when String
117
+ preloaders_for_one(association.to_sym, records, scope)
106
118
  else
107
119
  raise ArgumentError, "#{association.inspect} was not recognised for preload"
108
120
  end
109
121
  end
110
122
 
111
- def preload_hash(association)
112
- association.each do |parent, child|
113
- Preloader.new(records, parent, options).run
114
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
115
- end
123
+ def preloaders_for_hash(association, records, scope)
124
+ association.flat_map { |parent, child|
125
+ loaders = preloaders_for_one parent, records, scope
126
+
127
+ recs = loaders.flat_map(&:preloaded_records).uniq
128
+ loaders.concat Array.wrap(child).flat_map { |assoc|
129
+ preloaders_on assoc, recs, scope
130
+ }
131
+ loaders
132
+ }
116
133
  end
117
134
 
118
135
  # Not all records have the same class, so group then preload group on the reflection
@@ -122,52 +139,61 @@ module ActiveRecord
122
139
  # Additionally, polymorphic belongs_to associations can have multiple associated
123
140
  # classes, depending on the polymorphic_type field. So we group by the classes as
124
141
  # well.
125
- def preload_one(association)
126
- grouped_records(association).each do |reflection, klasses|
127
- klasses.each do |klass, records|
128
- preloader_for(reflection).new(klass, records, reflection, options).run
142
+ def preloaders_for_one(association, records, scope)
143
+ grouped_records(association, records).flat_map do |reflection, klasses|
144
+ klasses.map do |rhs_klass, rs|
145
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
146
+ loader.run self
147
+ loader
129
148
  end
130
149
  end
131
150
  end
132
151
 
133
- def grouped_records(association)
134
- Hash[
135
- records_by_reflection(association).map do |reflection, records|
136
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
137
- end
138
- ]
152
+ def grouped_records(association, records)
153
+ h = {}
154
+ records.each do |record|
155
+ next unless record
156
+ assoc = record.association(association)
157
+ klasses = h[assoc.reflection] ||= {}
158
+ (klasses[assoc.klass] ||= []) << record
159
+ end
160
+ h
139
161
  end
140
162
 
141
- def records_by_reflection(association)
142
- records.group_by do |record|
143
- reflection = record.class.reflections[association]
163
+ class AlreadyLoaded # :nodoc:
164
+ attr_reader :owners, :reflection
144
165
 
145
- unless reflection
146
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
147
- "perhaps you misspelled it?"
148
- end
166
+ def initialize(klass, owners, reflection, preload_scope)
167
+ @owners = owners
168
+ @reflection = reflection
169
+ end
149
170
 
150
- reflection
171
+ def run(preloader); end
172
+
173
+ def preloaded_records
174
+ owners.flat_map { |owner| owner.association(reflection.name).target }
151
175
  end
152
176
  end
153
177
 
154
- def association_klass(reflection, record)
155
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
156
- klass = record.send(reflection.foreign_type)
157
- klass && klass.constantize
158
- else
159
- reflection.klass
160
- end
178
+ class NullPreloader # :nodoc:
179
+ def self.new(klass, owners, reflection, preload_scope); self; end
180
+ def self.run(preloader); end
181
+ def self.preloaded_records; []; end
161
182
  end
162
183
 
163
- def preloader_for(reflection)
184
+ def preloader_for(reflection, owners, rhs_klass)
185
+ return NullPreloader unless rhs_klass
186
+
187
+ if owners.first.association(reflection.name).loaded?
188
+ return AlreadyLoaded
189
+ end
190
+ reflection.check_preloadable!
191
+
164
192
  case reflection.macro
165
193
  when :has_many
166
194
  reflection.options[:through] ? HasManyThrough : HasMany
167
195
  when :has_one
168
196
  reflection.options[:through] ? HasOneThrough : HasOne
169
- when :has_and_belongs_to_many
170
- HasAndBelongsToMany
171
197
  when :belongs_to
172
198
  BelongsTo
173
199
  end
@@ -3,7 +3,7 @@ module ActiveRecord
3
3
  class SingularAssociation < Association #:nodoc:
4
4
  # Implements the reader method, e.g. foo.bar for Foo.has_one :bar
5
5
  def reader(force_reload = false)
6
- if force_reload
6
+ if force_reload && klass
7
7
  klass.uncached { reload }
8
8
  elsif !loaded? || stale_target?
9
9
  reload
@@ -12,21 +12,21 @@ module ActiveRecord
12
12
  target
13
13
  end
14
14
 
15
- # Implements the writer method, e.g. foo.items= for Foo.has_many :items
15
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
16
16
  def writer(record)
17
17
  replace(record)
18
18
  end
19
19
 
20
- def create(attributes = {}, options = {}, &block)
21
- create_record(attributes, options, &block)
20
+ def create(attributes = {}, &block)
21
+ _create_record(attributes, &block)
22
22
  end
23
23
 
24
- def create!(attributes = {}, options = {}, &block)
25
- create_record(attributes, options, true, &block)
24
+ def create!(attributes = {}, &block)
25
+ _create_record(attributes, true, &block)
26
26
  end
27
27
 
28
- def build(attributes = {}, options = {})
29
- record = build_record(attributes, options)
28
+ def build(attributes = {})
29
+ record = build_record(attributes)
30
30
  yield(record) if block_given?
31
31
  set_new_record(record)
32
32
  record
@@ -35,14 +35,30 @@ module ActiveRecord
35
35
  private
36
36
 
37
37
  def create_scope
38
- scoped.scope_for_create.stringify_keys.except(klass.primary_key)
38
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
39
+ end
40
+
41
+ def get_records
42
+ return scope.limit(1).to_a if skip_statement_cache?
43
+
44
+ conn = klass.connection
45
+ sc = reflection.association_scope_cache(conn, owner) do
46
+ StatementCache.create(conn) { |params|
47
+ as = AssociationScope.create { params.bind }
48
+ target_scope.merge(as.scope(self, conn)).limit(1)
49
+ }
50
+ end
51
+
52
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
53
+ sc.execute binds, klass, klass.connection
39
54
  end
40
55
 
41
56
  def find_target
42
- scoped.first.tap { |record| set_inverse_instance(record) }
57
+ if record = get_records.first
58
+ set_inverse_instance record
59
+ end
43
60
  end
44
61
 
45
- # Implemented by subclasses
46
62
  def replace(record)
47
63
  raise NotImplementedError, "Subclasses must implement a replace(record) method"
48
64
  end
@@ -51,8 +67,8 @@ module ActiveRecord
51
67
  replace(record)
52
68
  end
53
69
 
54
- def create_record(attributes, options, raise_error = false)
55
- record = build_record(attributes, options)
70
+ def _create_record(attributes, raise_error = false)
71
+ record = build_record(attributes)
56
72
  yield(record) if block_given?
57
73
  saved = record.save
58
74
  set_new_record(record)
@@ -3,7 +3,7 @@ module ActiveRecord
3
3
  module Associations
4
4
  module ThroughAssociation #:nodoc:
5
5
 
6
- delegate :source_reflection, :through_reflection, :chain, :to => :reflection
6
+ delegate :source_reflection, :through_reflection, :to => :reflection
7
7
 
8
8
  protected
9
9
 
@@ -13,10 +13,10 @@ module ActiveRecord
13
13
  # 2. To get the type conditions for any STI models in the chain
14
14
  def target_scope
15
15
  scope = super
16
- chain[1..-1].each do |reflection|
17
- scope = scope.merge(
18
- reflection.klass.scoped.with_default_scope.
19
- except(:select, :create_with, :includes, :preload, :joins, :eager_load)
16
+ reflection.chain.drop(1).each do |reflection|
17
+ relation = reflection.klass.all
18
+ scope.merge!(
19
+ relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
20
20
  )
21
21
  end
22
22
  scope
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  # methods which create and delete records on the association.
29
29
  #
30
30
  # We only support indirectly modifying through associations which has a belongs_to source.
31
- # This is the "has_many :tags, :through => :taggings" situation, where the join model
31
+ # This is the "has_many :tags, through: :taggings" situation, where the join model
32
32
  # typically has a belongs_to on both side. In other words, associations which could also
33
33
  # be represented as has_and_belongs_to_many associations.
34
34
  #
@@ -37,16 +37,18 @@ module ActiveRecord
37
37
  # situation it is more natural for the user to just create or modify their join records
38
38
  # directly as required.
39
39
  def construct_join_attributes(*records)
40
- if source_reflection.macro != :belongs_to
41
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
42
- end
40
+ ensure_mutable
43
41
 
44
- join_attributes = {
45
- source_reflection.foreign_key =>
46
- records.map { |record|
47
- record.send(source_reflection.association_primary_key(reflection.klass))
48
- }
49
- }
42
+ if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
43
+ join_attributes = { source_reflection.name => records }
44
+ else
45
+ join_attributes = {
46
+ source_reflection.foreign_key =>
47
+ records.map { |record|
48
+ record.send(source_reflection.association_primary_key(reflection.klass))
49
+ }
50
+ }
51
+ end
50
52
 
51
53
  if options[:source_type]
52
54
  join_attributes[source_reflection.foreign_type] =
@@ -63,14 +65,19 @@ module ActiveRecord
63
65
  # Note: this does not capture all cases, for example it would be crazy to try to
64
66
  # properly support stale-checking for nested associations.
65
67
  def stale_state
66
- if through_reflection.macro == :belongs_to
67
- owner[through_reflection.foreign_key].to_s
68
+ if through_reflection.belongs_to?
69
+ owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
68
70
  end
69
71
  end
70
72
 
71
73
  def foreign_key_present?
72
- through_reflection.macro == :belongs_to &&
73
- !owner[through_reflection.foreign_key].nil?
74
+ through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil?
75
+ end
76
+
77
+ def ensure_mutable
78
+ unless source_reflection.belongs_to?
79
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
80
+ end
74
81
  end
75
82
 
76
83
  def ensure_not_nested
@@ -78,6 +85,17 @@ module ActiveRecord
78
85
  raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
79
86
  end
80
87
  end
88
+
89
+ def build_record(attributes)
90
+ inverse = source_reflection.inverse_of
91
+ target = through_association.target
92
+
93
+ if inverse && target && !target.is_a?(Array)
94
+ attributes[inverse.foreign_key] = target.id
95
+ end
96
+
97
+ super(attributes)
98
+ end
81
99
  end
82
100
  end
83
101
  end