activerecord 3.2.19 → 5.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 (264) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1715 -604
  3. data/MIT-LICENSE +2 -2
  4. data/README.rdoc +40 -45
  5. data/examples/performance.rb +33 -22
  6. data/examples/simple.rb +3 -4
  7. data/lib/active_record/aggregations.rb +76 -51
  8. data/lib/active_record/association_relation.rb +35 -0
  9. data/lib/active_record/associations/alias_tracker.rb +54 -40
  10. data/lib/active_record/associations/association.rb +76 -56
  11. data/lib/active_record/associations/association_scope.rb +125 -93
  12. data/lib/active_record/associations/belongs_to_association.rb +57 -28
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +7 -2
  14. data/lib/active_record/associations/builder/association.rb +120 -32
  15. data/lib/active_record/associations/builder/belongs_to.rb +115 -62
  16. data/lib/active_record/associations/builder/collection_association.rb +61 -53
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +117 -43
  18. data/lib/active_record/associations/builder/has_many.rb +9 -65
  19. data/lib/active_record/associations/builder/has_one.rb +18 -52
  20. data/lib/active_record/associations/builder/singular_association.rb +18 -19
  21. data/lib/active_record/associations/collection_association.rb +268 -186
  22. data/lib/active_record/associations/collection_proxy.rb +1003 -63
  23. data/lib/active_record/associations/foreign_association.rb +11 -0
  24. data/lib/active_record/associations/has_many_association.rb +81 -41
  25. data/lib/active_record/associations/has_many_through_association.rb +76 -55
  26. data/lib/active_record/associations/has_one_association.rb +51 -21
  27. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  28. data/lib/active_record/associations/join_dependency/join_association.rb +83 -108
  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 +239 -155
  32. data/lib/active_record/associations/preloader/association.rb +97 -62
  33. data/lib/active_record/associations/preloader/collection_association.rb +2 -8
  34. data/lib/active_record/associations/preloader/has_many_through.rb +7 -3
  35. data/lib/active_record/associations/preloader/has_one.rb +0 -8
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +75 -33
  38. data/lib/active_record/associations/preloader.rb +111 -79
  39. data/lib/active_record/associations/singular_association.rb +35 -13
  40. data/lib/active_record/associations/through_association.rb +41 -19
  41. data/lib/active_record/associations.rb +727 -501
  42. data/lib/active_record/attribute/user_provided_default.rb +28 -0
  43. data/lib/active_record/attribute.rb +213 -0
  44. data/lib/active_record/attribute_assignment.rb +32 -162
  45. data/lib/active_record/attribute_decorators.rb +67 -0
  46. data/lib/active_record/attribute_methods/before_type_cast.rb +52 -7
  47. data/lib/active_record/attribute_methods/dirty.rb +101 -61
  48. data/lib/active_record/attribute_methods/primary_key.rb +50 -36
  49. data/lib/active_record/attribute_methods/query.rb +7 -6
  50. data/lib/active_record/attribute_methods/read.rb +56 -117
  51. data/lib/active_record/attribute_methods/serialization.rb +43 -96
  52. data/lib/active_record/attribute_methods/time_zone_conversion.rb +93 -42
  53. data/lib/active_record/attribute_methods/write.rb +34 -45
  54. data/lib/active_record/attribute_methods.rb +333 -144
  55. data/lib/active_record/attribute_mutation_tracker.rb +70 -0
  56. data/lib/active_record/attribute_set/builder.rb +108 -0
  57. data/lib/active_record/attribute_set.rb +108 -0
  58. data/lib/active_record/attributes.rb +265 -0
  59. data/lib/active_record/autosave_association.rb +285 -223
  60. data/lib/active_record/base.rb +95 -490
  61. data/lib/active_record/callbacks.rb +95 -61
  62. data/lib/active_record/coders/json.rb +13 -0
  63. data/lib/active_record/coders/yaml_column.rb +28 -19
  64. data/lib/active_record/collection_cache_key.rb +40 -0
  65. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +724 -277
  66. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  67. data/lib/active_record/connection_adapters/abstract/database_statements.rb +199 -192
  68. data/lib/active_record/connection_adapters/abstract/query_cache.rb +31 -26
  69. data/lib/active_record/connection_adapters/abstract/quoting.rb +140 -57
  70. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +147 -0
  72. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +419 -276
  73. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +105 -0
  74. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +963 -276
  75. data/lib/active_record/connection_adapters/abstract/transaction.rb +232 -0
  76. data/lib/active_record/connection_adapters/abstract_adapter.rb +397 -106
  77. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +643 -342
  78. data/lib/active_record/connection_adapters/column.rb +30 -259
  79. data/lib/active_record/connection_adapters/connection_specification.rb +263 -0
  80. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +22 -0
  81. data/lib/active_record/connection_adapters/mysql/column.rb +50 -0
  82. data/lib/active_record/connection_adapters/mysql/database_statements.rb +125 -0
  83. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +70 -0
  84. data/lib/active_record/connection_adapters/mysql/quoting.rb +51 -0
  85. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +67 -0
  86. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +93 -0
  87. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +54 -0
  88. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +32 -0
  89. data/lib/active_record/connection_adapters/mysql2_adapter.rb +47 -196
  90. data/lib/active_record/connection_adapters/postgresql/column.rb +15 -0
  91. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +170 -0
  92. data/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +42 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +70 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +15 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +48 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +21 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +19 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +10 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +39 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb +50 -0
  108. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +93 -0
  109. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +15 -0
  110. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +109 -0
  111. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  112. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  113. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  114. data/lib/active_record/connection_adapters/postgresql/oid.rb +31 -0
  115. data/lib/active_record/connection_adapters/postgresql/quoting.rb +116 -0
  116. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +49 -0
  117. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +180 -0
  118. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +47 -0
  119. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +682 -0
  120. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +35 -0
  121. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  122. data/lib/active_record/connection_adapters/postgresql_adapter.rb +558 -1039
  123. data/lib/active_record/connection_adapters/schema_cache.rb +74 -36
  124. data/lib/active_record/connection_adapters/sql_type_metadata.rb +32 -0
  125. data/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +19 -0
  126. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +48 -0
  127. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  128. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +538 -24
  129. data/lib/active_record/connection_adapters/statement_pool.rb +31 -12
  130. data/lib/active_record/connection_handling.rb +155 -0
  131. data/lib/active_record/core.rb +561 -0
  132. data/lib/active_record/counter_cache.rb +146 -105
  133. data/lib/active_record/dynamic_matchers.rb +101 -64
  134. data/lib/active_record/enum.rb +234 -0
  135. data/lib/active_record/errors.rb +153 -56
  136. data/lib/active_record/explain.rb +15 -63
  137. data/lib/active_record/explain_registry.rb +30 -0
  138. data/lib/active_record/explain_subscriber.rb +10 -6
  139. data/lib/active_record/fixture_set/file.rb +77 -0
  140. data/lib/active_record/fixtures.rb +355 -232
  141. data/lib/active_record/gem_version.rb +15 -0
  142. data/lib/active_record/inheritance.rb +144 -79
  143. data/lib/active_record/integration.rb +66 -13
  144. data/lib/active_record/internal_metadata.rb +56 -0
  145. data/lib/active_record/legacy_yaml_adapter.rb +46 -0
  146. data/lib/active_record/locale/en.yml +9 -1
  147. data/lib/active_record/locking/optimistic.rb +77 -56
  148. data/lib/active_record/locking/pessimistic.rb +6 -6
  149. data/lib/active_record/log_subscriber.rb +53 -28
  150. data/lib/active_record/migration/command_recorder.rb +166 -33
  151. data/lib/active_record/migration/compatibility.rb +126 -0
  152. data/lib/active_record/migration/join_table.rb +15 -0
  153. data/lib/active_record/migration.rb +792 -264
  154. data/lib/active_record/model_schema.rb +192 -130
  155. data/lib/active_record/nested_attributes.rb +238 -145
  156. data/lib/active_record/no_touching.rb +52 -0
  157. data/lib/active_record/null_relation.rb +89 -0
  158. data/lib/active_record/persistence.rb +357 -157
  159. data/lib/active_record/query_cache.rb +22 -43
  160. data/lib/active_record/querying.rb +34 -23
  161. data/lib/active_record/railtie.rb +88 -48
  162. data/lib/active_record/railties/console_sandbox.rb +3 -4
  163. data/lib/active_record/railties/controller_runtime.rb +5 -4
  164. data/lib/active_record/railties/databases.rake +170 -422
  165. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  166. data/lib/active_record/readonly_attributes.rb +2 -5
  167. data/lib/active_record/reflection.rb +715 -189
  168. data/lib/active_record/relation/batches/batch_enumerator.rb +67 -0
  169. data/lib/active_record/relation/batches.rb +203 -50
  170. data/lib/active_record/relation/calculations.rb +203 -194
  171. data/lib/active_record/relation/delegation.rb +103 -25
  172. data/lib/active_record/relation/finder_methods.rb +457 -261
  173. data/lib/active_record/relation/from_clause.rb +32 -0
  174. data/lib/active_record/relation/merger.rb +167 -0
  175. data/lib/active_record/relation/predicate_builder/array_handler.rb +43 -0
  176. data/lib/active_record/relation/predicate_builder/association_query_handler.rb +88 -0
  177. data/lib/active_record/relation/predicate_builder/base_handler.rb +17 -0
  178. data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +17 -0
  179. data/lib/active_record/relation/predicate_builder/class_handler.rb +27 -0
  180. data/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +57 -0
  181. data/lib/active_record/relation/predicate_builder/range_handler.rb +33 -0
  182. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  183. data/lib/active_record/relation/predicate_builder.rb +153 -48
  184. data/lib/active_record/relation/query_attribute.rb +19 -0
  185. data/lib/active_record/relation/query_methods.rb +1019 -194
  186. data/lib/active_record/relation/record_fetch_warning.rb +49 -0
  187. data/lib/active_record/relation/spawn_methods.rb +46 -150
  188. data/lib/active_record/relation/where_clause.rb +174 -0
  189. data/lib/active_record/relation/where_clause_factory.rb +38 -0
  190. data/lib/active_record/relation.rb +450 -245
  191. data/lib/active_record/result.rb +104 -12
  192. data/lib/active_record/runtime_registry.rb +22 -0
  193. data/lib/active_record/sanitization.rb +120 -94
  194. data/lib/active_record/schema.rb +28 -18
  195. data/lib/active_record/schema_dumper.rb +141 -74
  196. data/lib/active_record/schema_migration.rb +50 -0
  197. data/lib/active_record/scoping/default.rb +64 -57
  198. data/lib/active_record/scoping/named.rb +93 -108
  199. data/lib/active_record/scoping.rb +73 -121
  200. data/lib/active_record/secure_token.rb +38 -0
  201. data/lib/active_record/serialization.rb +7 -5
  202. data/lib/active_record/statement_cache.rb +113 -0
  203. data/lib/active_record/store.rb +173 -15
  204. data/lib/active_record/suppressor.rb +58 -0
  205. data/lib/active_record/table_metadata.rb +68 -0
  206. data/lib/active_record/tasks/database_tasks.rb +313 -0
  207. data/lib/active_record/tasks/mysql_database_tasks.rb +151 -0
  208. data/lib/active_record/tasks/postgresql_database_tasks.rb +110 -0
  209. data/lib/active_record/tasks/sqlite_database_tasks.rb +59 -0
  210. data/lib/active_record/timestamp.rb +42 -24
  211. data/lib/active_record/touch_later.rb +58 -0
  212. data/lib/active_record/transactions.rb +233 -105
  213. data/lib/active_record/type/adapter_specific_registry.rb +130 -0
  214. data/lib/active_record/type/date.rb +7 -0
  215. data/lib/active_record/type/date_time.rb +7 -0
  216. data/lib/active_record/type/hash_lookup_type_map.rb +23 -0
  217. data/lib/active_record/type/internal/abstract_json.rb +29 -0
  218. data/lib/active_record/type/internal/timezone.rb +15 -0
  219. data/lib/active_record/type/serialized.rb +63 -0
  220. data/lib/active_record/type/time.rb +20 -0
  221. data/lib/active_record/type/type_map.rb +64 -0
  222. data/lib/active_record/type.rb +72 -0
  223. data/lib/active_record/type_caster/connection.rb +29 -0
  224. data/lib/active_record/type_caster/map.rb +19 -0
  225. data/lib/active_record/type_caster.rb +7 -0
  226. data/lib/active_record/validations/absence.rb +23 -0
  227. data/lib/active_record/validations/associated.rb +33 -18
  228. data/lib/active_record/validations/length.rb +24 -0
  229. data/lib/active_record/validations/presence.rb +66 -0
  230. data/lib/active_record/validations/uniqueness.rb +128 -68
  231. data/lib/active_record/validations.rb +48 -40
  232. data/lib/active_record/version.rb +5 -7
  233. data/lib/active_record.rb +71 -47
  234. data/lib/rails/generators/active_record/migration/migration_generator.rb +56 -8
  235. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +24 -0
  236. data/lib/rails/generators/active_record/migration/templates/migration.rb +28 -16
  237. data/lib/rails/generators/active_record/migration.rb +18 -8
  238. data/lib/rails/generators/active_record/model/model_generator.rb +38 -16
  239. data/lib/rails/generators/active_record/model/templates/application_record.rb +5 -0
  240. data/lib/rails/generators/active_record/model/templates/model.rb +7 -6
  241. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  242. data/lib/rails/generators/active_record.rb +3 -11
  243. metadata +188 -134
  244. data/examples/associations.png +0 -0
  245. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -63
  246. data/lib/active_record/associations/join_helper.rb +0 -55
  247. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  248. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +0 -32
  249. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -191
  250. data/lib/active_record/connection_adapters/mysql_adapter.rb +0 -441
  251. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -583
  252. data/lib/active_record/dynamic_finder_match.rb +0 -68
  253. data/lib/active_record/dynamic_scope_match.rb +0 -23
  254. data/lib/active_record/fixtures/file.rb +0 -65
  255. data/lib/active_record/identity_map.rb +0 -162
  256. data/lib/active_record/observer.rb +0 -121
  257. data/lib/active_record/serializers/xml_serializer.rb +0 -203
  258. data/lib/active_record/session_store.rb +0 -360
  259. data/lib/active_record/test_case.rb +0 -73
  260. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -15
  261. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  262. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  263. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  264. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -12
@@ -2,22 +2,16 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class CollectionAssociation < Association #:nodoc:
5
-
6
5
  private
7
6
 
8
- def build_scope
9
- super.order(preload_options[:order] || options[:order])
10
- end
11
-
12
- def preload
13
- associated_records_by_owner.each do |owner, records|
7
+ def preload(preloader)
8
+ associated_records_by_owner(preloader).each do |owner, records|
14
9
  association = owner.association(reflection.name)
15
10
  association.loaded!
16
11
  association.target.concat(records)
17
12
  records.each { |record| association.set_inverse_instance(record) }
18
13
  end
19
14
  end
20
-
21
15
  end
22
16
  end
23
17
  end
@@ -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(&:uniq!)
10
12
  end
13
+
14
+ records_by_owner
11
15
  end
12
16
  end
13
17
  end
@@ -2,7 +2,6 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class HasOne < SingularAssociation #:nodoc:
5
-
6
5
  def association_key_name
7
6
  reflection.foreign_key
8
7
  end
@@ -10,13 +9,6 @@ module ActiveRecord
10
9
  def owner_key_name
11
10
  reflection.active_record_primary_key
12
11
  end
13
-
14
- private
15
-
16
- def build_scope
17
- super.order(preload_options[:order] || options[:order])
18
- end
19
-
20
12
  end
21
13
  end
22
14
  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,98 @@ 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
+ center = target_records_from_association(association)
22
+ [owner, Array(center)]
25
23
  end
26
- end
27
24
 
28
- private
25
+ reset_association owners, through_reflection.name
29
26
 
30
- def through_records_by_owner
31
- ActiveRecord::Associations::Preloader.new(
32
- owners, through_reflection.name,
33
- through_options
34
- ).run
27
+ middle_records = through_records.flat_map { |(_,rec)| rec }
35
28
 
36
- Hash[owners.map do |owner|
37
- through_records = Array.wrap(owner.send(through_reflection.name))
29
+ preloaders = preloader.preload(middle_records,
30
+ source_reflection.name,
31
+ reflection_scope)
32
+
33
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
34
+
35
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
36
+ pl.owners.each { |middle|
37
+ h[middle] = pl
38
+ }
39
+ end
38
40
 
39
- # Dont cache the association - we would only be caching a subset
40
- if (preload_options != through_options) ||
41
- (reflection.options[:source_type] && through_reflection.collection?)
42
- owner.association(through_reflection.name).reset
41
+ through_records.each_with_object({}) do |(lhs,center), records_by_owner|
42
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
43
+
44
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
45
+ rhs_records = middles.flat_map { |r|
46
+ association = r.association source_reflection.name
47
+
48
+ target_records_from_association(association)
49
+ }.compact
50
+
51
+ # Respect the order on `reflection_scope` if it exists, else use the natural order.
52
+ if reflection_scope.values[:order].present?
53
+ @id_map ||= id_to_index_map @preloaded_records
54
+ rhs_records.sort_by { |rhs| @id_map[rhs] }
55
+ else
56
+ rhs_records
57
+ end
43
58
  end
59
+ end
60
+ end
44
61
 
45
- [owner, through_records]
46
- end]
62
+ private
63
+
64
+ def id_to_index_map(ids)
65
+ id_map = {}
66
+ ids.each_with_index { |id, index| id_map[id] = index }
67
+ id_map
68
+ end
69
+
70
+ def reset_association(owners, association_name)
71
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
72
+ (reflection.options[:source_type] && through_reflection.collection?)
73
+
74
+ # Don't cache the association - we would only be caching a subset
75
+ if should_reset
76
+ owners.each { |owner|
77
+ owner.association(association_name).reset
78
+ }
79
+ end
47
80
  end
48
81
 
49
- def through_options
50
- through_options = {}
82
+
83
+ def through_scope
84
+ scope = through_reflection.klass.unscoped
51
85
 
52
86
  if options[:source_type]
53
- through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
87
+ scope.where! reflection.foreign_type => options[:source_type]
54
88
  else
55
- if options[:conditions]
56
- through_options[:include] = options[:include] || options[:source]
57
- through_options[:conditions] = options[:conditions]
89
+ unless reflection_scope.where_clause.empty?
90
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
91
+ scope.where_clause = reflection_scope.where_clause
92
+ end
93
+
94
+ scope.references! reflection_scope.values[:references]
95
+ if scope.eager_loading? && order_values = reflection_scope.values[:order]
96
+ scope = scope.order(order_values)
58
97
  end
59
- through_options[:order] = options[:order] if options.has_key?(:order)
60
98
  end
61
99
 
62
- through_options
100
+ scope
101
+ end
102
+
103
+ def target_records_from_association(association)
104
+ association.loaded? ? association.target : association.reader
63
105
  end
64
106
  end
65
107
  end
@@ -2,33 +2,42 @@ 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
42
  extend ActiveSupport::Autoload
34
43
 
@@ -42,11 +51,10 @@ module ActiveRecord
42
51
  autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
43
52
  autoload :HasOne, 'active_record/associations/preloader/has_one'
44
53
  autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
45
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
46
54
  autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
47
55
  end
48
56
 
49
- attr_reader :records, :associations, :options, :model
57
+ NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
50
58
 
51
59
  # Eager loads the named associations for the given Active Record record(s).
52
60
  #
@@ -72,7 +80,7 @@ module ActiveRecord
72
80
  # books.
73
81
  # - a Hash which specifies multiple association names, as well as
74
82
  # association names for the to-be-preloaded association objects. For
75
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
83
+ # example, specifying <tt>{ author: :avatar }</tt> will preload a
76
84
  # book's author, as well as that author's avatar.
77
85
  #
78
86
  # +:associations+ has the same format as the +:include+ option for
@@ -80,45 +88,55 @@ module ActiveRecord
80
88
  #
81
89
  # :books
82
90
  # [ :books, :author ]
83
- # { :author => :avatar }
84
- # [ :books, { :author => :avatar } ]
85
- #
86
- # +options+ contains options that will be passed to ActiveRecord::Base#find
87
- # (which is called under the hood for preloading records). But it is passed
88
- # only one level deep in the +associations+ argument, i.e. it's not passed
89
- # to the child associations when +associations+ is a Hash.
90
- def initialize(records, associations, options = {})
91
- @records = Array.wrap(records).compact.uniq
92
- @associations = Array.wrap(associations)
93
- @options = options
94
- end
91
+ # { author: :avatar }
92
+ # [ :books, { author: :avatar } ]
93
+ def preload(records, associations, preload_scope = nil)
94
+ records = Array.wrap(records).compact.uniq
95
+ associations = Array.wrap(associations)
96
+ preload_scope = preload_scope || NULL_RELATION
95
97
 
96
- def run
97
- unless records.empty?
98
- associations.each { |association| preload(association) }
98
+ if records.empty?
99
+ []
100
+ else
101
+ associations.flat_map { |association|
102
+ preloaders_on association, records, preload_scope
103
+ }
99
104
  end
100
105
  end
101
106
 
102
107
  private
103
108
 
104
- def preload(association)
109
+ # Loads all the given data into +records+ for the +association+.
110
+ def preloaders_on(association, records, scope)
105
111
  case association
106
112
  when Hash
107
- preload_hash(association)
108
- when String, Symbol
109
- 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)
110
118
  else
111
- raise ArgumentError, "#{association.inspect} was not recognised for preload"
119
+ raise ArgumentError, "#{association.inspect} was not recognized for preload"
112
120
  end
113
121
  end
114
122
 
115
- def preload_hash(association)
116
- association.each do |parent, child|
117
- Preloader.new(records, parent, options).run
118
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
119
- 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
+ }
120
133
  end
121
134
 
135
+ # Loads all the given data into +records+ for a singular +association+.
136
+ #
137
+ # Functions by instantiating a preloader class such as Preloader::HasManyThrough and
138
+ # call the +run+ method for each passed in class in the +records+ argument.
139
+ #
122
140
  # Not all records have the same class, so group then preload group on the reflection
123
141
  # itself so that if various subclass share the same association then we do not split
124
142
  # them unnecessarily
@@ -126,52 +144,66 @@ module ActiveRecord
126
144
  # Additionally, polymorphic belongs_to associations can have multiple associated
127
145
  # classes, depending on the polymorphic_type field. So we group by the classes as
128
146
  # well.
129
- def preload_one(association)
130
- grouped_records(association).each do |reflection, klasses|
131
- klasses.each do |klass, records|
132
- preloader_for(reflection).new(klass, records, reflection, options).run
147
+ def preloaders_for_one(association, records, scope)
148
+ grouped_records(association, records).flat_map do |reflection, klasses|
149
+ klasses.map do |rhs_klass, rs|
150
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
151
+ loader.run self
152
+ loader
133
153
  end
134
154
  end
135
155
  end
136
156
 
137
- def grouped_records(association)
138
- Hash[
139
- records_by_reflection(association).map do |reflection, records|
140
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
141
- end
142
- ]
157
+ def grouped_records(association, records)
158
+ h = {}
159
+ records.each do |record|
160
+ next unless record
161
+ assoc = record.association(association)
162
+ klasses = h[assoc.reflection] ||= {}
163
+ (klasses[assoc.klass] ||= []) << record
164
+ end
165
+ h
143
166
  end
144
167
 
145
- def records_by_reflection(association)
146
- records.group_by do |record|
147
- reflection = record.class.reflections[association]
168
+ class AlreadyLoaded # :nodoc:
169
+ attr_reader :owners, :reflection
148
170
 
149
- unless reflection
150
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
151
- "perhaps you misspelled it?"
152
- end
171
+ def initialize(klass, owners, reflection, preload_scope)
172
+ @owners = owners
173
+ @reflection = reflection
174
+ end
175
+
176
+ def run(preloader); end
153
177
 
154
- reflection
178
+ def preloaded_records
179
+ owners.flat_map { |owner| owner.association(reflection.name).target }
155
180
  end
156
181
  end
157
182
 
158
- def association_klass(reflection, record)
159
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
160
- klass = record.send(reflection.foreign_type)
161
- klass && klass.constantize
162
- else
163
- reflection.klass
164
- end
183
+ class NullPreloader # :nodoc:
184
+ def self.new(klass, owners, reflection, preload_scope); self; end
185
+ def self.run(preloader); end
186
+ def self.preloaded_records; []; end
187
+ def self.owners; []; end
165
188
  end
166
189
 
167
- def preloader_for(reflection)
190
+ # Returns a class containing the logic needed to load preload the data
191
+ # and attach it to a relation. For example +Preloader::Association+ or
192
+ # +Preloader::HasManyThrough+. The class returned implements a `run` method
193
+ # that accepts a preloader.
194
+ def preloader_for(reflection, owners, rhs_klass)
195
+ return NullPreloader unless rhs_klass
196
+
197
+ if owners.first.association(reflection.name).loaded?
198
+ return AlreadyLoaded
199
+ end
200
+ reflection.check_preloadable!
201
+
168
202
  case reflection.macro
169
203
  when :has_many
170
204
  reflection.options[:through] ? HasManyThrough : HasMany
171
205
  when :has_one
172
206
  reflection.options[:through] ? HasOneThrough : HasOne
173
- when :has_and_belongs_to_many
174
- HasAndBelongsToMany
175
207
  when :belongs_to
176
208
  BelongsTo
177
209
  end
@@ -3,7 +3,13 @@ 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
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
8
+ Passing an argument to force an association to reload is now
9
+ deprecated and will be removed in Rails 5.1. Please call `reload`
10
+ on the parent object instead.
11
+ MSG
12
+
7
13
  klass.uncached { reload }
8
14
  elsif !loaded? || stale_target?
9
15
  reload
@@ -12,21 +18,21 @@ module ActiveRecord
12
18
  target
13
19
  end
14
20
 
15
- # Implements the writer method, e.g. foo.items= for Foo.has_many :items
21
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
16
22
  def writer(record)
17
23
  replace(record)
18
24
  end
19
25
 
20
- def create(attributes = {}, options = {}, &block)
21
- create_record(attributes, options, &block)
26
+ def create(attributes = {}, &block)
27
+ _create_record(attributes, &block)
22
28
  end
23
29
 
24
- def create!(attributes = {}, options = {}, &block)
25
- create_record(attributes, options, true, &block)
30
+ def create!(attributes = {}, &block)
31
+ _create_record(attributes, true, &block)
26
32
  end
27
33
 
28
- def build(attributes = {}, options = {})
29
- record = build_record(attributes, options)
34
+ def build(attributes = {})
35
+ record = build_record(attributes)
30
36
  yield(record) if block_given?
31
37
  set_new_record(record)
32
38
  record
@@ -35,14 +41,30 @@ module ActiveRecord
35
41
  private
36
42
 
37
43
  def create_scope
38
- scoped.scope_for_create.stringify_keys.except(klass.primary_key)
44
+ scope.scope_for_create.stringify_keys.except(klass.primary_key)
45
+ end
46
+
47
+ def get_records
48
+ return scope.limit(1).records if skip_statement_cache?
49
+
50
+ conn = klass.connection
51
+ sc = reflection.association_scope_cache(conn, owner) do
52
+ StatementCache.create(conn) { |params|
53
+ as = AssociationScope.create { params.bind }
54
+ target_scope.merge(as.scope(self, conn)).limit(1)
55
+ }
56
+ end
57
+
58
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
59
+ sc.execute binds, klass, klass.connection
39
60
  end
40
61
 
41
62
  def find_target
42
- scoped.first.tap { |record| set_inverse_instance(record) }
63
+ if record = get_records.first
64
+ set_inverse_instance record
65
+ end
43
66
  end
44
67
 
45
- # Implemented by subclasses
46
68
  def replace(record)
47
69
  raise NotImplementedError, "Subclasses must implement a replace(record) method"
48
70
  end
@@ -51,8 +73,8 @@ module ActiveRecord
51
73
  replace(record)
52
74
  end
53
75
 
54
- def create_record(attributes, options, raise_error = false)
55
- record = build_record(attributes, options)
76
+ def _create_record(attributes, raise_error = false)
77
+ record = build_record(attributes)
56
78
  yield(record) if block_given?
57
79
  saved = record.save
58
80
  set_new_record(record)