activerecord 1.0.0 → 2.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 (311) hide show
  1. data/CHANGELOG +4928 -3
  2. data/README +45 -46
  3. data/RUNNING_UNIT_TESTS +8 -11
  4. data/Rakefile +247 -0
  5. data/install.rb +8 -38
  6. data/lib/active_record/aggregations.rb +64 -49
  7. data/lib/active_record/associations/association_collection.rb +217 -47
  8. data/lib/active_record/associations/association_proxy.rb +159 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +56 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +155 -37
  12. data/lib/active_record/associations/has_many_association.rb +145 -75
  13. data/lib/active_record/associations/has_many_through_association.rb +283 -0
  14. data/lib/active_record/associations/has_one_association.rb +96 -0
  15. data/lib/active_record/associations.rb +1537 -304
  16. data/lib/active_record/attribute_methods.rb +328 -0
  17. data/lib/active_record/base.rb +2001 -588
  18. data/lib/active_record/calculations.rb +269 -0
  19. data/lib/active_record/callbacks.rb +169 -165
  20. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +308 -0
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -0
  22. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  23. data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
  24. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +472 -0
  25. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +306 -0
  26. data/lib/active_record/connection_adapters/abstract_adapter.rb +125 -279
  27. data/lib/active_record/connection_adapters/mysql_adapter.rb +442 -77
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +805 -135
  29. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  30. data/lib/active_record/connection_adapters/sqlite_adapter.rb +353 -69
  31. data/lib/active_record/fixtures.rb +946 -100
  32. data/lib/active_record/locking/optimistic.rb +144 -0
  33. data/lib/active_record/locking/pessimistic.rb +77 -0
  34. data/lib/active_record/migration.rb +417 -0
  35. data/lib/active_record/observer.rb +142 -32
  36. data/lib/active_record/query_cache.rb +23 -0
  37. data/lib/active_record/reflection.rb +163 -70
  38. data/lib/active_record/schema.rb +58 -0
  39. data/lib/active_record/schema_dumper.rb +171 -0
  40. data/lib/active_record/serialization.rb +98 -0
  41. data/lib/active_record/serializers/json_serializer.rb +71 -0
  42. data/lib/active_record/serializers/xml_serializer.rb +315 -0
  43. data/lib/active_record/timestamp.rb +41 -0
  44. data/lib/active_record/transactions.rb +87 -57
  45. data/lib/active_record/validations.rb +909 -122
  46. data/lib/active_record/vendor/db2.rb +362 -0
  47. data/lib/active_record/vendor/mysql.rb +126 -29
  48. data/lib/active_record/version.rb +9 -0
  49. data/lib/active_record.rb +35 -7
  50. data/lib/activerecord.rb +1 -0
  51. data/test/aaa_create_tables_test.rb +72 -0
  52. data/test/abstract_unit.rb +73 -5
  53. data/test/active_schema_test_mysql.rb +43 -0
  54. data/test/adapter_test.rb +105 -0
  55. data/test/adapter_test_sqlserver.rb +95 -0
  56. data/test/aggregations_test.rb +110 -16
  57. data/test/all.sh +2 -2
  58. data/test/ar_schema_test.rb +33 -0
  59. data/test/association_inheritance_reload.rb +14 -0
  60. data/test/associations/ar_joins_test.rb +0 -0
  61. data/test/associations/callbacks_test.rb +147 -0
  62. data/test/associations/cascaded_eager_loading_test.rb +110 -0
  63. data/test/associations/eager_singularization_test.rb +145 -0
  64. data/test/associations/eager_test.rb +442 -0
  65. data/test/associations/extension_test.rb +47 -0
  66. data/test/associations/inner_join_association_test.rb +88 -0
  67. data/test/associations/join_model_test.rb +553 -0
  68. data/test/associations_test.rb +1930 -267
  69. data/test/attribute_methods_test.rb +146 -0
  70. data/test/base_test.rb +1316 -84
  71. data/test/binary_test.rb +32 -0
  72. data/test/calculations_test.rb +251 -0
  73. data/test/callbacks_test.rb +400 -0
  74. data/test/class_inheritable_attributes_test.rb +3 -4
  75. data/test/column_alias_test.rb +17 -0
  76. data/test/connection_test_firebird.rb +8 -0
  77. data/test/connection_test_mysql.rb +30 -0
  78. data/test/connections/native_db2/connection.rb +25 -0
  79. data/test/connections/native_firebird/connection.rb +26 -0
  80. data/test/connections/native_frontbase/connection.rb +27 -0
  81. data/test/connections/native_mysql/connection.rb +21 -18
  82. data/test/connections/native_openbase/connection.rb +21 -0
  83. data/test/connections/native_oracle/connection.rb +27 -0
  84. data/test/connections/native_postgresql/connection.rb +17 -18
  85. data/test/connections/native_sqlite/connection.rb +17 -16
  86. data/test/connections/native_sqlite3/connection.rb +25 -0
  87. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  88. data/test/connections/native_sybase/connection.rb +23 -0
  89. data/test/copy_table_test_sqlite.rb +69 -0
  90. data/test/datatype_test_postgresql.rb +203 -0
  91. data/test/date_time_test.rb +37 -0
  92. data/test/default_test_firebird.rb +16 -0
  93. data/test/defaults_test.rb +67 -0
  94. data/test/deprecated_finder_test.rb +30 -0
  95. data/test/finder_test.rb +607 -32
  96. data/test/fixtures/accounts.yml +28 -0
  97. data/test/fixtures/all/developers.yml +0 -0
  98. data/test/fixtures/all/people.csv +0 -0
  99. data/test/fixtures/all/tasks.yml +0 -0
  100. data/test/fixtures/author.rb +107 -0
  101. data/test/fixtures/author_favorites.yml +4 -0
  102. data/test/fixtures/authors.yml +7 -0
  103. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  104. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  105. data/test/fixtures/bad_fixtures/blank_line +3 -0
  106. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  107. data/test/fixtures/bad_fixtures/missing_value +1 -0
  108. data/test/fixtures/binaries.yml +132 -0
  109. data/test/fixtures/binary.rb +2 -0
  110. data/test/fixtures/book.rb +4 -0
  111. data/test/fixtures/books.yml +7 -0
  112. data/test/fixtures/categories/special_categories.yml +9 -0
  113. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  114. data/test/fixtures/categories.yml +14 -0
  115. data/test/fixtures/categories_ordered.yml +7 -0
  116. data/test/fixtures/categories_posts.yml +23 -0
  117. data/test/fixtures/categorization.rb +5 -0
  118. data/test/fixtures/categorizations.yml +17 -0
  119. data/test/fixtures/category.rb +26 -0
  120. data/test/fixtures/citation.rb +6 -0
  121. data/test/fixtures/comment.rb +23 -0
  122. data/test/fixtures/comments.yml +59 -0
  123. data/test/fixtures/companies.yml +55 -0
  124. data/test/fixtures/company.rb +81 -4
  125. data/test/fixtures/company_in_module.rb +32 -6
  126. data/test/fixtures/computer.rb +4 -0
  127. data/test/fixtures/computers.yml +4 -0
  128. data/test/fixtures/contact.rb +16 -0
  129. data/test/fixtures/courses.yml +7 -0
  130. data/test/fixtures/customer.rb +28 -3
  131. data/test/fixtures/customers.yml +17 -0
  132. data/test/fixtures/db_definitions/db2.drop.sql +33 -0
  133. data/test/fixtures/db_definitions/db2.sql +235 -0
  134. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  135. data/test/fixtures/db_definitions/db22.sql +5 -0
  136. data/test/fixtures/db_definitions/firebird.drop.sql +65 -0
  137. data/test/fixtures/db_definitions/firebird.sql +310 -0
  138. data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
  139. data/test/fixtures/db_definitions/firebird2.sql +6 -0
  140. data/test/fixtures/db_definitions/frontbase.drop.sql +33 -0
  141. data/test/fixtures/db_definitions/frontbase.sql +273 -0
  142. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  143. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  144. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  145. data/test/fixtures/db_definitions/openbase.sql +318 -0
  146. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  147. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  148. data/test/fixtures/db_definitions/oracle.drop.sql +67 -0
  149. data/test/fixtures/db_definitions/oracle.sql +330 -0
  150. data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
  151. data/test/fixtures/db_definitions/oracle2.sql +6 -0
  152. data/test/fixtures/db_definitions/postgresql.drop.sql +44 -0
  153. data/test/fixtures/db_definitions/postgresql.sql +217 -38
  154. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  155. data/test/fixtures/db_definitions/postgresql2.sql +2 -2
  156. data/test/fixtures/db_definitions/schema.rb +354 -0
  157. data/test/fixtures/db_definitions/schema2.rb +11 -0
  158. data/test/fixtures/db_definitions/sqlite.drop.sql +33 -0
  159. data/test/fixtures/db_definitions/sqlite.sql +139 -5
  160. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  161. data/test/fixtures/db_definitions/sqlite2.sql +1 -0
  162. data/test/fixtures/db_definitions/sybase.drop.sql +35 -0
  163. data/test/fixtures/db_definitions/sybase.sql +222 -0
  164. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  165. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  166. data/test/fixtures/developer.rb +70 -6
  167. data/test/fixtures/developers.yml +21 -0
  168. data/test/fixtures/developers_projects/david_action_controller +2 -1
  169. data/test/fixtures/developers_projects/david_active_record +2 -1
  170. data/test/fixtures/developers_projects.yml +17 -0
  171. data/test/fixtures/edge.rb +5 -0
  172. data/test/fixtures/edges.yml +6 -0
  173. data/test/fixtures/entrants.yml +14 -0
  174. data/test/fixtures/example.log +1 -0
  175. data/test/fixtures/fk_test_has_fk.yml +3 -0
  176. data/test/fixtures/fk_test_has_pk.yml +2 -0
  177. data/test/fixtures/flowers.jpg +0 -0
  178. data/test/fixtures/funny_jokes.yml +10 -0
  179. data/test/fixtures/item.rb +7 -0
  180. data/test/fixtures/items.yml +4 -0
  181. data/test/fixtures/joke.rb +3 -0
  182. data/test/fixtures/keyboard.rb +3 -0
  183. data/test/fixtures/legacy_thing.rb +3 -0
  184. data/test/fixtures/legacy_things.yml +3 -0
  185. data/test/fixtures/matey.rb +4 -0
  186. data/test/fixtures/mateys.yml +4 -0
  187. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  188. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  189. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  190. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  191. data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
  192. data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
  193. data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
  194. data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
  195. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  196. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  197. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  198. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  199. data/test/fixtures/minimalistic.rb +2 -0
  200. data/test/fixtures/minimalistics.yml +2 -0
  201. data/test/fixtures/mixed_case_monkey.rb +3 -0
  202. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  203. data/test/fixtures/mixins.yml +29 -0
  204. data/test/fixtures/movies.yml +7 -0
  205. data/test/fixtures/naked/csv/accounts.csv +1 -0
  206. data/test/fixtures/naked/yml/accounts.yml +1 -0
  207. data/test/fixtures/naked/yml/companies.yml +1 -0
  208. data/test/fixtures/naked/yml/courses.yml +1 -0
  209. data/test/fixtures/order.rb +4 -0
  210. data/test/fixtures/parrot.rb +13 -0
  211. data/test/fixtures/parrots.yml +27 -0
  212. data/test/fixtures/parrots_pirates.yml +7 -0
  213. data/test/fixtures/people.yml +3 -0
  214. data/test/fixtures/person.rb +4 -0
  215. data/test/fixtures/pirate.rb +5 -0
  216. data/test/fixtures/pirates.yml +9 -0
  217. data/test/fixtures/post.rb +59 -0
  218. data/test/fixtures/posts.yml +48 -0
  219. data/test/fixtures/project.rb +27 -2
  220. data/test/fixtures/projects.yml +7 -0
  221. data/test/fixtures/reader.rb +4 -0
  222. data/test/fixtures/readers.yml +4 -0
  223. data/test/fixtures/reply.rb +18 -2
  224. data/test/fixtures/reserved_words/distinct.yml +5 -0
  225. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  226. data/test/fixtures/reserved_words/group.yml +14 -0
  227. data/test/fixtures/reserved_words/select.yml +8 -0
  228. data/test/fixtures/reserved_words/values.yml +7 -0
  229. data/test/fixtures/ship.rb +3 -0
  230. data/test/fixtures/ships.yml +5 -0
  231. data/test/fixtures/subject.rb +4 -0
  232. data/test/fixtures/subscriber.rb +4 -3
  233. data/test/fixtures/tag.rb +7 -0
  234. data/test/fixtures/tagging.rb +10 -0
  235. data/test/fixtures/taggings.yml +25 -0
  236. data/test/fixtures/tags.yml +7 -0
  237. data/test/fixtures/task.rb +3 -0
  238. data/test/fixtures/tasks.yml +7 -0
  239. data/test/fixtures/topic.rb +20 -3
  240. data/test/fixtures/topics.yml +22 -0
  241. data/test/fixtures/treasure.rb +4 -0
  242. data/test/fixtures/treasures.yml +10 -0
  243. data/test/fixtures/vertex.rb +9 -0
  244. data/test/fixtures/vertices.yml +4 -0
  245. data/test/fixtures_test.rb +574 -8
  246. data/test/inheritance_test.rb +113 -27
  247. data/test/json_serialization_test.rb +180 -0
  248. data/test/lifecycle_test.rb +56 -29
  249. data/test/locking_test.rb +273 -0
  250. data/test/method_scoping_test.rb +416 -0
  251. data/test/migration_test.rb +933 -0
  252. data/test/migration_test_firebird.rb +124 -0
  253. data/test/mixin_test.rb +95 -0
  254. data/test/modules_test.rb +23 -10
  255. data/test/multiple_db_test.rb +17 -3
  256. data/test/pk_test.rb +59 -15
  257. data/test/query_cache_test.rb +104 -0
  258. data/test/readonly_test.rb +107 -0
  259. data/test/reflection_test.rb +124 -27
  260. data/test/reserved_word_test_mysql.rb +177 -0
  261. data/test/schema_authorization_test_postgresql.rb +75 -0
  262. data/test/schema_dumper_test.rb +131 -0
  263. data/test/schema_test_postgresql.rb +64 -0
  264. data/test/serialization_test.rb +47 -0
  265. data/test/synonym_test_oracle.rb +17 -0
  266. data/test/table_name_test_sqlserver.rb +23 -0
  267. data/test/threaded_connections_test.rb +48 -0
  268. data/test/transactions_test.rb +227 -29
  269. data/test/unconnected_test.rb +14 -6
  270. data/test/validations_test.rb +1293 -32
  271. data/test/xml_serialization_test.rb +202 -0
  272. metadata +347 -143
  273. data/dev-utils/eval_debugger.rb +0 -9
  274. data/examples/associations.rb +0 -87
  275. data/examples/shared_setup.rb +0 -15
  276. data/examples/validation.rb +0 -88
  277. data/lib/active_record/deprecated_associations.rb +0 -70
  278. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  279. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  280. data/lib/active_record/support/clean_logger.rb +0 -10
  281. data/lib/active_record/support/inflector.rb +0 -70
  282. data/lib/active_record/vendor/simple.rb +0 -702
  283. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  284. data/lib/active_record/wrappings.rb +0 -59
  285. data/rakefile +0 -122
  286. data/test/deprecated_associations_test.rb +0 -336
  287. data/test/fixtures/accounts/signals37 +0 -3
  288. data/test/fixtures/accounts/unknown +0 -2
  289. data/test/fixtures/companies/first_client +0 -6
  290. data/test/fixtures/companies/first_firm +0 -4
  291. data/test/fixtures/companies/second_client +0 -6
  292. data/test/fixtures/courses/java +0 -2
  293. data/test/fixtures/courses/ruby +0 -2
  294. data/test/fixtures/customers/david +0 -6
  295. data/test/fixtures/db_definitions/mysql.sql +0 -96
  296. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  297. data/test/fixtures/developers/david +0 -2
  298. data/test/fixtures/developers/jamis +0 -2
  299. data/test/fixtures/entrants/first +0 -3
  300. data/test/fixtures/entrants/second +0 -3
  301. data/test/fixtures/entrants/third +0 -3
  302. data/test/fixtures/fixture_database.sqlite +0 -0
  303. data/test/fixtures/fixture_database_2.sqlite +0 -0
  304. data/test/fixtures/movies/first +0 -2
  305. data/test/fixtures/movies/second +0 -2
  306. data/test/fixtures/projects/action_controller +0 -2
  307. data/test/fixtures/projects/active_record +0 -2
  308. data/test/fixtures/topics/first +0 -9
  309. data/test/fixtures/topics/second +0 -8
  310. data/test/inflector_test.rb +0 -104
  311. data/test/thread_safety_test.rb +0 -33
@@ -1,18 +1,87 @@
1
+ require 'active_record/associations/association_proxy'
1
2
  require 'active_record/associations/association_collection'
3
+ require 'active_record/associations/belongs_to_association'
4
+ require 'active_record/associations/belongs_to_polymorphic_association'
5
+ require 'active_record/associations/has_one_association'
2
6
  require 'active_record/associations/has_many_association'
7
+ require 'active_record/associations/has_many_through_association'
3
8
  require 'active_record/associations/has_and_belongs_to_many_association'
4
- require 'active_record/deprecated_associations'
5
9
 
6
10
  module ActiveRecord
11
+ class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
12
+ def initialize(owner_class_name, reflection)
13
+ super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
14
+ end
15
+ end
16
+
17
+ class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
18
+ def initialize(owner_class_name, reflection, source_reflection)
19
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
20
+ end
21
+ end
22
+
23
+ class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
24
+ def initialize(owner_class_name, reflection, source_reflection)
25
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
26
+ end
27
+ end
28
+
29
+ class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
30
+ def initialize(reflection)
31
+ through_reflection = reflection.through_reflection
32
+ source_reflection_names = reflection.source_reflection_names
33
+ source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
34
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :connector => 'or'}?")
35
+ end
36
+ end
37
+
38
+ class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
39
+ def initialize(reflection)
40
+ through_reflection = reflection.through_reflection
41
+ source_reflection = reflection.source_reflection
42
+ super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
43
+ end
44
+ end
45
+
46
+ class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
47
+ def initialize(owner, reflection)
48
+ super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
49
+ end
50
+ end
51
+
52
+ class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
53
+ def initialize(owner, reflection)
54
+ super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
55
+ end
56
+ end
57
+
58
+ class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
59
+ def initialize(reflection)
60
+ super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
61
+ end
62
+ end
63
+
64
+ class ReadOnlyAssociation < ActiveRecordError #:nodoc:
65
+ def initialize(reflection)
66
+ super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
67
+ end
68
+ end
69
+
7
70
  module Associations # :nodoc:
8
- def self.append_features(base)
9
- super
71
+ def self.included(base)
10
72
  base.extend(ClassMethods)
11
73
  end
12
74
 
75
+ # Clears out the association cache
76
+ def clear_association_cache #:nodoc:
77
+ self.class.reflect_on_all_associations.to_a.each do |assoc|
78
+ instance_variable_set "@#{assoc.name}", nil
79
+ end unless self.new_record?
80
+ end
81
+
13
82
  # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
14
83
  # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
15
- # specialized according to the collection or association symbol and the options hash. It works much the same was as Ruby's own attr*
84
+ # specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
16
85
  # methods. Example:
17
86
  #
18
87
  # class Project < ActiveRecord::Base
@@ -22,48 +91,336 @@ module ActiveRecord
22
91
  # has_and_belongs_to_many :categories
23
92
  # end
24
93
  #
25
- # The project class now has the following methods to ease the traversel and manipulation of its relationships:
26
- # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
27
- # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manger.nil?,</tt>
28
- # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
94
+ # The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
95
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
96
+ # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
29
97
  # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
30
- # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
98
+ # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find(:all, options),</tt>
31
99
  # <tt>Project#milestones.build, Project#milestones.create</tt>
32
100
  # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
33
101
  # <tt>Project#categories.delete(category1)</tt>
34
102
  #
35
- # == Example
103
+ # === A word of warning
36
104
  #
37
- # link:../examples/associations.png
105
+ # Don't create associations that have the same name as instance methods of ActiveRecord::Base. Since the association
106
+ # adds a method with that name to its model, it will override the inherited method and break things.
107
+ # For instance, #attributes and #connection would be bad choices for association names.
38
108
  #
39
- # == Is it belongs_to or has_one?
109
+ # == Auto-generated methods
40
110
  #
41
- # Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
42
- # saying belongs_to. Example:
111
+ # ===Singular associations (one-to-one)
112
+ # | | belongs_to |
113
+ # generated methods | belongs_to | :polymorphic | has_one
114
+ # ----------------------------------+------------+--------------+---------
115
+ # #other | X | X | X
116
+ # #other=(other) | X | X | X
117
+ # #build_other(attributes={}) | X | | X
118
+ # #create_other(attributes={}) | X | | X
119
+ # #other.create!(attributes={}) | | | X
120
+ # #other.nil? | X | X |
43
121
  #
44
- # class Post < ActiveRecord::Base
45
- # has_one :author
122
+ # ===Collection associations (one-to-many / many-to-many)
123
+ # | | | has_many
124
+ # generated methods | habtm | has_many | :through
125
+ # ----------------------------------+-------+----------+----------
126
+ # #others | X | X | X
127
+ # #others=(other,other,...) | X | X |
128
+ # #other_ids | X | X | X
129
+ # #other_ids=(id,id,...) | X | X |
130
+ # #others<< | X | X | X
131
+ # #others.push | X | X | X
132
+ # #others.concat | X | X | X
133
+ # #others.build(attributes={}) | X | X | X
134
+ # #others.create(attributes={}) | X | X |
135
+ # #others.create!(attributes={}) | X | X | X
136
+ # #others.size | X | X | X
137
+ # #others.length | X | X | X
138
+ # #others.count | | X | X
139
+ # #others.sum(args*,&block) | X | X | X
140
+ # #others.empty? | X | X | X
141
+ # #others.clear | X | X |
142
+ # #others.delete(other,other,...) | X | X | X
143
+ # #others.delete_all | X | X |
144
+ # #others.destroy_all | X | X | X
145
+ # #others.find(*args) | X | X | X
146
+ # #others.find_first | X | |
147
+ # #others.uniq | X | X |
148
+ # #others.reset | X | X | X
149
+ #
150
+ # == Cardinality and associations
151
+ #
152
+ # ActiveRecord associations can be used to describe relations with one-to-one, one-to-many
153
+ # and many-to-many cardinality. Each model uses an association to describe its role in
154
+ # the relation. In each case, the +belongs_to+ association is used in the model that has
155
+ # the foreign key.
156
+ #
157
+ # === One-to-one
158
+ #
159
+ # Use +has_one+ in the base, and +belongs_to+ in the associated model.
160
+ #
161
+ # class Employee < ActiveRecord::Base
162
+ # has_one :office
163
+ # end
164
+ # class Office < ActiveRecord::Base
165
+ # belongs_to :employee # foreign key - employee_id
46
166
  # end
47
167
  #
48
- # class Author < ActiveRecord::Base
49
- # belongs_to :post
168
+ # === One-to-many
169
+ #
170
+ # Use +has_many+ in the base, and +belongs_to+ in the associated model.
171
+ #
172
+ # class Manager < ActiveRecord::Base
173
+ # has_many :employees
174
+ # end
175
+ # class Employee < ActiveRecord::Base
176
+ # belongs_to :manager # foreign key - manager_id
177
+ # end
178
+ #
179
+ # === Many-to-many
180
+ #
181
+ # There are two ways to build a many-to-many relationship.
182
+ #
183
+ # The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so
184
+ # there are two stages of associations.
185
+ #
186
+ # class Assignment < ActiveRecord::Base
187
+ # belongs_to :programmer # foreign key - programmer_id
188
+ # belongs_to :project # foreign key - project_id
189
+ # end
190
+ # class Programmer < ActiveRecord::Base
191
+ # has_many :assignments
192
+ # has_many :projects, :through => :assignments
193
+ # end
194
+ # class Project < ActiveRecord::Base
195
+ # has_many :assignments
196
+ # has_many :programmers, :through => :assignments
197
+ # end
198
+ #
199
+ # For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
200
+ # that has no corresponding model or primary key.
201
+ #
202
+ # class Programmer < ActiveRecord::Base
203
+ # has_and_belongs_to_many :projects # foreign keys in the join table
204
+ # end
205
+ # class Project < ActiveRecord::Base
206
+ # has_and_belongs_to_many :programmers # foreign keys in the join table
207
+ # end
208
+ #
209
+ # Choosing which way to build a many-to-many relationship is not always simple.
210
+ # If you need to work with the relationship model as its own entity,
211
+ # use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when
212
+ # you never work directly with the relationship itself.
213
+ #
214
+ # == Is it a +belongs_to+ or +has_one+ association?
215
+ #
216
+ # Both express a 1-1 relationship. The difference is mostly where to place the foreign key, which goes on the table for the class
217
+ # declaring the +belongs_to+ relationship. Example:
218
+ #
219
+ # class User < ActiveRecord::Base
220
+ # # I reference an account.
221
+ # belongs_to :account
222
+ # end
223
+ #
224
+ # class Account < ActiveRecord::Base
225
+ # # One user references me.
226
+ # has_one :user
50
227
  # end
51
228
  #
52
229
  # The tables for these classes could look something like:
53
230
  #
54
- # CREATE TABLE posts (
231
+ # CREATE TABLE users (
55
232
  # id int(11) NOT NULL auto_increment,
56
- # title varchar default NULL,
233
+ # account_id int(11) default NULL,
234
+ # name varchar default NULL,
57
235
  # PRIMARY KEY (id)
58
236
  # )
59
237
  #
60
- # CREATE TABLE authors (
238
+ # CREATE TABLE accounts (
61
239
  # id int(11) NOT NULL auto_increment,
62
- # post_id int(11) default NULL,
63
240
  # name varchar default NULL,
64
241
  # PRIMARY KEY (id)
65
242
  # )
66
243
  #
244
+ # == Unsaved objects and associations
245
+ #
246
+ # You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
247
+ # aware of, mostly involving the saving of associated objects.
248
+ #
249
+ # === One-to-one associations
250
+ #
251
+ # * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in
252
+ # order to update their primary keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
253
+ # * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns +false+ and the assignment
254
+ # is cancelled.
255
+ # * If you wish to assign an object to a +has_one+ association without saving it, use the <tt>#association.build</tt> method (documented below).
256
+ # * Assigning an object to a +belongs_to+ association does not save the object, since the foreign key field belongs on the parent. It
257
+ # does not save the parent either.
258
+ #
259
+ # === Collections
260
+ #
261
+ # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically saves that object, except if the parent object
262
+ # (the owner of the collection) is not yet stored in the database.
263
+ # * If saving any of the objects being added to a collection (via <tt>#push</tt> or similar) fails, then <tt>#push</tt> returns +false+.
264
+ # * You can add an object to a collection without automatically saving it by using the <tt>#collection.build</tt> method (documented below).
265
+ # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically saved when the parent is saved.
266
+ #
267
+ # === Association callbacks
268
+ #
269
+ # Similar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get
270
+ # triggered when you add an object to or remove an object from an association collection. Example:
271
+ #
272
+ # class Project
273
+ # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
274
+ #
275
+ # def evaluate_velocity(developer)
276
+ # ...
277
+ # end
278
+ # end
279
+ #
280
+ # It's possible to stack callbacks by passing them as an array. Example:
281
+ #
282
+ # class Project
283
+ # has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
284
+ # end
285
+ #
286
+ # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
287
+ #
288
+ # Should any of the +before_add+ callbacks throw an exception, the object does not get added to the collection. Same with
289
+ # the +before_remove+ callbacks; if an exception is thrown the object doesn't get removed.
290
+ #
291
+ # === Association extensions
292
+ #
293
+ # The proxy objects that control the access to associations can be extended through anonymous modules. This is especially
294
+ # beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association.
295
+ # Example:
296
+ #
297
+ # class Account < ActiveRecord::Base
298
+ # has_many :people do
299
+ # def find_or_create_by_name(name)
300
+ # first_name, last_name = name.split(" ", 2)
301
+ # find_or_create_by_first_name_and_last_name(first_name, last_name)
302
+ # end
303
+ # end
304
+ # end
305
+ #
306
+ # person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
307
+ # person.first_name # => "David"
308
+ # person.last_name # => "Heinemeier Hansson"
309
+ #
310
+ # If you need to share the same extensions between many associations, you can use a named extension module. Example:
311
+ #
312
+ # module FindOrCreateByNameExtension
313
+ # def find_or_create_by_name(name)
314
+ # first_name, last_name = name.split(" ", 2)
315
+ # find_or_create_by_first_name_and_last_name(first_name, last_name)
316
+ # end
317
+ # end
318
+ #
319
+ # class Account < ActiveRecord::Base
320
+ # has_many :people, :extend => FindOrCreateByNameExtension
321
+ # end
322
+ #
323
+ # class Company < ActiveRecord::Base
324
+ # has_many :people, :extend => FindOrCreateByNameExtension
325
+ # end
326
+ #
327
+ # If you need to use multiple named extension modules, you can specify an array of modules with the <tt>:extend</tt> option.
328
+ # In the case of name conflicts between methods in the modules, methods in modules later in the array supercede
329
+ # those earlier in the array. Example:
330
+ #
331
+ # class Account < ActiveRecord::Base
332
+ # has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
333
+ # end
334
+ #
335
+ # Some extensions can only be made to work with knowledge of the association proxy's internals.
336
+ # Extensions can access relevant state using accessors on the association proxy:
337
+ #
338
+ # * +proxy_owner+ - Returns the object the association is part of.
339
+ # * +proxy_reflection+ - Returns the reflection object that describes the association.
340
+ # * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
341
+ #
342
+ # === Association Join Models
343
+ #
344
+ # Has Many associations can be configured with the <tt>:through</tt> option to use an explicit join model to retrieve the data. This
345
+ # operates similarly to a +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
346
+ # callbacks, and extra attributes on the join model. Consider the following schema:
347
+ #
348
+ # class Author < ActiveRecord::Base
349
+ # has_many :authorships
350
+ # has_many :books, :through => :authorships
351
+ # end
352
+ #
353
+ # class Authorship < ActiveRecord::Base
354
+ # belongs_to :author
355
+ # belongs_to :book
356
+ # end
357
+ #
358
+ # @author = Author.find :first
359
+ # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to.
360
+ # @author.books # selects all books by using the Authorship join model
361
+ #
362
+ # You can also go through a +has_many+ association on the join model:
363
+ #
364
+ # class Firm < ActiveRecord::Base
365
+ # has_many :clients
366
+ # has_many :invoices, :through => :clients
367
+ # end
368
+ #
369
+ # class Client < ActiveRecord::Base
370
+ # belongs_to :firm
371
+ # has_many :invoices
372
+ # end
373
+ #
374
+ # class Invoice < ActiveRecord::Base
375
+ # belongs_to :client
376
+ # end
377
+ #
378
+ # @firm = Firm.find :first
379
+ # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
380
+ # @firm.invoices # selects all invoices by going through the Client join model.
381
+ #
382
+ # === Polymorphic Associations
383
+ #
384
+ # Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they
385
+ # specify an interface that a +has_many+ association must adhere to.
386
+ #
387
+ # class Asset < ActiveRecord::Base
388
+ # belongs_to :attachable, :polymorphic => true
389
+ # end
390
+ #
391
+ # class Post < ActiveRecord::Base
392
+ # has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use.
393
+ # end
394
+ #
395
+ # @asset.attachable = @post
396
+ #
397
+ # This works by using a type column in addition to a foreign key to specify the associated record. In the Asset example, you'd need
398
+ # an +attachable_id+ integer column and an +attachable_type+ string column.
399
+ #
400
+ # Using polymorphic associations in combination with single table inheritance (STI) is a little tricky. In order
401
+ # for the associations to work as expected, ensure that you store the base model for the STI models in the
402
+ # type column of the polymorphic association. To continue with the asset example above, suppose there are guest posts
403
+ # and member posts that use the posts table for STI. In this case, there must be a +type+ column in the posts table.
404
+ #
405
+ # class Asset < ActiveRecord::Base
406
+ # belongs_to :attachable, :polymorphic => true
407
+ #
408
+ # def attachable_type=(sType)
409
+ # super(sType.to_s.classify.constantize.base_class.to_s)
410
+ # end
411
+ # end
412
+ #
413
+ # class Post < ActiveRecord::Base
414
+ # # because we store "Post" in attachable_type now :dependent => :destroy will work
415
+ # has_many :assets, :as => :attachable, :dependent => :destroy
416
+ # end
417
+ #
418
+ # class GuestPost < Post
419
+ # end
420
+ #
421
+ # class MemberPost < Post
422
+ # end
423
+ #
67
424
  # == Caching
68
425
  #
69
426
  # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
@@ -76,6 +433,108 @@ module ActiveRecord
76
433
  # project.milestones(true).size # fetches milestones from the database
77
434
  # project.milestones # uses the milestone cache
78
435
  #
436
+ # == Eager loading of associations
437
+ #
438
+ # Eager loading is a way to find objects of a certain class and a number of named associations along with it in a single SQL call. This is
439
+ # one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each need to display their author
440
+ # triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 1. Example:
441
+ #
442
+ # class Post < ActiveRecord::Base
443
+ # belongs_to :author
444
+ # has_many :comments
445
+ # end
446
+ #
447
+ # Consider the following loop using the class above:
448
+ #
449
+ # for post in Post.find(:all)
450
+ # puts "Post: " + post.title
451
+ # puts "Written by: " + post.author.name
452
+ # puts "Last comment on: " + post.comments.first.created_on
453
+ # end
454
+ #
455
+ # To iterate over these one hundred posts, we'll generate 201 database queries. Let's first just optimize it for retrieving the author:
456
+ #
457
+ # for post in Post.find(:all, :include => :author)
458
+ #
459
+ # This references the name of the +belongs_to+ association that also used the <tt>:author</tt> symbol, so the find will now weave in a join something
460
+ # like this: <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Doing so will cut down the number of queries from 201 to 101.
461
+ #
462
+ # We can improve upon the situation further by referencing both associations in the finder with:
463
+ #
464
+ # for post in Post.find(:all, :include => [ :author, :comments ])
465
+ #
466
+ # That'll add another join along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt>. And we'll be down to 1 query.
467
+ #
468
+ # To include a deep hierarchy of associations, use a hash:
469
+ #
470
+ # for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
471
+ #
472
+ # That'll grab not only all the comments but all their authors and gravatar pictures. You can mix and match
473
+ # symbols, arrays and hashes in any combination to describe the associations you want to load.
474
+ #
475
+ # All of this power shouldn't fool you into thinking that you can pull out huge amounts of data with no performance penalty just because you've reduced
476
+ # the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
477
+ # catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
478
+ #
479
+ # Since the eager loading pulls from multiple tables, you'll have to disambiguate any column references in both conditions and orders. So
480
+ # <tt>:order => "posts.id DESC"</tt> will work while <tt>:order => "id DESC"</tt> will not. Because eager loading generates the +SELECT+ statement too, the
481
+ # <tt>:select</tt> option is ignored.
482
+ #
483
+ # You can use eager loading on multiple associations from the same table, but you cannot use those associations in orders and conditions
484
+ # as there is currently not any way to disambiguate them. Eager loading will not pull additional attributes on join tables, so "rich
485
+ # associations" with +has_and_belongs_to_many+ are not a good fit for eager loading.
486
+ #
487
+ # When eager loaded, conditions are interpolated in the context of the model class, not the model instance. Conditions are lazily interpolated
488
+ # before the actual model exists.
489
+ #
490
+ # == Table Aliasing
491
+ #
492
+ # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once,
493
+ # the standard table name is used. The second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. Indexes are appended
494
+ # for any more successive uses of the table name.
495
+ #
496
+ # Post.find :all, :include => :comments
497
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ...
498
+ # Post.find :all, :include => :special_comments # STI
499
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... AND comments.type = 'SpecialComment'
500
+ # Post.find :all, :include => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name
501
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... LEFT OUTER JOIN comments special_comments_posts
502
+ #
503
+ # Acts as tree example:
504
+ #
505
+ # TreeMixin.find :all, :include => :children
506
+ # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
507
+ # TreeMixin.find :all, :include => {:children => :parent} # using cascading eager includes
508
+ # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
509
+ # LEFT OUTER JOIN parents_mixins ...
510
+ # TreeMixin.find :all, :include => {:children => {:parent => :children}}
511
+ # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
512
+ # LEFT OUTER JOIN parents_mixins ...
513
+ # LEFT OUTER JOIN mixins childrens_mixins_2
514
+ #
515
+ # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
516
+ #
517
+ # Post.find :all, :include => :categories
518
+ # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
519
+ # Post.find :all, :include => {:categories => :posts}
520
+ # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
521
+ # LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories
522
+ # Post.find :all, :include => {:categories => {:posts => :categories}}
523
+ # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
524
+ # LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories
525
+ # LEFT OUTER JOIN categories_posts categories_posts_join LEFT OUTER JOIN categories categories_posts
526
+ #
527
+ # If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table names will take precedence over the eager associations:
528
+ #
529
+ # Post.find :all, :include => :comments, :joins => "inner join comments ..."
530
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments_posts ON ... INNER JOIN comments ...
531
+ # Post.find :all, :include => [:comments, :special_comments], :joins => "inner join comments ..."
532
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments comments_posts ON ...
533
+ # LEFT OUTER JOIN comments special_comments_posts ...
534
+ # INNER JOIN comments ...
535
+ #
536
+ # Table aliases are automatically truncated according to the maximum length of table identifiers according to the specific database.
537
+ #
79
538
  # == Modules
80
539
  #
81
540
  # By default, associations will look for objects within the current module scope. Consider:
@@ -90,8 +549,8 @@ module ActiveRecord
90
549
  # end
91
550
  # end
92
551
  #
93
- # When Firm#clients is called, it'll in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
94
- # with a class in another module scope this can be done by specifying the complete class name, such as:
552
+ # When <tt>Firm#clients</tt> is called, it will in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
553
+ # with a class in another module scope, this can be done by specifying the complete class name. Example:
95
554
  #
96
555
  # module MyApplication
97
556
  # module Business
@@ -105,325 +564,414 @@ module ActiveRecord
105
564
  # end
106
565
  # end
107
566
  #
108
- # == Type safety with ActiveRecord::AssociationTypeMismatch
567
+ # == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
109
568
  #
110
569
  # If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
111
- # get a ActiveRecord::AssociationTypeMismatch.
570
+ # get an <tt>ActiveRecord::AssociationTypeMismatch</tt>.
112
571
  #
113
572
  # == Options
114
573
  #
115
- # All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
574
+ # All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones
116
575
  # possible.
117
576
  module ClassMethods
118
- # Adds the following methods for retrival and query of collections of associated objects.
577
+ # Adds the following methods for retrieval and query of collections of associated objects:
119
578
  # +collection+ is replaced with the symbol passed as the first argument, so
120
- # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
579
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
121
580
  # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
122
- # An empty array is returned if none is found.
123
- # * <tt>collection<<(object)</tt> - adds the object to the collection (by setting the foreign key on it) and saves it.
124
- # * <tt>collection.delete(object)</tt> - removes the association by setting the foreign key to null on the associated object.
125
- # * <tt>!collection.empty?</tt> - returns true if there's any associated objects.
581
+ # An empty array is returned if none are found.
582
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
583
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL.
584
+ # This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
585
+ # * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
586
+ # * <tt>collection_singular_ids</tt> - returns an array of the associated objects' ids
587
+ # * <tt>collection_singular_ids=ids</tt> - replace the collection with the objects identified by the primary keys in +ids+
588
+ # * <tt>collection.clear</tt> - removes every object from the collection. This destroys the associated objects if they
589
+ # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
590
+ # otherwise sets their foreign keys to NULL.
591
+ # * <tt>collection.empty?</tt> - returns +true+ if there are no associated objects.
126
592
  # * <tt>collection.size</tt> - returns the number of associated objects.
127
- # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
128
- # meets the condition that it has to be associated with this object.
129
- # * <tt>collection.find_all(conditions = nil, orderings = nil, limit = nil, joins = nil)</tt> - finds all associated objects responding
130
- # criterias mentioned (like in the standard find_all) and that meets the condition that it has to be associated with this object.
131
- # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
132
- # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
593
+ # * <tt>collection.find</tt> - finds an associated object according to the same rules as Base.find.
594
+ # * <tt>collection.build(attributes = {}, ...)</tt> - returns one or more new objects of the collection type that have been instantiated
595
+ # with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
596
+ # associated object already exists, not if it's +nil+!
133
597
  # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
134
- # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
598
+ # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
599
+ # *Note:* This only works if an associated object already exists, not if it's +nil+!
135
600
  #
136
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
137
- # * <tt>Firm#clients</tt> (similar to <tt>Clients.find_all "firm_id = #{id}"</tt>)
601
+ # Example: A +Firm+ class declares <tt>has_many :clients</tt>, which will add:
602
+ # * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
138
603
  # * <tt>Firm#clients<<</tt>
139
604
  # * <tt>Firm#clients.delete</tt>
140
- # * <tt>!Firm#clients.empty?</tt> (similar to <tt>firm.clients.length > 0</tt>)
605
+ # * <tt>Firm#clients=</tt>
606
+ # * <tt>Firm#client_ids</tt>
607
+ # * <tt>Firm#client_ids=</tt>
608
+ # * <tt>Firm#clients.clear</tt>
609
+ # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
141
610
  # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
142
- # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find_on_conditions(id, "firm_id = #{id}")</tt>)
143
- # * <tt>Firm#clients.find_all</tt> (similar to <tt>Client.find_all "firm_id = #{id}"</tt>)
611
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
144
612
  # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
145
- # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
146
- # The declaration can also include an options hash to specialize the generated methods.
613
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
614
+ # The declaration can also include an options hash to specialize the behavior of the association.
147
615
  #
148
616
  # Options are:
149
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
617
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
150
618
  # from the association name. So <tt>has_many :products</tt> will by default be linked to the +Product+ class, but
151
619
  # if the real class name is +SpecialProduct+, you'll have to specify it with this option.
152
- # * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
153
- # sql fragment, such as "price > 5 AND name LIKE 'B%'".
154
- # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
155
- # such as "last_name, first_name DESC"
620
+ # * <tt>:conditions</tt> - specify the conditions that the associated objects must meet in order to be included as a +WHERE+
621
+ # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>.
622
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
623
+ # such as <tt>last_name, first_name DESC</tt>
156
624
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
157
- # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
158
- # as the default foreign_key.
159
- # * <tt>:dependent</tt> - if set to true all the associated object are destroyed alongside this object
160
- # * <tt>:exclusively_dependent</tt> - if set to true all the associated object are deleted in one SQL statement without having their
161
- # before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
162
- # clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
625
+ # of this class in lower-case and +_id+ suffixed. So a +Person+ class that makes a +has_many+ association will use +person_id+
626
+ # as the default +foreign_key+.
627
+ # * <tt>:dependent</tt> - if set to <tt>:destroy</tt> all the associated objects are destroyed
628
+ # alongside this object by calling their destroy method. If set to <tt>:delete_all</tt> all associated
629
+ # objects are deleted *without* calling their destroy method. If set to <tt>:nullify</tt> all associated
630
+ # objects' foreign keys are set to +NULL+ *without* calling their save callbacks.
163
631
  # * <tt>:finder_sql</tt> - specify a complete SQL statement to fetch the association. This is a good way to go for complex
164
- # associations that depends on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
632
+ # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
633
+ # * <tt>:counter_sql</tt> - specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
634
+ # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
635
+ # * <tt>:extend</tt> - specify a named module for extending the proxy. See "Association extensions".
636
+ # * <tt>:include</tt> - specify second-order associations that should be eager loaded when the collection is loaded.
637
+ # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
638
+ # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned.
639
+ # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
640
+ # * <tt>:select</tt>: By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
641
+ # but not include the joined columns.
642
+ # * <tt>:as</tt>: Specifies a polymorphic interface (See <tt>#belongs_to</tt>).
643
+ # * <tt>:through</tt>: Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
644
+ # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
645
+ # or <tt>has_many</tt> association on the join model.
646
+ # * <tt>:source</tt>: Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
647
+ # inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
648
+ # <tt>:subscriber</tt> on +Subscription+, unless a <tt>:source</tt> is given.
649
+ # * <tt>:source_type</tt>: Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
650
+ # association is a polymorphic +belongs_to+.
651
+ # * <tt>:uniq</tt> - if set to +true+, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
165
652
  #
166
653
  # Option examples:
167
654
  # has_many :comments, :order => "posted_on"
655
+ # has_many :comments, :include => :author
168
656
  # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
169
- # has_many :tracks, :order => "position", :dependent => true
657
+ # has_many :tracks, :order => "position", :dependent => :destroy
658
+ # has_many :comments, :dependent => :nullify
659
+ # has_many :tags, :as => :taggable
660
+ # has_many :subscribers, :through => :subscriptions, :source => :user
170
661
  # has_many :subscribers, :class_name => "Person", :finder_sql =>
171
662
  # 'SELECT DISTINCT people.* ' +
172
663
  # 'FROM people p, post_subscriptions ps ' +
173
664
  # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
174
665
  # 'ORDER BY p.first_name'
175
- def has_many(association_id, options = {})
176
- validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
177
- association_name, association_class_name, association_class_primary_key_name =
178
- associate_identification(association_id, options[:class_name], options[:foreign_key])
179
-
180
- if options[:dependent]
181
- module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
182
- end
666
+ def has_many(association_id, options = {}, &extension)
667
+ reflection = create_has_many_reflection(association_id, options, &extension)
183
668
 
184
- if options[:exclusively_dependent]
185
- module_eval "before_destroy Proc.new{ |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = '\#{record.id}')) }"
186
- end
669
+ configure_dependency_for_has_many(reflection)
187
670
 
188
- module_eval <<-"end_eval", __FILE__, __LINE__
189
- def #{association_name}(force_reload = false)
190
- if @#{association_name}.nil?
191
- @#{association_name} = HasManyAssociation.new(self, "#{association_name}", "#{association_class_name}",
192
- "#{association_class_primary_key_name}", #{options.inspect})
193
- end
194
- @#{association_name}.reload if force_reload
195
-
196
- return @#{association_name}
197
- end
198
- end_eval
199
-
200
- # deprecated api
201
- deprecated_collection_count_method(association_name)
202
- deprecated_add_association_relation(association_name)
203
- deprecated_remove_association_relation(association_name)
204
- deprecated_has_collection_method(association_name)
205
- deprecated_find_in_collection_method(association_name)
206
- deprecated_find_all_in_collection_method(association_name)
207
- deprecated_create_method(association_name)
208
- deprecated_build_method(association_name)
671
+ if options[:through]
672
+ collection_reader_method(reflection, HasManyThroughAssociation)
673
+ collection_accessor_methods(reflection, HasManyThroughAssociation, false)
674
+ else
675
+ add_multiple_associated_save_callbacks(reflection.name)
676
+ add_association_callbacks(reflection.name, reflection.options)
677
+ collection_accessor_methods(reflection, HasManyAssociation)
678
+ end
209
679
  end
210
680
 
211
- # Adds the following methods for retrival and query of a single associated object.
681
+ # Adds the following methods for retrieval and query of a single associated object:
212
682
  # +association+ is replaced with the symbol passed as the first argument, so
213
- # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
214
- # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
683
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
684
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. +nil+ is returned if none is found.
215
685
  # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
216
686
  # and saves the associate object.
217
- # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
218
- # same id as the associated object.
219
- # * <tt>!association.nil?</tt> - returns true if there's an associated object.
687
+ # * <tt>association.nil?</tt> - returns +true+ if there is no associated object.
220
688
  # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
221
- # with +attributes+ and linked to this object through a foreign key but has not yet been saved.
689
+ # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. Note: This ONLY works if
690
+ # an association already exists. It will NOT work if the association is +nil+.
222
691
  # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
223
- # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
692
+ # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
224
693
  #
225
694
  # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
226
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
695
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
227
696
  # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
228
- # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
229
- # * <tt>!Account#beneficiary.nil?</tt>
697
+ # * <tt>Account#beneficiary.nil?</tt>
230
698
  # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
231
699
  # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
232
- # The declaration can also include an options hash to specialize the generated methods.
700
+ #
701
+ # The declaration can also include an options hash to specialize the behavior of the association.
233
702
  #
234
703
  # Options are:
235
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
704
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
236
705
  # from the association name. So <tt>has_one :manager</tt> will by default be linked to the +Manager+ class, but
237
706
  # if the real class name is +Person+, you'll have to specify it with this option.
238
- # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
239
- # sql fragment, such as "rank = 5".
240
- # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
241
- # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
242
- # * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
707
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a +WHERE+
708
+ # SQL fragment, such as <tt>rank = 5</tt>.
709
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
710
+ # such as <tt>last_name, first_name DESC</tt>
711
+ # * <tt>:dependent</tt> - if set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
712
+ # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated
713
+ # object's foreign key is set to +NULL+. Also, association is assigned.
243
714
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
244
- # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
245
- # as the default foreign_key.
246
- #
715
+ # of this class in lower-case and +_id+ suffixed. So a +Person+ class that makes a +has_one+ association will use +person_id+
716
+ # as the default +foreign_key+.
717
+ # * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded.
718
+ # * <tt>:as</tt>: Specifies a polymorphic interface (See <tt>#belongs_to</tt>).
719
+ #
247
720
  # Option examples:
248
- # has_one :credit_card, :dependent => true
721
+ # has_one :credit_card, :dependent => :destroy # destroys the associated credit card
722
+ # has_one :credit_card, :dependent => :nullify # updates the associated records foreign key value to NULL rather than destroying it
249
723
  # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
250
724
  # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
725
+ # has_one :attachment, :as => :attachable
251
726
  def has_one(association_id, options = {})
252
- options.merge!({ :remote => true })
253
- belongs_to(association_id, options)
254
-
255
- association_name, association_class_name, class_primary_key_name =
256
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
727
+ reflection = create_has_one_reflection(association_id, options)
257
728
 
258
- has_one_writer_method(association_name, association_class_name, class_primary_key_name)
259
- build_method("build_", association_name, association_class_name, class_primary_key_name)
260
- create_method("create_", association_name, association_class_name, class_primary_key_name)
729
+ module_eval do
730
+ after_save <<-EOF
731
+ association = instance_variable_get("@#{reflection.name}")
732
+ if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
733
+ association["#{reflection.primary_key_name}"] = id
734
+ association.save(true)
735
+ end
736
+ EOF
737
+ end
738
+
739
+ association_accessor_methods(reflection, HasOneAssociation)
740
+ association_constructor_method(:build, reflection, HasOneAssociation)
741
+ association_constructor_method(:create, reflection, HasOneAssociation)
261
742
 
262
- module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
743
+ configure_dependency_for_has_one(reflection)
263
744
  end
264
745
 
265
- # Adds the following methods for retrival and query for a single associated object that this object holds an id to.
746
+ # Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
266
747
  # +association+ is replaced with the symbol passed as the first argument, so
267
- # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
268
- # * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
748
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
749
+ # * <tt>association(force_reload = false)</tt> - returns the associated object. +nil+ is returned if none is found.
269
750
  # * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
270
- # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
271
- # same id as the associated object.
272
- # * <tt>association.nil?</tt> - returns true if there's an associated object.
751
+ # * <tt>association.nil?</tt> - returns +true+ if there is no associated object.
752
+ # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
753
+ # with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
754
+ # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
755
+ # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
273
756
  #
274
- # Example: An Post class declares <tt>has_one :author</tt>, which will add:
757
+ # Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
275
758
  # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
276
759
  # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
277
760
  # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
278
- # * <tt>!Post#author.nil?</tt>
279
- # The declaration can also include an options hash to specialize the generated methods.
761
+ # * <tt>Post#author.nil?</tt>
762
+ # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
763
+ # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
764
+ # The declaration can also include an options hash to specialize the behavior of the association.
280
765
  #
281
766
  # Options are:
282
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
767
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
283
768
  # from the association name. So <tt>has_one :author</tt> will by default be linked to the +Author+ class, but
284
769
  # if the real class name is +Person+, you'll have to specify it with this option.
285
- # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a "WHERE"
286
- # sql fragment, such as "authorized = 1".
287
- # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
288
- # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
770
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a +WHERE+
771
+ # SQL fragment, such as <tt>authorized = 1</tt>.
772
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
773
+ # such as <tt>last_name, first_name DESC</tt>
289
774
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
290
- # of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
291
- # +Boss+ class will use "boss_id" as the default foreign_key.
292
- # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
293
- # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
294
- # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
295
- # is used on the associate class (such as a Post class).
775
+ # of the associated class in lower-case and +_id+ suffixed. So a +Person+ class that makes a +belongs_to+ association to a
776
+ # +Boss+ class will use +boss_id+ as the default +foreign_key+.
777
+ # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through the use of +increment_counter+
778
+ # and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
779
+ # destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging +Comment+ class)
780
+ # is used on the associate class (such as a +Post+ class). You can also specify a custom counter cache column by providing
781
+ # a column name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
782
+ # Note: Specifying a counter_cache will add it to that model's list of readonly attributes using #attr_readonly.
783
+ # * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded.
784
+ # * <tt>:polymorphic</tt> - specify this association is a polymorphic association by passing +true+.
785
+ # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
786
+ # to the attr_readonly list in the associated classes (e.g. class Post; attr_readonly :comments_count; end).
296
787
  #
297
788
  # Option examples:
298
789
  # belongs_to :firm, :foreign_key => "client_of"
299
790
  # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
300
791
  # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
301
792
  # :conditions => 'discounts > #{payments_count}'
302
- def belongs_to(association_id, options = {})
303
- validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
304
-
305
- association_name, association_class_name, class_primary_key_name =
306
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
793
+ # belongs_to :attachable, :polymorphic => true
794
+ def belongs_to(association_id, options = {})
795
+ reflection = create_belongs_to_reflection(association_id, options)
796
+
797
+ if reflection.options[:polymorphic]
798
+ association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
307
799
 
308
- association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
800
+ module_eval do
801
+ before_save <<-EOF
802
+ association = instance_variable_get("@#{reflection.name}")
803
+ if association && association.target
804
+ if association.new_record?
805
+ association.save(true)
806
+ end
807
+
808
+ if association.updated?
809
+ self["#{reflection.primary_key_name}"] = association.id
810
+ self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
811
+ end
812
+ end
813
+ EOF
814
+ end
815
+ else
816
+ association_accessor_methods(reflection, BelongsToAssociation)
817
+ association_constructor_method(:build, reflection, BelongsToAssociation)
818
+ association_constructor_method(:create, reflection, BelongsToAssociation)
309
819
 
310
- if options[:remote]
311
- association_finder = <<-"end_eval"
312
- #{association_class_name}.find_first(
313
- "#{class_primary_key_name} = '\#{id}'#{options[:conditions] ? " AND " + options[:conditions] : ""}",
314
- #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
315
- )
316
- end_eval
317
- else
318
- association_finder = options[:conditions] ?
319
- "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
320
- "#{association_class_name}.find(#{association_class_primary_key_name})"
820
+ module_eval do
821
+ before_save <<-EOF
822
+ association = instance_variable_get("@#{reflection.name}")
823
+ if !association.nil?
824
+ if association.new_record?
825
+ association.save(true)
826
+ end
827
+
828
+ if association.updated?
829
+ self["#{reflection.primary_key_name}"] = association.id
830
+ end
831
+ end
832
+ EOF
321
833
  end
834
+ end
322
835
 
323
- has_association_method(association_name)
324
- association_reader_method(association_name, association_finder)
325
- belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
326
- association_comparison_method(association_name, association_class_name)
836
+ # Create the callbacks to update counter cache
837
+ if options[:counter_cache]
838
+ cache_column = options[:counter_cache] == true ?
839
+ "#{self.to_s.underscore.pluralize}_count" :
840
+ options[:counter_cache]
327
841
 
328
- if options[:counter_cache]
329
- module_eval(
330
- "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
331
- " if has_#{association_name}?'"
332
- )
842
+ module_eval(
843
+ "after_create '#{reflection.name}.class.increment_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
844
+ " unless #{reflection.name}.nil?'"
845
+ )
333
846
 
334
- module_eval(
335
- "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
336
- " if has_#{association_name}?'"
337
- )
338
- end
847
+ module_eval(
848
+ "before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
849
+ " unless #{reflection.name}.nil?'"
850
+ )
851
+
852
+ module_eval(
853
+ "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
854
+ )
339
855
  end
856
+ end
340
857
 
341
858
  # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
342
- # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
343
- # will give the default join table name of "developers_projects" because "D" outranks "P".
344
- # Adds the following methods for retrival and query.
859
+ # an option, it is guessed using the lexical order of the class names. So a join between +Developer+ and +Project+
860
+ # will give the default join table name of +developers_projects+ because "D" outranks "P". Note that this precedence
861
+ # is calculated using the <tt><</tt> operator for <tt>String</tt>. This means that if the strings are of different lengths,
862
+ # and the strings are equal when compared up to the shortest length, then the longer string is considered of higher
863
+ # lexical precedence than the shorter one. For example, one would expect the tables <tt>paper_boxes</tt> and <tt>papers</tt>
864
+ # to generate a join table name of <tt>papers_paper_boxes</tt> because of the length of the name <tt>paper_boxes</tt>,
865
+ # but it in fact generates a join table name of <tt>paper_boxes_papers</tt>. Be aware of this caveat, and use the
866
+ # custom <tt>join_table</tt> option if you need to.
867
+ #
868
+ # Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through
869
+ # +has_and_belongs_to_many+ associations. Records returned from join tables with additional attributes will be marked as
870
+ # +ReadOnly+ (because we can't save changes to the additional attributes). It's strongly recommended that you upgrade any
871
+ # associations with attributes to a real join model (see introduction).
872
+ #
873
+ # Adds the following methods for retrieval and query:
345
874
  # +collection+ is replaced with the symbol passed as the first argument, so
346
- # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
875
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
347
876
  # * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
348
- # An empty array is returned if none is found.
349
- # * <tt>!collection.empty?</tt> - returns true if there's any associated objects.
877
+ # An empty array is returned if none are found.
878
+ # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
879
+ # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
880
+ # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
881
+ # This does not destroy the objects.
882
+ # * <tt>collection=objects</tt> - replaces the collection's content by deleting and adding objects as appropriate.
883
+ # * <tt>collection_singular_ids</tt> - returns an array of the associated objects' ids
884
+ # * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
885
+ # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
886
+ # * <tt>collection.empty?</tt> - returns +true+ if there are no associated objects.
350
887
  # * <tt>collection.size</tt> - returns the number of associated objects.
351
- # * <tt>collection<<(object)</tt> - adds an association between this object and the object given as argument. Multiple associations
352
- # can be created by passing an array of objects instead.
353
- # * <tt>collection.delete(object)</tt> - removes the association between this object and the object given as
354
- # argument. Multiple associations can be removed by passing an array of objects instead.
888
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
889
+ # meets the condition that it has to be associated with this object.
890
+ # * <tt>collection.build(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
891
+ # with +attributes+ and linked to this object through the join table, but has not yet been saved.
892
+ # * <tt>collection.create(attributes = {})</tt> - returns a new object of the collection type that has been instantiated
893
+ # with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation).
355
894
  #
356
- # Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
895
+ # Example: A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
357
896
  # * <tt>Developer#projects</tt>
358
- # * <tt>!Developer#projects.empty?</tt>
359
- # * <tt>Developer#projects.size</tt>
360
897
  # * <tt>Developer#projects<<</tt>
361
898
  # * <tt>Developer#projects.delete</tt>
362
- # The declaration can also include an options hash to specialize the generated methods.
899
+ # * <tt>Developer#projects=</tt>
900
+ # * <tt>Developer#project_ids</tt>
901
+ # * <tt>Developer#project_ids=</tt>
902
+ # * <tt>Developer#projects.clear</tt>
903
+ # * <tt>Developer#projects.empty?</tt>
904
+ # * <tt>Developer#projects.size</tt>
905
+ # * <tt>Developer#projects.find(id)</tt>
906
+ # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
907
+ # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
908
+ # The declaration may include an options hash to specialize the behavior of the association.
363
909
  #
364
910
  # Options are:
365
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
911
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
366
912
  # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
367
913
  # +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
368
914
  # * <tt>:join_table</tt> - specify the name of the join table if the default based on lexical order isn't what you want.
369
- # WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
370
- # has_and_belongs_to_many declaration in order to work.
915
+ # WARNING: If you're overwriting the table name of either class, the +table_name+ method MUST be declared underneath any
916
+ # +has_and_belongs_to_many+ declaration in order to work.
371
917
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
372
- # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
373
- # will use "person_id" as the default foreign_key.
918
+ # of this class in lower-case and +_id+ suffixed. So a +Person+ class that makes a +has_and_belongs_to_many+ association
919
+ # will use +person_id+ as the default +foreign_key+.
374
920
  # * <tt>:association_foreign_key</tt> - specify the association foreign key used for the association. By default this is
375
- # guessed to be the name of the associated class in lower-case and "_id" suffixed. So the associated class is +Project+
376
- # that makes a has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
377
- # * <tt>:order</tt> - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC".
378
- # * <tt>:finder_sql</tt> - overwrite the default generated SQL used to fetch the association with a manual one
379
- # * <tt>:delete_sql</tt> - overwrite the default generated SQL used to remove links between the associated
380
- # classes with a manual one
381
- # * <tt>:insert_sql</tt> - overwrite the default generated SQL used to add links between the associated classes
382
- # with a manual one
921
+ # guessed to be the name of the associated class in lower-case and +_id+ suffixed. So if the associated class is +Project+,
922
+ # the +has_and_belongs_to_many+ association will use +project_id+ as the default association +foreign_key+.
923
+ # * <tt>:conditions</tt> - specify the conditions that the associated object must meet in order to be included as a +WHERE+
924
+ # SQL fragment, such as <tt>authorized = 1</tt>.
925
+ # * <tt>:order</tt> - specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
926
+ # such as <tt>last_name, first_name DESC</tt>
927
+ # * <tt>:uniq</tt> - if set to +true+, duplicate associated objects will be ignored by accessors and query methods
928
+ # * <tt>:finder_sql</tt> - overwrite the default generated SQL statement used to fetch the association with a manual statement
929
+ # * <tt>:delete_sql</tt> - overwrite the default generated SQL statement used to remove links between the associated
930
+ # classes with a manual statement
931
+ # * <tt>:insert_sql</tt> - overwrite the default generated SQL statement used to add links between the associated classes
932
+ # with a manual statement
933
+ # * <tt>:extend</tt> - anonymous module for extending the proxy, see "Association extensions".
934
+ # * <tt>:include</tt> - specify second-order associations that should be eager loaded when the collection is loaded.
935
+ # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
936
+ # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned.
937
+ # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
938
+ # * <tt>:select</tt>: By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
939
+ # but not include the joined columns.
383
940
  #
384
941
  # Option examples:
385
942
  # has_and_belongs_to_many :projects
943
+ # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
386
944
  # has_and_belongs_to_many :nations, :class_name => "Country"
387
945
  # has_and_belongs_to_many :categories, :join_table => "prods_cats"
388
- def has_and_belongs_to_many(association_id, options = {})
389
- validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key,
390
- :join_table, :finder_sql, :delete_sql, :insert_sql, :order ], options.keys)
391
- association_name, association_class_name, association_class_primary_key_name =
392
- associate_identification(association_id, options[:class_name], options[:foreign_key])
393
-
394
- join_table = options[:join_table] ||
395
- join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
396
-
946
+ # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
947
+ # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
948
+ def has_and_belongs_to_many(association_id, options = {}, &extension)
949
+ reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
397
950
 
398
- module_eval <<-"end_eval", __FILE__, __LINE__
399
- def #{association_name}(force_reload = false)
400
- if @#{association_name}.nil?
401
- @#{association_name} = HasAndBelongsToManyCollection.new(self, "#{association_name}", "#{association_class_name}",
402
- "#{association_class_primary_key_name}", '#{join_table}', #{options.inspect})
403
- end
404
- @#{association_name}.reload if force_reload
405
-
406
- return @#{association_name}
951
+ add_multiple_associated_save_callbacks(reflection.name)
952
+ collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
953
+
954
+ # Don't use a before_destroy callback since users' before_destroy
955
+ # callbacks will be executed after the association is wiped out.
956
+ old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
957
+ class_eval <<-end_eval unless method_defined?(old_method)
958
+ alias_method :#{old_method}, :destroy_without_callbacks
959
+ def destroy_without_callbacks
960
+ #{reflection.name}.clear
961
+ #{old_method}
407
962
  end
408
963
  end_eval
409
964
 
410
- before_destroy_sql = "DELETE FROM #{join_table} WHERE #{Inflector.foreign_key(self.class_name)} = '\\\#{self.id}'"
411
- module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
412
-
413
- # deprecated api
414
- deprecated_collection_count_method(association_name)
415
- deprecated_add_association_relation(association_name)
416
- deprecated_remove_association_relation(association_name)
417
- deprecated_has_collection_method(association_name)
965
+ add_association_callbacks(reflection.name, options)
418
966
  end
419
967
 
420
968
  private
421
- # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
422
- def validate_options(valid_option_keys, supplied_option_keys)
423
- unknown_option_keys = supplied_option_keys - valid_option_keys
424
- raise(ActiveRecord::ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
425
- end
426
-
969
+ # Generate a join table name from two provided tables names.
970
+ # The order of names in join name is determined by lexical precedence.
971
+ # join_table_name("members", "clubs")
972
+ # => "clubs_members"
973
+ # join_table_name("members", "special_clubs")
974
+ # => "members_special_clubs"
427
975
  def join_table_name(first_table_name, second_table_name)
428
976
  if first_table_name < second_table_name
429
977
  join_table = "#{first_table_name}_#{second_table_name}"
@@ -433,104 +981,789 @@ module ActiveRecord
433
981
 
434
982
  table_name_prefix + join_table + table_name_suffix
435
983
  end
436
-
437
- def associate_identification(association_id, association_class_name, foreign_key, plural = true)
438
- if association_class_name !~ /::/
439
- association_class_name = type_name_with_module(
440
- association_class_name ||
441
- Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
442
- )
984
+
985
+ def association_accessor_methods(reflection, association_proxy_class)
986
+ define_method(reflection.name) do |*params|
987
+ force_reload = params.first unless params.empty?
988
+ association = instance_variable_get("@#{reflection.name}")
989
+
990
+ if association.nil? || force_reload
991
+ association = association_proxy_class.new(self, reflection)
992
+ retval = association.reload
993
+ if retval.nil? and association_proxy_class == BelongsToAssociation
994
+ instance_variable_set("@#{reflection.name}", nil)
995
+ return nil
996
+ end
997
+ instance_variable_set("@#{reflection.name}", association)
998
+ end
999
+
1000
+ association.target.nil? ? nil : association
443
1001
  end
444
1002
 
445
- primary_key_name = foreign_key || Inflector.underscore(Inflector.demodulize(name)) + "_id"
446
-
447
- return association_id.id2name, association_class_name, primary_key_name
1003
+ define_method("#{reflection.name}=") do |new_value|
1004
+ association = instance_variable_get("@#{reflection.name}")
1005
+ if association.nil? || association.target != new_value
1006
+ association = association_proxy_class.new(self, reflection)
1007
+ end
1008
+
1009
+ association.replace(new_value)
1010
+
1011
+ unless new_value.nil?
1012
+ instance_variable_set("@#{reflection.name}", association)
1013
+ else
1014
+ instance_variable_set("@#{reflection.name}", nil)
1015
+ end
1016
+ end
1017
+
1018
+ define_method("set_#{reflection.name}_target") do |target|
1019
+ return if target.nil? and association_proxy_class == BelongsToAssociation
1020
+ association = association_proxy_class.new(self, reflection)
1021
+ association.target = target
1022
+ instance_variable_set("@#{reflection.name}", association)
1023
+ end
448
1024
  end
449
-
450
- def association_comparison_method(association_name, association_class_name)
451
- module_eval <<-"end_eval", __FILE__, __LINE__
452
- def #{association_name}?(comparison_object, force_reload = false)
453
- if comparison_object.kind_of?(#{association_class_name})
454
- #{association_name}(force_reload) == comparison_object
1025
+
1026
+ def collection_reader_method(reflection, association_proxy_class)
1027
+ define_method(reflection.name) do |*params|
1028
+ force_reload = params.first unless params.empty?
1029
+ association = instance_variable_get("@#{reflection.name}")
1030
+
1031
+ unless association.respond_to?(:loaded?)
1032
+ association = association_proxy_class.new(self, reflection)
1033
+ instance_variable_set("@#{reflection.name}", association)
1034
+ end
1035
+
1036
+ association.reload if force_reload
1037
+
1038
+ association
1039
+ end
1040
+ end
1041
+
1042
+ def collection_accessor_methods(reflection, association_proxy_class, writer = true)
1043
+ collection_reader_method(reflection, association_proxy_class)
1044
+
1045
+ define_method("#{reflection.name}=") do |new_value|
1046
+ # Loads proxy class instance (defined in collection_reader_method) if not already loaded
1047
+ association = send(reflection.name)
1048
+ association.replace(new_value)
1049
+ association
1050
+ end
1051
+
1052
+ define_method("#{reflection.name.to_s.singularize}_ids") do
1053
+ send(reflection.name).map(&:id)
1054
+ end
1055
+
1056
+ define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
1057
+ ids = (new_value || []).reject { |nid| nid.blank? }
1058
+ send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
1059
+ end if writer
1060
+ end
1061
+
1062
+ def add_multiple_associated_save_callbacks(association_name)
1063
+ method_name = "validate_associated_records_for_#{association_name}".to_sym
1064
+ define_method(method_name) do
1065
+ association = instance_variable_get("@#{association_name}")
1066
+ if association.respond_to?(:loaded?)
1067
+ if new_record?
1068
+ association
455
1069
  else
456
- raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
1070
+ association.select { |record| record.new_record? }
1071
+ end.each do |record|
1072
+ errors.add "#{association_name}" unless record.valid?
457
1073
  end
458
1074
  end
1075
+ end
1076
+
1077
+ validate method_name
1078
+ before_save("@new_record_before_save = new_record?; true")
1079
+
1080
+ after_callback = <<-end_eval
1081
+ association = instance_variable_get("@#{association_name}")
1082
+
1083
+ records_to_save = if @new_record_before_save
1084
+ association
1085
+ elsif association.respond_to?(:loaded?) && association.loaded?
1086
+ association.select { |record| record.new_record? }
1087
+ else
1088
+ []
1089
+ end
1090
+
1091
+ records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
1092
+
1093
+ # reconstruct the SQL queries now that we know the owner's id
1094
+ association.send(:construct_sql) if association.respond_to?(:construct_sql)
459
1095
  end_eval
1096
+
1097
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
1098
+ after_create(after_callback)
1099
+ after_update(after_callback)
460
1100
  end
461
1101
 
462
- def association_reader_method(association_name, association_finder)
463
- module_eval <<-"end_eval", __FILE__, __LINE__
464
- def #{association_name}(force_reload = false)
465
- if @#{association_name}.nil? || force_reload
466
- begin
467
- @#{association_name} = #{association_finder}
468
- rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
469
- nil
470
- end
471
- end
472
-
473
- return @#{association_name}
1102
+ def association_constructor_method(constructor, reflection, association_proxy_class)
1103
+ define_method("#{constructor}_#{reflection.name}") do |*params|
1104
+ attributees = params.first unless params.empty?
1105
+ replace_existing = params[1].nil? ? true : params[1]
1106
+ association = instance_variable_get("@#{reflection.name}")
1107
+
1108
+ if association.nil?
1109
+ association = association_proxy_class.new(self, reflection)
1110
+ instance_variable_set("@#{reflection.name}", association)
474
1111
  end
475
- end_eval
1112
+
1113
+ if association_proxy_class == HasOneAssociation
1114
+ association.send(constructor, attributees, replace_existing)
1115
+ else
1116
+ association.send(constructor, attributees)
1117
+ end
1118
+ end
1119
+ end
1120
+
1121
+ def find_with_associations(options = {})
1122
+ catch :invalid_query do
1123
+ join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
1124
+ rows = select_all_rows(options, join_dependency)
1125
+ return join_dependency.instantiate(rows)
1126
+ end
1127
+ []
476
1128
  end
477
1129
 
478
- def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
479
- module_eval <<-"end_eval", __FILE__, __LINE__
480
- def #{association_name}=(association)
481
- if association.nil?
482
- @#{association_name}.#{class_primary_key_name} = nil
483
- @#{association_name}.save(false)
484
- @#{association_name} = nil
1130
+ # See HasManyAssociation#delete_records. Dependent associations
1131
+ # delete children, otherwise foreign key is set to NULL.
1132
+ def configure_dependency_for_has_many(reflection)
1133
+ if reflection.options.include?(:dependent)
1134
+ # Add polymorphic type if the :as option is present
1135
+ dependent_conditions = []
1136
+ dependent_conditions << "#{reflection.primary_key_name} = \#{record.quoted_id}"
1137
+ dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
1138
+ dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions]
1139
+ dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
1140
+
1141
+ case reflection.options[:dependent]
1142
+ when :destroy
1143
+ module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
1144
+ when :delete_all
1145
+ module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }"
1146
+ when :nullify
1147
+ module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{dependent_conditions})) }"
485
1148
  else
486
- raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
487
- association.#{class_primary_key_name} = id
488
- association.save(false)
489
- @#{association_name} = association
490
- end
1149
+ raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
491
1150
  end
492
- end_eval
1151
+ end
493
1152
  end
494
1153
 
495
- def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
496
- module_eval <<-"end_eval", __FILE__, __LINE__
497
- def #{association_name}=(association)
498
- if association.nil?
499
- @#{association_name} = self.#{association_class_primary_key_name} = nil
1154
+ def configure_dependency_for_has_one(reflection)
1155
+ if reflection.options.include?(:dependent)
1156
+ case reflection.options[:dependent]
1157
+ when :destroy
1158
+ module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'"
1159
+ when :delete
1160
+ module_eval "before_destroy '#{reflection.class_name}.delete(#{reflection.name}.id) unless #{reflection.name}.nil?'"
1161
+ when :nullify
1162
+ module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil) unless #{reflection.name}.nil?'"
500
1163
  else
501
- raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
502
- @#{association_name} = association
503
- self.#{association_class_primary_key_name} = association.id
504
- end
1164
+ raise ArgumentError, "The :dependent option expects either :destroy, :delete or :nullify (#{reflection.options[:dependent].inspect})"
505
1165
  end
506
- end_eval
1166
+ end
1167
+ end
1168
+
1169
+ def create_has_many_reflection(association_id, options, &extension)
1170
+ options.assert_valid_keys(
1171
+ :class_name, :table_name, :foreign_key,
1172
+ :dependent,
1173
+ :select, :conditions, :include, :order, :group, :limit, :offset,
1174
+ :as, :through, :source, :source_type,
1175
+ :uniq,
1176
+ :finder_sql, :counter_sql,
1177
+ :before_add, :after_add, :before_remove, :after_remove,
1178
+ :extend
1179
+ )
1180
+
1181
+ options[:extend] = create_extension_modules(association_id, extension, options[:extend]) if block_given?
1182
+
1183
+ create_reflection(:has_many, association_id, options, self)
1184
+ end
1185
+
1186
+ def create_has_one_reflection(association_id, options)
1187
+ options.assert_valid_keys(
1188
+ :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as
1189
+ )
1190
+
1191
+ create_reflection(:has_one, association_id, options, self)
507
1192
  end
508
1193
 
509
- def has_association_method(association_name)
510
- module_eval <<-"end_eval", __FILE__, __LINE__
511
- def has_#{association_name}?(force_reload = false)
512
- !#{association_name}(force_reload).nil?
1194
+ def create_belongs_to_reflection(association_id, options)
1195
+ options.assert_valid_keys(
1196
+ :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,
1197
+ :counter_cache, :extend, :polymorphic
1198
+ )
1199
+
1200
+ reflection = create_reflection(:belongs_to, association_id, options, self)
1201
+
1202
+ if options[:polymorphic]
1203
+ reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
1204
+ end
1205
+
1206
+ reflection
1207
+ end
1208
+
1209
+ def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
1210
+ options.assert_valid_keys(
1211
+ :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
1212
+ :select, :conditions, :include, :order, :group, :limit, :offset,
1213
+ :uniq,
1214
+ :finder_sql, :delete_sql, :insert_sql,
1215
+ :before_add, :after_add, :before_remove, :after_remove,
1216
+ :extend
1217
+ )
1218
+
1219
+ options[:extend] = create_extension_modules(association_id, extension, options[:extend]) if block_given?
1220
+
1221
+ reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
1222
+
1223
+ reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
1224
+
1225
+ reflection
1226
+ end
1227
+
1228
+ def reflect_on_included_associations(associations)
1229
+ [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
1230
+ end
1231
+
1232
+ def guard_against_unlimitable_reflections(reflections, options)
1233
+ if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections)
1234
+ raise(
1235
+ ConfigurationError,
1236
+ "You can not use offset and limit together with has_many or has_and_belongs_to_many associations"
1237
+ )
1238
+ end
1239
+ end
1240
+
1241
+ def select_all_rows(options, join_dependency)
1242
+ connection.select_all(
1243
+ construct_finder_sql_with_included_associations(options, join_dependency),
1244
+ "#{name} Load Including Associations"
1245
+ )
1246
+ end
1247
+
1248
+ def construct_finder_sql_with_included_associations(options, join_dependency)
1249
+ scope = scope(:find)
1250
+ sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
1251
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
1252
+
1253
+ add_joins!(sql, options, scope)
1254
+ add_conditions!(sql, options[:conditions], scope)
1255
+ add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
1256
+
1257
+ add_group!(sql, options[:group], scope)
1258
+ add_order!(sql, options[:order], scope)
1259
+ add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
1260
+ add_lock!(sql, options, scope)
1261
+
1262
+ return sanitize_sql(sql)
1263
+ end
1264
+
1265
+ def add_limited_ids_condition!(sql, options, join_dependency)
1266
+ unless (id_list = select_limited_ids_list(options, join_dependency)).empty?
1267
+ sql << "#{condition_word(sql)} #{connection.quote_table_name table_name}.#{primary_key} IN (#{id_list}) "
1268
+ else
1269
+ throw :invalid_query
1270
+ end
1271
+ end
1272
+
1273
+ def select_limited_ids_list(options, join_dependency)
1274
+ pk = columns_hash[primary_key]
1275
+
1276
+ connection.select_all(
1277
+ construct_finder_sql_for_association_limiting(options, join_dependency),
1278
+ "#{name} Load IDs For Limited Eager Loading"
1279
+ ).collect { |row| connection.quote(row[primary_key], pk) }.join(", ")
1280
+ end
1281
+
1282
+ def construct_finder_sql_for_association_limiting(options, join_dependency)
1283
+ scope = scope(:find)
1284
+ is_distinct = !options[:joins].blank? || include_eager_conditions?(options) || include_eager_order?(options)
1285
+ sql = "SELECT "
1286
+ if is_distinct
1287
+ sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order])
1288
+ else
1289
+ sql << primary_key
1290
+ end
1291
+ sql << " FROM #{connection.quote_table_name table_name} "
1292
+
1293
+ if is_distinct
1294
+ sql << join_dependency.join_associations.collect(&:association_join).join
1295
+ add_joins!(sql, options, scope)
1296
+ end
1297
+
1298
+ add_conditions!(sql, options[:conditions], scope)
1299
+ add_group!(sql, options[:group], scope)
1300
+
1301
+ if options[:order] && is_distinct
1302
+ connection.add_order_by_for_association_limiting!(sql, options)
1303
+ else
1304
+ add_order!(sql, options[:order], scope)
1305
+ end
1306
+
1307
+ add_limit!(sql, options, scope)
1308
+
1309
+ return sanitize_sql(sql)
1310
+ end
1311
+
1312
+ # Checks if the conditions reference a table other than the current model table
1313
+ def include_eager_conditions?(options)
1314
+ # look in both sets of conditions
1315
+ conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
1316
+ case cond
1317
+ when nil then all
1318
+ when Array then all << cond.first
1319
+ else all << cond
513
1320
  end
514
- end_eval
1321
+ end
1322
+ return false unless conditions.any?
1323
+ conditions.join(' ').scan(/([\.\w]+)\.\w+/).flatten.any? do |condition_table_name|
1324
+ condition_table_name != table_name
1325
+ end
515
1326
  end
516
1327
 
517
- def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
518
- module_eval <<-"end_eval", __FILE__, __LINE__
519
- def #{method_prefix + collection_name}(attributes = {})
520
- association = #{collection_class_name}.new
521
- association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
522
- association
1328
+ # Checks if the query order references a table other than the current model's table.
1329
+ def include_eager_order?(options)
1330
+ order = options[:order]
1331
+ return false unless order
1332
+ order.scan(/([\.\w]+)\.\w+/).flatten.any? do |order_table_name|
1333
+ order_table_name != table_name
1334
+ end
1335
+ end
1336
+
1337
+ def using_limitable_reflections?(reflections)
1338
+ reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
1339
+ end
1340
+
1341
+ def column_aliases(join_dependency)
1342
+ join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
1343
+ "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
1344
+ end
1345
+
1346
+ def add_association_callbacks(association_name, options)
1347
+ callbacks = %w(before_add after_add before_remove after_remove)
1348
+ callbacks.each do |callback_name|
1349
+ full_callback_name = "#{callback_name}_for_#{association_name}"
1350
+ defined_callbacks = options[callback_name.to_sym]
1351
+ if options.has_key?(callback_name.to_sym)
1352
+ class_inheritable_reader full_callback_name.to_sym
1353
+ write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten)
1354
+ else
1355
+ write_inheritable_attribute(full_callback_name.to_sym, [])
523
1356
  end
524
- end_eval
1357
+ end
1358
+ end
1359
+
1360
+ def condition_word(sql)
1361
+ sql =~ /where/i ? " AND " : "WHERE "
525
1362
  end
526
1363
 
527
- def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
528
- module_eval <<-"end_eval", __FILE__, __LINE__
529
- def #{method_prefix + collection_name}(attributes = nil)
530
- #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
1364
+ def create_extension_modules(association_id, block_extension, extensions)
1365
+ extension_module_name = "#{self.to_s}#{association_id.to_s.camelize}AssociationExtension"
1366
+
1367
+ silence_warnings do
1368
+ Object.const_set(extension_module_name, Module.new(&block_extension))
1369
+ end
1370
+
1371
+ Array(extensions).push(extension_module_name.constantize)
1372
+ end
1373
+
1374
+ class JoinDependency # :nodoc:
1375
+ attr_reader :joins, :reflections, :table_aliases
1376
+
1377
+ def initialize(base, associations, joins)
1378
+ @joins = [JoinBase.new(base, joins)]
1379
+ @associations = associations
1380
+ @reflections = []
1381
+ @base_records_hash = {}
1382
+ @base_records_in_order = []
1383
+ @table_aliases = Hash.new { |aliases, table| aliases[table] = 0 }
1384
+ @table_aliases[base.table_name] = 1
1385
+ build(associations)
1386
+ end
1387
+
1388
+ def join_associations
1389
+ @joins[1..-1].to_a
1390
+ end
1391
+
1392
+ def join_base
1393
+ @joins[0]
1394
+ end
1395
+
1396
+ def instantiate(rows)
1397
+ rows.each_with_index do |row, i|
1398
+ primary_id = join_base.record_id(row)
1399
+ unless @base_records_hash[primary_id]
1400
+ @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
1401
+ end
1402
+ construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
531
1403
  end
532
- end_eval
1404
+ remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations)
1405
+ return @base_records_in_order
1406
+ end
1407
+
1408
+ def remove_duplicate_results!(base, records, associations)
1409
+ case associations
1410
+ when Symbol, String
1411
+ reflection = base.reflections[associations]
1412
+ if reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
1413
+ records.each { |record| record.send(reflection.name).target.uniq! }
1414
+ end
1415
+ when Array
1416
+ associations.each do |association|
1417
+ remove_duplicate_results!(base, records, association)
1418
+ end
1419
+ when Hash
1420
+ associations.keys.each do |name|
1421
+ reflection = base.reflections[name]
1422
+ is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
1423
+
1424
+ parent_records = records.map do |record|
1425
+ next unless record.send(reflection.name)
1426
+ is_collection ? record.send(reflection.name).target.uniq! : record.send(reflection.name)
1427
+ end.flatten.compact
1428
+
1429
+ remove_duplicate_results!(reflection.class_name.constantize, parent_records, associations[name]) unless parent_records.empty?
1430
+ end
1431
+ end
1432
+ end
1433
+
1434
+ protected
1435
+ def build(associations, parent = nil)
1436
+ parent ||= @joins.last
1437
+ case associations
1438
+ when Symbol, String
1439
+ reflection = parent.reflections[associations.to_s.intern] or
1440
+ raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
1441
+ @reflections << reflection
1442
+ @joins << build_join_association(reflection, parent)
1443
+ when Array
1444
+ associations.each do |association|
1445
+ build(association, parent)
1446
+ end
1447
+ when Hash
1448
+ associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
1449
+ build(name, parent)
1450
+ build(associations[name])
1451
+ end
1452
+ else
1453
+ raise ConfigurationError, associations.inspect
1454
+ end
1455
+ end
1456
+
1457
+ # overridden in InnerJoinDependency subclass
1458
+ def build_join_association(reflection, parent)
1459
+ JoinAssociation.new(reflection, self, parent)
1460
+ end
1461
+
1462
+ def construct(parent, associations, joins, row)
1463
+ case associations
1464
+ when Symbol, String
1465
+ while (join = joins.shift).reflection.name.to_s != associations.to_s
1466
+ raise ConfigurationError, "Not Enough Associations" if joins.empty?
1467
+ end
1468
+ construct_association(parent, join, row)
1469
+ when Array
1470
+ associations.each do |association|
1471
+ construct(parent, association, joins, row)
1472
+ end
1473
+ when Hash
1474
+ associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
1475
+ association = construct_association(parent, joins.shift, row)
1476
+ construct(association, associations[name], joins, row) if association
1477
+ end
1478
+ else
1479
+ raise ConfigurationError, associations.inspect
1480
+ end
1481
+ end
1482
+
1483
+ def construct_association(record, join, row)
1484
+ case join.reflection.macro
1485
+ when :has_many, :has_and_belongs_to_many
1486
+ collection = record.send(join.reflection.name)
1487
+ collection.loaded
1488
+
1489
+ return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
1490
+ association = join.instantiate(row)
1491
+ collection.target.push(association)
1492
+ when :has_one
1493
+ return if record.id.to_s != join.parent.record_id(row).to_s
1494
+ association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
1495
+ record.send("set_#{join.reflection.name}_target", association)
1496
+ when :belongs_to
1497
+ return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
1498
+ association = join.instantiate(row)
1499
+ record.send("set_#{join.reflection.name}_target", association)
1500
+ else
1501
+ raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
1502
+ end
1503
+ return association
1504
+ end
1505
+
1506
+ class JoinBase # :nodoc:
1507
+ attr_reader :active_record, :table_joins
1508
+ delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record
1509
+
1510
+ def initialize(active_record, joins = nil)
1511
+ @active_record = active_record
1512
+ @cached_record = {}
1513
+ @table_joins = joins
1514
+ end
1515
+
1516
+ def aliased_prefix
1517
+ "t0"
1518
+ end
1519
+
1520
+ def aliased_primary_key
1521
+ "#{ aliased_prefix }_r0"
1522
+ end
1523
+
1524
+ def aliased_table_name
1525
+ active_record.table_name
1526
+ end
1527
+
1528
+ def column_names_with_alias
1529
+ unless @column_names_with_alias
1530
+ @column_names_with_alias = []
1531
+ ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
1532
+ @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
1533
+ end
1534
+ end
1535
+ return @column_names_with_alias
1536
+ end
1537
+
1538
+ def extract_record(row)
1539
+ column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record}
1540
+ end
1541
+
1542
+ def record_id(row)
1543
+ row[aliased_primary_key]
1544
+ end
1545
+
1546
+ def instantiate(row)
1547
+ @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
1548
+ end
1549
+ end
1550
+
1551
+ class JoinAssociation < JoinBase # :nodoc:
1552
+ attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name
1553
+ delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
1554
+
1555
+ def initialize(reflection, join_dependency, parent = nil)
1556
+ reflection.check_validity!
1557
+ if reflection.options[:polymorphic]
1558
+ raise EagerLoadPolymorphicError.new(reflection)
1559
+ end
1560
+
1561
+ super(reflection.klass)
1562
+ @parent = parent
1563
+ @reflection = reflection
1564
+ @aliased_prefix = "t#{ join_dependency.joins.size }"
1565
+ @aliased_table_name = table_name #.tr('.', '_') # start with the table name, sub out any .'s
1566
+ @parent_table_name = parent.active_record.table_name
1567
+
1568
+ if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\son}
1569
+ join_dependency.table_aliases[aliased_table_name] += 1
1570
+ end
1571
+
1572
+ unless join_dependency.table_aliases[aliased_table_name].zero?
1573
+ # if the table name has been used, then use an alias
1574
+ @aliased_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}"
1575
+ table_index = join_dependency.table_aliases[aliased_table_name]
1576
+ join_dependency.table_aliases[aliased_table_name] += 1
1577
+ @aliased_table_name = @aliased_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
1578
+ else
1579
+ join_dependency.table_aliases[aliased_table_name] += 1
1580
+ end
1581
+
1582
+ if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through])
1583
+ @aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : reflection.through_reflection.klass.table_name
1584
+ unless join_dependency.table_aliases[aliased_join_table_name].zero?
1585
+ @aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join"
1586
+ table_index = join_dependency.table_aliases[aliased_join_table_name]
1587
+ join_dependency.table_aliases[aliased_join_table_name] += 1
1588
+ @aliased_join_table_name = @aliased_join_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
1589
+ else
1590
+ join_dependency.table_aliases[aliased_join_table_name] += 1
1591
+ end
1592
+ end
1593
+ end
1594
+
1595
+ def association_join
1596
+ connection = reflection.active_record.connection
1597
+ join = case reflection.macro
1598
+ when :has_and_belongs_to_many
1599
+ " #{join_type} %s ON %s.%s = %s.%s " % [
1600
+ table_alias_for(options[:join_table], aliased_join_table_name),
1601
+ connection.quote_table_name(aliased_join_table_name),
1602
+ options[:foreign_key] || reflection.active_record.to_s.foreign_key,
1603
+ connection.quote_table_name(parent.aliased_table_name),
1604
+ reflection.active_record.primary_key] +
1605
+ " #{join_type} %s ON %s.%s = %s.%s " % [
1606
+ table_name_and_alias,
1607
+ connection.quote_table_name(aliased_table_name),
1608
+ klass.primary_key,
1609
+ connection.quote_table_name(aliased_join_table_name),
1610
+ options[:association_foreign_key] || klass.to_s.foreign_key
1611
+ ]
1612
+ when :has_many, :has_one
1613
+ case
1614
+ when reflection.macro == :has_many && reflection.options[:through]
1615
+ through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
1616
+
1617
+ jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
1618
+ first_key = second_key = as_extra = nil
1619
+
1620
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
1621
+ jt_foreign_key = through_reflection.options[:as].to_s + '_id'
1622
+ jt_as_extra = " AND %s.%s = %s" % [
1623
+ connection.quote_table_name(aliased_join_table_name),
1624
+ connection.quote_column_name(through_reflection.options[:as].to_s + '_type'),
1625
+ klass.quote_value(parent.active_record.base_class.name)
1626
+ ]
1627
+ else
1628
+ jt_foreign_key = through_reflection.primary_key_name
1629
+ end
1630
+
1631
+ case source_reflection.macro
1632
+ when :has_many
1633
+ if source_reflection.options[:as]
1634
+ first_key = "#{source_reflection.options[:as]}_id"
1635
+ second_key = options[:foreign_key] || primary_key
1636
+ as_extra = " AND %s.%s = %s" % [
1637
+ connection.quote_table_name(aliased_table_name),
1638
+ connection.quote_column_name("#{source_reflection.options[:as]}_type"),
1639
+ klass.quote_value(source_reflection.active_record.base_class.name)
1640
+ ]
1641
+ else
1642
+ first_key = through_reflection.klass.base_class.to_s.foreign_key
1643
+ second_key = options[:foreign_key] || primary_key
1644
+ end
1645
+
1646
+ unless through_reflection.klass.descends_from_active_record?
1647
+ jt_sti_extra = " AND %s.%s = %s" % [
1648
+ connection.quote_table_name(aliased_join_table_name),
1649
+ connection.quote_column_name(through_reflection.active_record.inheritance_column),
1650
+ through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
1651
+ end
1652
+ when :belongs_to
1653
+ first_key = primary_key
1654
+ if reflection.options[:source_type]
1655
+ second_key = source_reflection.association_foreign_key
1656
+ jt_source_extra = " AND %s.%s = %s" % [
1657
+ connection.quote_table_name(aliased_join_table_name),
1658
+ connection.quote_column_name(reflection.source_reflection.options[:foreign_type]),
1659
+ klass.quote_value(reflection.options[:source_type])
1660
+ ]
1661
+ else
1662
+ second_key = source_reflection.primary_key_name
1663
+ end
1664
+ end
1665
+
1666
+ " #{join_type} %s ON (%s.%s = %s.%s%s%s%s) " % [
1667
+ table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
1668
+ connection.quote_table_name(parent.aliased_table_name),
1669
+ connection.quote_column_name(parent.primary_key),
1670
+ connection.quote_table_name(aliased_join_table_name),
1671
+ connection.quote_column_name(jt_foreign_key),
1672
+ jt_as_extra, jt_source_extra, jt_sti_extra
1673
+ ] +
1674
+ " #{join_type} %s ON (%s.%s = %s.%s%s) " % [
1675
+ table_name_and_alias,
1676
+ connection.quote_table_name(aliased_table_name),
1677
+ connection.quote_column_name(first_key),
1678
+ connection.quote_table_name(aliased_join_table_name),
1679
+ connection.quote_column_name(second_key),
1680
+ as_extra
1681
+ ]
1682
+
1683
+ when reflection.options[:as] && [:has_many, :has_one].include?(reflection.macro)
1684
+ " #{join_type} %s ON %s.%s = %s.%s AND %s.%s = %s" % [
1685
+ table_name_and_alias,
1686
+ connection.quote_table_name(aliased_table_name),
1687
+ "#{reflection.options[:as]}_id",
1688
+ connection.quote_table_name(parent.aliased_table_name),
1689
+ parent.primary_key,
1690
+ connection.quote_table_name(aliased_table_name),
1691
+ "#{reflection.options[:as]}_type",
1692
+ klass.quote_value(parent.active_record.base_class.name)
1693
+ ]
1694
+ else
1695
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
1696
+ " #{join_type} %s ON %s.%s = %s.%s " % [
1697
+ table_name_and_alias,
1698
+ aliased_table_name,
1699
+ foreign_key,
1700
+ parent.aliased_table_name,
1701
+ parent.primary_key
1702
+ ]
1703
+ end
1704
+ when :belongs_to
1705
+ " #{join_type} %s ON %s.%s = %s.%s " % [
1706
+ table_name_and_alias,
1707
+ connection.quote_table_name(aliased_table_name),
1708
+ reflection.klass.primary_key,
1709
+ connection.quote_table_name(parent.aliased_table_name),
1710
+ options[:foreign_key] || klass.to_s.foreign_key
1711
+ ]
1712
+ else
1713
+ ""
1714
+ end || ''
1715
+ join << %(AND %s.%s = %s ) % [
1716
+ connection.quote_table_name(aliased_table_name),
1717
+ connection.quote_column_name(klass.inheritance_column),
1718
+ klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record?
1719
+
1720
+ [through_reflection, reflection].each do |ref|
1721
+ join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]
1722
+ end
1723
+
1724
+ join
1725
+ end
1726
+
1727
+ protected
1728
+
1729
+ def pluralize(table_name)
1730
+ ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
1731
+ end
1732
+
1733
+ def table_alias_for(table_name, table_alias)
1734
+ "#{reflection.active_record.connection.quote_table_name(table_name)} #{table_alias if table_name != table_alias}".strip
1735
+ end
1736
+
1737
+ def table_name_and_alias
1738
+ table_alias_for table_name, @aliased_table_name
1739
+ end
1740
+
1741
+ def interpolate_sql(sql)
1742
+ instance_eval("%@#{sql.gsub('@', '\@')}@")
1743
+ end
1744
+
1745
+ private
1746
+
1747
+ def join_type
1748
+ "LEFT OUTER JOIN"
1749
+ end
1750
+ end
1751
+ end
1752
+
1753
+ class InnerJoinDependency < JoinDependency # :nodoc:
1754
+ protected
1755
+ def build_join_association(reflection, parent)
1756
+ InnerJoinAssociation.new(reflection, self, parent)
1757
+ end
1758
+
1759
+ class InnerJoinAssociation < JoinAssociation
1760
+ private
1761
+ def join_type
1762
+ "INNER JOIN"
1763
+ end
1764
+ end
533
1765
  end
1766
+
534
1767
  end
535
1768
  end
536
- end
1769
+ end