activerecord 1.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (255) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +213 -0
  5. data/examples/performance.rb +172 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record/aggregations.rb +180 -84
  8. data/lib/active_record/associations/alias_tracker.rb +76 -0
  9. data/lib/active_record/associations/association.rb +248 -0
  10. data/lib/active_record/associations/association_scope.rb +135 -0
  11. data/lib/active_record/associations/belongs_to_association.rb +92 -0
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +35 -0
  13. data/lib/active_record/associations/builder/association.rb +108 -0
  14. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  15. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  17. data/lib/active_record/associations/builder/has_many.rb +15 -0
  18. data/lib/active_record/associations/builder/has_one.rb +25 -0
  19. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  20. data/lib/active_record/associations/collection_association.rb +608 -0
  21. data/lib/active_record/associations/collection_proxy.rb +986 -0
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +58 -39
  23. data/lib/active_record/associations/has_many_association.rb +116 -85
  24. data/lib/active_record/associations/has_many_through_association.rb +197 -0
  25. data/lib/active_record/associations/has_one_association.rb +102 -0
  26. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_dependency.rb +235 -0
  31. data/lib/active_record/associations/join_helper.rb +45 -0
  32. data/lib/active_record/associations/preloader/association.rb +121 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  42. data/lib/active_record/associations/preloader.rb +178 -0
  43. data/lib/active_record/associations/singular_association.rb +64 -0
  44. data/lib/active_record/associations/through_association.rb +87 -0
  45. data/lib/active_record/associations.rb +1437 -431
  46. data/lib/active_record/attribute_assignment.rb +201 -0
  47. data/lib/active_record/attribute_methods/before_type_cast.rb +70 -0
  48. data/lib/active_record/attribute_methods/dirty.rb +118 -0
  49. data/lib/active_record/attribute_methods/primary_key.rb +122 -0
  50. data/lib/active_record/attribute_methods/query.rb +40 -0
  51. data/lib/active_record/attribute_methods/read.rb +107 -0
  52. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  53. data/lib/active_record/attribute_methods/time_zone_conversion.rb +59 -0
  54. data/lib/active_record/attribute_methods/write.rb +63 -0
  55. data/lib/active_record/attribute_methods.rb +393 -0
  56. data/lib/active_record/autosave_association.rb +426 -0
  57. data/lib/active_record/base.rb +268 -930
  58. data/lib/active_record/callbacks.rb +203 -230
  59. data/lib/active_record/coders/yaml_column.rb +38 -0
  60. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +638 -0
  61. data/lib/active_record/connection_adapters/abstract/database_limits.rb +67 -0
  62. data/lib/active_record/connection_adapters/abstract/database_statements.rb +390 -0
  63. data/lib/active_record/connection_adapters/abstract/query_cache.rb +95 -0
  64. data/lib/active_record/connection_adapters/abstract/quoting.rb +129 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +501 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  67. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +873 -0
  68. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  69. data/lib/active_record/connection_adapters/abstract_adapter.rb +389 -275
  70. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  71. data/lib/active_record/connection_adapters/column.rb +318 -0
  72. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +517 -90
  75. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  76. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  77. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  78. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  79. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  80. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  81. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  82. data/lib/active_record/connection_adapters/postgresql_adapter.rb +911 -138
  83. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  84. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +624 -0
  85. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  86. data/lib/active_record/connection_handling.rb +98 -0
  87. data/lib/active_record/core.rb +463 -0
  88. data/lib/active_record/counter_cache.rb +122 -0
  89. data/lib/active_record/dynamic_matchers.rb +131 -0
  90. data/lib/active_record/errors.rb +213 -0
  91. data/lib/active_record/explain.rb +38 -0
  92. data/lib/active_record/explain_registry.rb +30 -0
  93. data/lib/active_record/explain_subscriber.rb +29 -0
  94. data/lib/active_record/fixture_set/file.rb +55 -0
  95. data/lib/active_record/fixtures.rb +892 -138
  96. data/lib/active_record/inheritance.rb +200 -0
  97. data/lib/active_record/integration.rb +60 -0
  98. data/lib/active_record/locale/en.yml +47 -0
  99. data/lib/active_record/locking/optimistic.rb +181 -0
  100. data/lib/active_record/locking/pessimistic.rb +77 -0
  101. data/lib/active_record/log_subscriber.rb +82 -0
  102. data/lib/active_record/migration/command_recorder.rb +164 -0
  103. data/lib/active_record/migration/join_table.rb +15 -0
  104. data/lib/active_record/migration.rb +1015 -0
  105. data/lib/active_record/model_schema.rb +345 -0
  106. data/lib/active_record/nested_attributes.rb +546 -0
  107. data/lib/active_record/null_relation.rb +65 -0
  108. data/lib/active_record/persistence.rb +509 -0
  109. data/lib/active_record/query_cache.rb +56 -0
  110. data/lib/active_record/querying.rb +62 -0
  111. data/lib/active_record/railtie.rb +205 -0
  112. data/lib/active_record/railties/console_sandbox.rb +5 -0
  113. data/lib/active_record/railties/controller_runtime.rb +50 -0
  114. data/lib/active_record/railties/databases.rake +402 -0
  115. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  116. data/lib/active_record/readonly_attributes.rb +30 -0
  117. data/lib/active_record/reflection.rb +544 -87
  118. data/lib/active_record/relation/batches.rb +93 -0
  119. data/lib/active_record/relation/calculations.rb +399 -0
  120. data/lib/active_record/relation/delegation.rb +125 -0
  121. data/lib/active_record/relation/finder_methods.rb +349 -0
  122. data/lib/active_record/relation/merger.rb +161 -0
  123. data/lib/active_record/relation/predicate_builder.rb +106 -0
  124. data/lib/active_record/relation/query_methods.rb +1044 -0
  125. data/lib/active_record/relation/spawn_methods.rb +73 -0
  126. data/lib/active_record/relation.rb +655 -0
  127. data/lib/active_record/result.rb +67 -0
  128. data/lib/active_record/runtime_registry.rb +17 -0
  129. data/lib/active_record/sanitization.rb +168 -0
  130. data/lib/active_record/schema.rb +65 -0
  131. data/lib/active_record/schema_dumper.rb +204 -0
  132. data/lib/active_record/schema_migration.rb +39 -0
  133. data/lib/active_record/scoping/default.rb +146 -0
  134. data/lib/active_record/scoping/named.rb +175 -0
  135. data/lib/active_record/scoping.rb +82 -0
  136. data/lib/active_record/serialization.rb +22 -0
  137. data/lib/active_record/serializers/xml_serializer.rb +197 -0
  138. data/lib/active_record/statement_cache.rb +26 -0
  139. data/lib/active_record/store.rb +156 -0
  140. data/lib/active_record/tasks/database_tasks.rb +203 -0
  141. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  142. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  143. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  144. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  145. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  146. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  147. data/lib/active_record/test_case.rb +96 -0
  148. data/lib/active_record/timestamp.rb +119 -0
  149. data/lib/active_record/transactions.rb +366 -69
  150. data/lib/active_record/translation.rb +22 -0
  151. data/lib/active_record/validations/associated.rb +49 -0
  152. data/lib/active_record/validations/presence.rb +65 -0
  153. data/lib/active_record/validations/uniqueness.rb +225 -0
  154. data/lib/active_record/validations.rb +64 -185
  155. data/lib/active_record/version.rb +11 -0
  156. data/lib/active_record.rb +149 -24
  157. data/lib/rails/generators/active_record/migration/migration_generator.rb +62 -0
  158. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  159. data/lib/rails/generators/active_record/migration/templates/migration.rb +39 -0
  160. data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
  161. data/lib/rails/generators/active_record/model/templates/model.rb +10 -0
  162. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  163. data/lib/rails/generators/active_record.rb +23 -0
  164. metadata +261 -161
  165. data/CHANGELOG +0 -581
  166. data/README +0 -361
  167. data/RUNNING_UNIT_TESTS +0 -36
  168. data/dev-utils/eval_debugger.rb +0 -9
  169. data/examples/associations.png +0 -0
  170. data/examples/associations.rb +0 -87
  171. data/examples/shared_setup.rb +0 -15
  172. data/examples/validation.rb +0 -88
  173. data/install.rb +0 -60
  174. data/lib/active_record/associations/association_collection.rb +0 -70
  175. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -107
  176. data/lib/active_record/deprecated_associations.rb +0 -70
  177. data/lib/active_record/observer.rb +0 -71
  178. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  179. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  180. data/lib/active_record/support/clean_logger.rb +0 -10
  181. data/lib/active_record/support/inflector.rb +0 -70
  182. data/lib/active_record/vendor/mysql.rb +0 -1117
  183. data/lib/active_record/vendor/simple.rb +0 -702
  184. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  185. data/lib/active_record/wrappings.rb +0 -59
  186. data/rakefile +0 -122
  187. data/test/abstract_unit.rb +0 -16
  188. data/test/aggregations_test.rb +0 -34
  189. data/test/all.sh +0 -8
  190. data/test/associations_test.rb +0 -477
  191. data/test/base_test.rb +0 -513
  192. data/test/class_inheritable_attributes_test.rb +0 -33
  193. data/test/connections/native_mysql/connection.rb +0 -24
  194. data/test/connections/native_postgresql/connection.rb +0 -24
  195. data/test/connections/native_sqlite/connection.rb +0 -24
  196. data/test/deprecated_associations_test.rb +0 -336
  197. data/test/finder_test.rb +0 -67
  198. data/test/fixtures/accounts/signals37 +0 -3
  199. data/test/fixtures/accounts/unknown +0 -2
  200. data/test/fixtures/auto_id.rb +0 -4
  201. data/test/fixtures/column_name.rb +0 -3
  202. data/test/fixtures/companies/first_client +0 -6
  203. data/test/fixtures/companies/first_firm +0 -4
  204. data/test/fixtures/companies/second_client +0 -6
  205. data/test/fixtures/company.rb +0 -37
  206. data/test/fixtures/company_in_module.rb +0 -33
  207. data/test/fixtures/course.rb +0 -3
  208. data/test/fixtures/courses/java +0 -2
  209. data/test/fixtures/courses/ruby +0 -2
  210. data/test/fixtures/customer.rb +0 -30
  211. data/test/fixtures/customers/david +0 -6
  212. data/test/fixtures/db_definitions/mysql.sql +0 -96
  213. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  214. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  215. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  216. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  217. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  218. data/test/fixtures/default.rb +0 -2
  219. data/test/fixtures/developer.rb +0 -8
  220. data/test/fixtures/developers/david +0 -2
  221. data/test/fixtures/developers/jamis +0 -2
  222. data/test/fixtures/developers_projects/david_action_controller +0 -2
  223. data/test/fixtures/developers_projects/david_active_record +0 -2
  224. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  225. data/test/fixtures/entrant.rb +0 -3
  226. data/test/fixtures/entrants/first +0 -3
  227. data/test/fixtures/entrants/second +0 -3
  228. data/test/fixtures/entrants/third +0 -3
  229. data/test/fixtures/fixture_database.sqlite +0 -0
  230. data/test/fixtures/fixture_database_2.sqlite +0 -0
  231. data/test/fixtures/movie.rb +0 -5
  232. data/test/fixtures/movies/first +0 -2
  233. data/test/fixtures/movies/second +0 -2
  234. data/test/fixtures/project.rb +0 -3
  235. data/test/fixtures/projects/action_controller +0 -2
  236. data/test/fixtures/projects/active_record +0 -2
  237. data/test/fixtures/reply.rb +0 -21
  238. data/test/fixtures/subscriber.rb +0 -5
  239. data/test/fixtures/subscribers/first +0 -2
  240. data/test/fixtures/subscribers/second +0 -2
  241. data/test/fixtures/topic.rb +0 -20
  242. data/test/fixtures/topics/first +0 -9
  243. data/test/fixtures/topics/second +0 -8
  244. data/test/fixtures_test.rb +0 -20
  245. data/test/inflector_test.rb +0 -104
  246. data/test/inheritance_test.rb +0 -125
  247. data/test/lifecycle_test.rb +0 -110
  248. data/test/modules_test.rb +0 -21
  249. data/test/multiple_db_test.rb +0 -46
  250. data/test/pk_test.rb +0 -57
  251. data/test/reflection_test.rb +0 -78
  252. data/test/thread_safety_test.rb +0 -33
  253. data/test/transactions_test.rb +0 -83
  254. data/test/unconnected_test.rb +0 -24
  255. data/test/validations_test.rb +0 -126
@@ -1,19 +1,23 @@
1
1
  module ActiveRecord
2
+ # = Active Record Aggregations
2
3
  module Aggregations # :nodoc:
3
- def self.append_features(base)
4
- super
5
- base.extend(ClassMethods)
4
+ extend ActiveSupport::Concern
5
+
6
+ def clear_aggregation_cache #:nodoc:
7
+ @aggregation_cache.clear if persisted?
6
8
  end
7
9
 
8
- # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
9
- # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
10
- # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
11
- # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
12
- # and how it can be turned back into attributes (when the entity is saved to the database). Example:
10
+ # Active Record implements aggregation through a macro-like class method called +composed_of+
11
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
12
+ # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
13
+ # to the macro adds a description of how the value objects are created from the attributes of
14
+ # the entity object (when the entity is initialized either as a new object or from finding an
15
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
16
+ # the database).
13
17
  #
14
18
  # class Customer < ActiveRecord::Base
15
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
16
- # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
19
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
20
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
17
21
  # end
18
22
  #
19
23
  # The customer class now has the following methods to manipulate the value objects:
@@ -25,10 +29,10 @@ module ActiveRecord
25
29
  # class Money
26
30
  # include Comparable
27
31
  # attr_reader :amount, :currency
28
- # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
29
- #
30
- # def initialize(amount, currency = "USD")
31
- # @amount, @currency = amount, currency
32
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
33
+ #
34
+ # def initialize(amount, currency = "USD")
35
+ # @amount, @currency = amount, currency
32
36
  # end
33
37
  #
34
38
  # def exchange_to(other_currency)
@@ -42,7 +46,7 @@ module ActiveRecord
42
46
  #
43
47
  # def <=>(other_money)
44
48
  # if currency == other_money.currency
45
- # among <=> amount
49
+ # amount <=> other_money.amount
46
50
  # else
47
51
  # amount <=> other_money.exchange_to(currency).amount
48
52
  # end
@@ -51,114 +55,206 @@ module ActiveRecord
51
55
  #
52
56
  # class Address
53
57
  # attr_reader :street, :city
54
- # def initialize(street, city)
55
- # @street, @city = street, city
58
+ # def initialize(street, city)
59
+ # @street, @city = street, city
56
60
  # end
57
61
  #
58
- # def close_to?(other_address)
59
- # city == other_address.city
62
+ # def close_to?(other_address)
63
+ # city == other_address.city
60
64
  # end
61
65
  #
62
66
  # def ==(other_address)
63
67
  # city == other_address.city && street == other_address.street
64
68
  # end
65
69
  # end
66
- #
67
- # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
68
- # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
69
- # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
70
+ #
71
+ # Now it's possible to access attributes from the database through the value objects instead. If
72
+ # you choose to name the composition the same as the attribute's name, it will be the only way to
73
+ # access that attribute. That's the case with our +balance+ attribute. You interact with the value
74
+ # objects just like you would with any other attribute:
70
75
  #
71
76
  # customer.balance = Money.new(20) # sets the Money value object and the attribute
72
77
  # customer.balance # => Money value object
73
- # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
78
+ # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
74
79
  # customer.balance > Money.new(10) # => true
75
80
  # customer.balance == Money.new(20) # => true
76
81
  # customer.balance < Money.new(5) # => false
77
82
  #
78
- # Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
79
- # determine the order of the parameters. Example:
83
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order
84
+ # of the mappings will determine the order of the parameters.
80
85
  #
81
86
  # customer.address_street = "Hyancintvej"
82
87
  # customer.address_city = "Copenhagen"
83
88
  # customer.address # => Address.new("Hyancintvej", "Copenhagen")
89
+ #
90
+ # customer.address_street = "Vesterbrogade"
91
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
92
+ # customer.clear_aggregation_cache
93
+ # customer.address # => Address.new("Vesterbrogade", "Copenhagen")
94
+ #
84
95
  # customer.address = Address.new("May Street", "Chicago")
85
- # customer.address_street # => "May Street"
86
- # customer.address_city # => "Chicago"
96
+ # customer.address_street # => "May Street"
97
+ # customer.address_city # => "Chicago"
87
98
  #
88
99
  # == Writing value objects
89
100
  #
90
- # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
91
- # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
92
- # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
93
- # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
94
- # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
95
- #
96
- # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
97
- # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
98
- # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
99
- # changed through other means than the writer method.
100
- #
101
- # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
102
- # change it afterwards will result in a TypeError.
103
- #
104
- # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
105
- # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
101
+ # Value objects are immutable and interchangeable objects that represent a given value, such as
102
+ # a Money object representing $5. Two Money objects both representing $5 should be equal (through
103
+ # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
104
+ # unlike entity objects where equality is determined by identity. An entity class such as Customer can
105
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is
106
+ # determined by object or relational unique identifiers (such as primary keys). Normal
107
+ # ActiveRecord::Base classes are entity objects.
108
+ #
109
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have
110
+ # its amount changed after creation. Create a new Money object with the new value instead. The
111
+ # Money#exchange_to method is an example of this. It returns a new value object instead of changing
112
+ # its own values. Active Record won't persist value objects that have been changed through means
113
+ # other than the writer method.
114
+ #
115
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
116
+ # object. Attempting to change it afterwards will result in a RuntimeError.
117
+ #
118
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
119
+ # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
120
+ #
121
+ # == Custom constructors and converters
122
+ #
123
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
124
+ # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
125
+ # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
126
+ # a custom constructor to be specified.
127
+ #
128
+ # When a new value is assigned to the value object, the default assumption is that the new value
129
+ # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
130
+ # converted to an instance of value class if necessary.
131
+ #
132
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
133
+ # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
134
+ # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
135
+ # values can be assigned to the value object using either another NetAddr::CIDR object, a string
136
+ # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
137
+ # these requirements:
138
+ #
139
+ # class NetworkResource < ActiveRecord::Base
140
+ # composed_of :cidr,
141
+ # class_name: 'NetAddr::CIDR',
142
+ # mapping: [ %w(network_address network), %w(cidr_range bits) ],
143
+ # allow_nil: true,
144
+ # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
145
+ # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
146
+ # end
147
+ #
148
+ # # This calls the :constructor
149
+ # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
150
+ #
151
+ # # These assignments will both use the :converter
152
+ # network_resource.cidr = [ '192.168.2.1', 8 ]
153
+ # network_resource.cidr = '192.168.0.1/24'
154
+ #
155
+ # # This assignment won't use the :converter as the value is already an instance of the value class
156
+ # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
157
+ #
158
+ # # Saving and then reloading will use the :constructor on reload
159
+ # network_resource.save
160
+ # network_resource.reload
161
+ #
162
+ # == Finding records by a value object
163
+ #
164
+ # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
165
+ # by specifying an instance of the value object in the conditions hash. The following example
166
+ # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
167
+ #
168
+ # Customer.where(balance: Money.new(20, "USD"))
169
+ #
106
170
  module ClassMethods
107
- # Adds the a reader and writer method for manipulating a value object, so
108
- # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
171
+ # Adds reader and writer methods for manipulating a value object:
172
+ # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
109
173
  #
110
174
  # Options are:
111
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
112
- # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
113
- # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
114
- # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
115
- # to a constructor parameter on the value class.
175
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
176
+ # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
177
+ # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
178
+ # with this option.
179
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
180
+ # object. Each mapping is represented as an array where the first item is the name of the
181
+ # entity attribute and the second item is the name of the attribute in the value object. The
182
+ # order in which mappings are defined determines the order in which attributes are sent to the
183
+ # value class constructor.
184
+ # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
185
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
186
+ # mapped attributes.
187
+ # This defaults to +false+.
188
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
189
+ # is called to initialize the value object. The constructor is passed all of the mapped attributes,
190
+ # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
191
+ # to instantiate a <tt>:class_name</tt> object.
192
+ # The default is <tt>:new</tt>.
193
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
194
+ # or a Proc that is called when a new value is assigned to the value object. The converter is
195
+ # passed the single value that is used in the assignment and is only called if the new value is
196
+ # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter
197
+ # can return nil to skip the assignment.
116
198
  #
117
199
  # Option examples:
118
- # composed_of :temperature, :mapping => %w(reading celsius)
119
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
120
- # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
200
+ # composed_of :temperature, mapping: %w(reading celsius)
201
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount),
202
+ # converter: Proc.new { |balance| balance.to_money }
203
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
204
+ # composed_of :gps_location
205
+ # composed_of :gps_location, allow_nil: true
206
+ # composed_of :ip_address,
207
+ # class_name: 'IPAddr',
208
+ # mapping: %w(ip to_i),
209
+ # constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
210
+ # converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
211
+ #
121
212
  def composed_of(part_id, options = {})
122
- validate_options([ :class_name, :mapping ], options.keys)
213
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
123
214
 
124
215
  name = part_id.id2name
125
- class_name = options[:class_name] || name_to_class_name(name)
126
- mapping = options[:mapping]
216
+ class_name = options[:class_name] || name.camelize
217
+ mapping = options[:mapping] || [ name, name ]
218
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
219
+ allow_nil = options[:allow_nil] || false
220
+ constructor = options[:constructor] || :new
221
+ converter = options[:converter]
222
+
223
+ reader_method(name, class_name, mapping, allow_nil, constructor)
224
+ writer_method(name, class_name, mapping, allow_nil, converter)
127
225
 
128
- reader_method(name, class_name, mapping)
129
- writer_method(name, class_name, mapping)
226
+ create_reflection(:composed_of, part_id, nil, options, self)
130
227
  end
131
228
 
132
229
  private
133
- # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
134
- def validate_options(valid_option_keys, supplied_option_keys)
135
- unknown_option_keys = supplied_option_keys - valid_option_keys
136
- raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
230
+ def reader_method(name, class_name, mapping, allow_nil, constructor)
231
+ define_method(name) do
232
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
233
+ attrs = mapping.collect {|pair| read_attribute(pair.first)}
234
+ object = constructor.respond_to?(:call) ?
235
+ constructor.call(*attrs) :
236
+ class_name.constantize.send(constructor, *attrs)
237
+ @aggregation_cache[name] = object
238
+ end
239
+ @aggregation_cache[name]
240
+ end
137
241
  end
138
242
 
139
- def name_to_class_name(name)
140
- name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
141
- end
142
-
143
- def reader_method(name, class_name, mapping)
144
- module_eval <<-end_eval
145
- def #{name}(force_reload = false)
146
- if @#{name}.nil? || force_reload
147
- @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
148
- end
149
-
150
- return @#{name}
243
+ def writer_method(name, class_name, mapping, allow_nil, converter)
244
+ define_method("#{name}=") do |part|
245
+ klass = class_name.constantize
246
+ unless part.is_a?(klass) || converter.nil? || part.nil?
247
+ part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
151
248
  end
152
- end_eval
153
- end
154
-
155
- def writer_method(name, class_name, mapping)
156
- module_eval <<-end_eval
157
- def #{name}=(part)
158
- @#{name} = part.freeze
159
- #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
249
+
250
+ if part.nil? && allow_nil
251
+ mapping.each { |pair| self[pair.first] = nil }
252
+ @aggregation_cache[name] = nil
253
+ else
254
+ mapping.each { |pair| self[pair.first] = part.send(pair.last) }
255
+ @aggregation_cache[name] = part.freeze
160
256
  end
161
- end_eval
257
+ end
162
258
  end
163
259
  end
164
260
  end
@@ -0,0 +1,76 @@
1
+ require 'active_support/core_ext/string/conversions'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
6
+ # ActiveRecord::Associations::ThroughAssociationScope
7
+ class AliasTracker # :nodoc:
8
+ attr_reader :aliases, :table_joins, :connection
9
+
10
+ # table_joins is an array of arel joins which might conflict with the aliases we assign here
11
+ def initialize(connection = Base.connection, table_joins = [])
12
+ @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
13
+ @table_joins = table_joins
14
+ @connection = connection
15
+ end
16
+
17
+ def aliased_table_for(table_name, aliased_name = nil)
18
+ table_alias = aliased_name_for(table_name, aliased_name)
19
+
20
+ if table_alias == table_name
21
+ Arel::Table.new(table_name)
22
+ else
23
+ Arel::Table.new(table_name).alias(table_alias)
24
+ end
25
+ end
26
+
27
+ def aliased_name_for(table_name, aliased_name = nil)
28
+ aliased_name ||= table_name
29
+
30
+ if aliases[table_name].zero?
31
+ # If it's zero, we can have our table_name
32
+ aliases[table_name] = 1
33
+ table_name
34
+ else
35
+ # Otherwise, we need to use an alias
36
+ aliased_name = connection.table_alias_for(aliased_name)
37
+
38
+ # Update the count
39
+ aliases[aliased_name] += 1
40
+
41
+ if aliases[aliased_name] > 1
42
+ "#{truncate(aliased_name)}_#{aliases[aliased_name]}"
43
+ else
44
+ aliased_name
45
+ end
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def initial_count_for(name)
52
+ return 0 if Arel::Table === table_joins
53
+
54
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
55
+ quoted_name = connection.quote_table_name(name).downcase
56
+
57
+ counts = table_joins.map do |join|
58
+ if join.is_a?(Arel::Nodes::StringJoin)
59
+ # Table names + table aliases
60
+ join.left.downcase.scan(
61
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
62
+ ).size
63
+ else
64
+ join.left.table_name == name ? 1 : 0
65
+ end
66
+ end
67
+
68
+ counts.sum
69
+ end
70
+
71
+ def truncate(name)
72
+ name.slice(0, connection.table_alias_length - 2)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,248 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # = Active Record Associations
6
+ #
7
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
8
+ #
9
+ # Association
10
+ # SingularAssociation
11
+ # HasOneAssociation
12
+ # HasOneThroughAssociation + ThroughAssociation
13
+ # BelongsToAssociation
14
+ # BelongsToPolymorphicAssociation
15
+ # CollectionAssociation
16
+ # HasAndBelongsToManyAssociation
17
+ # HasManyAssociation
18
+ # HasManyThroughAssociation + ThroughAssociation
19
+ class Association #:nodoc:
20
+ attr_reader :owner, :target, :reflection
21
+
22
+ delegate :options, :to => :reflection
23
+
24
+ def initialize(owner, reflection)
25
+ reflection.check_validity!
26
+
27
+ @owner, @reflection = owner, reflection
28
+
29
+ reset
30
+ reset_scope
31
+ end
32
+
33
+ # Returns the name of the table of the associated class:
34
+ #
35
+ # post.comments.aliased_table_name # => "comments"
36
+ #
37
+ def aliased_table_name
38
+ klass.table_name
39
+ end
40
+
41
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
42
+ def reset
43
+ @loaded = false
44
+ @target = nil
45
+ @stale_state = nil
46
+ end
47
+
48
+ # Reloads the \target and returns +self+ on success.
49
+ def reload
50
+ reset
51
+ reset_scope
52
+ load_target
53
+ self unless target.nil?
54
+ end
55
+
56
+ # Has the \target been already \loaded?
57
+ def loaded?
58
+ @loaded
59
+ end
60
+
61
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
62
+ def loaded!
63
+ @loaded = true
64
+ @stale_state = stale_state
65
+ end
66
+
67
+ # The target is stale if the target no longer points to the record(s) that the
68
+ # relevant foreign_key(s) refers to. If stale, the association accessor method
69
+ # on the owner will reload the target. It's up to subclasses to implement the
70
+ # state_state method if relevant.
71
+ #
72
+ # Note that if the target has not been loaded, it is not considered stale.
73
+ def stale_target?
74
+ loaded? && @stale_state != stale_state
75
+ end
76
+
77
+ # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
78
+ def target=(target)
79
+ @target = target
80
+ loaded!
81
+ end
82
+
83
+ def scope
84
+ target_scope.merge(association_scope)
85
+ end
86
+
87
+ def scoped
88
+ ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
89
+ scope
90
+ end
91
+
92
+ # The scope for this association.
93
+ #
94
+ # Note that the association_scope is merged into the target_scope only when the
95
+ # scope method is called. This is because at that point the call may be surrounded
96
+ # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
97
+ # actually gets built.
98
+ def association_scope
99
+ if klass
100
+ @association_scope ||= AssociationScope.new(self).scope
101
+ end
102
+ end
103
+
104
+ def reset_scope
105
+ @association_scope = nil
106
+ end
107
+
108
+ # Set the inverse association, if possible
109
+ def set_inverse_instance(record)
110
+ if record && invertible_for?(record)
111
+ inverse = record.association(inverse_reflection_for(record).name)
112
+ inverse.target = owner
113
+ end
114
+ end
115
+
116
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
117
+ # polymorphic_type field on the owner.
118
+ def klass
119
+ reflection.klass
120
+ end
121
+
122
+ # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
123
+ # through association's scope)
124
+ def target_scope
125
+ klass.all
126
+ end
127
+
128
+ # Loads the \target if needed and returns it.
129
+ #
130
+ # This method is abstract in the sense that it relies on +find_target+,
131
+ # which is expected to be provided by descendants.
132
+ #
133
+ # If the \target is already \loaded it is just returned. Thus, you can call
134
+ # +load_target+ unconditionally to get the \target.
135
+ #
136
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
137
+ # not reraised. The proxy is \reset and +nil+ is the return value.
138
+ def load_target
139
+ @target = find_target if (@stale_state && stale_target?) || find_target?
140
+
141
+ loaded! unless loaded?
142
+ target
143
+ rescue ActiveRecord::RecordNotFound
144
+ reset
145
+ end
146
+
147
+ def interpolate(sql, record = nil)
148
+ if sql.respond_to?(:to_proc)
149
+ owner.instance_exec(record, &sql)
150
+ else
151
+ sql
152
+ end
153
+ end
154
+
155
+ # We can't dump @reflection since it contains the scope proc
156
+ def marshal_dump
157
+ ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
158
+ [@reflection.name, ivars]
159
+ end
160
+
161
+ def marshal_load(data)
162
+ reflection_name, ivars = data
163
+ ivars.each { |name, val| instance_variable_set(name, val) }
164
+ @reflection = @owner.class.reflect_on_association(reflection_name)
165
+ end
166
+
167
+ def initialize_attributes(record) #:nodoc:
168
+ skip_assign = [reflection.foreign_key, reflection.type].compact
169
+ attributes = create_scope.except(*(record.changed - skip_assign))
170
+ record.assign_attributes(attributes)
171
+ set_inverse_instance(record)
172
+ end
173
+
174
+ private
175
+
176
+ def find_target?
177
+ !loaded? && (!owner.new_record? || foreign_key_present?) && klass
178
+ end
179
+
180
+ def creation_attributes
181
+ attributes = {}
182
+
183
+ if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
184
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
185
+
186
+ if reflection.options[:as]
187
+ attributes[reflection.type] = owner.class.base_class.name
188
+ end
189
+ end
190
+
191
+ attributes
192
+ end
193
+
194
+ # Sets the owner attributes on the given record
195
+ def set_owner_attributes(record)
196
+ creation_attributes.each { |key, value| record[key] = value }
197
+ end
198
+
199
+ # Should be true if there is a foreign key present on the owner which
200
+ # references the target. This is used to determine whether we can load
201
+ # the target if the owner is currently a new record (and therefore
202
+ # without a key).
203
+ #
204
+ # Currently implemented by belongs_to (vanilla and polymorphic) and
205
+ # has_one/has_many :through associations which go through a belongs_to
206
+ def foreign_key_present?
207
+ false
208
+ end
209
+
210
+ # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
211
+ # the kind of the class of the associated objects. Meant to be used as
212
+ # a sanity check when you are about to assign an associated record.
213
+ def raise_on_type_mismatch!(record)
214
+ unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
215
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
216
+ raise ActiveRecord::AssociationTypeMismatch, message
217
+ end
218
+ end
219
+
220
+ # Can be redefined by subclasses, notably polymorphic belongs_to
221
+ # The record parameter is necessary to support polymorphic inverses as we must check for
222
+ # the association in the specific class of the record.
223
+ def inverse_reflection_for(record)
224
+ reflection.inverse_of
225
+ end
226
+
227
+ # Returns true if inverse association on the given record needs to be set.
228
+ # This method is redefined by subclasses.
229
+ def invertible_for?(record)
230
+ inverse_reflection_for(record)
231
+ end
232
+
233
+ # This should be implemented to return the values of the relevant key(s) on the owner,
234
+ # so that when stale_state is different from the value stored on the last find_target,
235
+ # the target is stale.
236
+ #
237
+ # This is only relevant to certain associations, which is why it returns nil by default.
238
+ def stale_state
239
+ end
240
+
241
+ def build_record(attributes)
242
+ reflection.build_association(attributes) do |record|
243
+ initialize_attributes(record)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end