activerecord 2.1.2 → 2.2.2

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 (110) hide show
  1. data/CHANGELOG +32 -6
  2. data/README +0 -0
  3. data/Rakefile +4 -5
  4. data/lib/active_record.rb +11 -10
  5. data/lib/active_record/aggregations.rb +110 -38
  6. data/lib/active_record/association_preload.rb +104 -15
  7. data/lib/active_record/associations.rb +427 -212
  8. data/lib/active_record/associations/association_collection.rb +101 -16
  9. data/lib/active_record/associations/association_proxy.rb +65 -13
  10. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -3
  13. data/lib/active_record/associations/has_many_association.rb +28 -28
  14. data/lib/active_record/associations/has_many_through_association.rb +21 -19
  15. data/lib/active_record/associations/has_one_association.rb +24 -7
  16. data/lib/active_record/associations/has_one_through_association.rb +3 -4
  17. data/lib/active_record/attribute_methods.rb +13 -5
  18. data/lib/active_record/base.rb +435 -212
  19. data/lib/active_record/calculations.rb +12 -5
  20. data/lib/active_record/callbacks.rb +28 -9
  21. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +355 -0
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +42 -215
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -5
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -1
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +48 -7
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +10 -4
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -26
  28. data/lib/active_record/connection_adapters/mysql_adapter.rb +71 -45
  29. data/lib/active_record/connection_adapters/postgresql_adapter.rb +155 -84
  30. data/lib/active_record/dirty.rb +25 -7
  31. data/lib/active_record/dynamic_finder_match.rb +41 -0
  32. data/lib/active_record/fixtures.rb +10 -9
  33. data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
  34. data/lib/active_record/locale/en.yml +54 -0
  35. data/lib/active_record/migration.rb +47 -10
  36. data/lib/active_record/named_scope.rb +29 -16
  37. data/lib/active_record/reflection.rb +118 -54
  38. data/lib/active_record/schema_dumper.rb +13 -7
  39. data/lib/active_record/test_case.rb +18 -5
  40. data/lib/active_record/transactions.rb +89 -34
  41. data/lib/active_record/validations.rb +270 -180
  42. data/lib/active_record/version.rb +1 -1
  43. data/test/cases/active_schema_test_mysql.rb +5 -0
  44. data/test/cases/adapter_test.rb +6 -0
  45. data/test/cases/aggregations_test.rb +39 -0
  46. data/test/cases/associations/belongs_to_associations_test.rb +10 -0
  47. data/test/cases/associations/eager_load_nested_include_test.rb +30 -12
  48. data/test/cases/associations/eager_test.rb +54 -5
  49. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +77 -10
  50. data/test/cases/associations/has_many_associations_test.rb +74 -7
  51. data/test/cases/associations/has_many_through_associations_test.rb +50 -3
  52. data/test/cases/associations/has_one_associations_test.rb +17 -0
  53. data/test/cases/associations/has_one_through_associations_test.rb +49 -1
  54. data/test/cases/associations_test.rb +0 -0
  55. data/test/cases/attribute_methods_test.rb +59 -4
  56. data/test/cases/base_test.rb +93 -21
  57. data/test/cases/binary_test.rb +1 -5
  58. data/test/cases/calculations_test.rb +5 -0
  59. data/test/cases/callbacks_observers_test.rb +38 -0
  60. data/test/cases/connection_test_mysql.rb +1 -1
  61. data/test/cases/defaults_test.rb +32 -1
  62. data/test/cases/deprecated_finder_test.rb +0 -0
  63. data/test/cases/dirty_test.rb +13 -0
  64. data/test/cases/finder_test.rb +162 -12
  65. data/test/cases/fixtures_test.rb +32 -3
  66. data/test/cases/helper.rb +15 -0
  67. data/test/cases/i18n_test.rb +41 -0
  68. data/test/cases/inheritance_test.rb +2 -2
  69. data/test/cases/lifecycle_test.rb +0 -0
  70. data/test/cases/locking_test.rb +4 -9
  71. data/test/cases/method_scoping_test.rb +109 -2
  72. data/test/cases/migration_test.rb +43 -8
  73. data/test/cases/multiple_db_test.rb +25 -0
  74. data/test/cases/named_scope_test.rb +74 -0
  75. data/test/cases/pooled_connections_test.rb +103 -0
  76. data/test/cases/readonly_test.rb +0 -0
  77. data/test/cases/reflection_test.rb +11 -3
  78. data/test/cases/reload_models_test.rb +20 -0
  79. data/test/cases/sanitize_test.rb +25 -0
  80. data/test/cases/schema_authorization_test_postgresql.rb +2 -2
  81. data/test/cases/transactions_test.rb +62 -12
  82. data/test/cases/unconnected_test.rb +0 -0
  83. data/test/cases/validations_i18n_test.rb +921 -0
  84. data/test/cases/validations_test.rb +44 -33
  85. data/test/connections/native_mysql/connection.rb +1 -3
  86. data/test/fixtures/companies.yml +1 -0
  87. data/test/fixtures/customers.yml +10 -1
  88. data/test/fixtures/fixture_database.sqlite3 +0 -0
  89. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  90. data/test/fixtures/organizations.yml +5 -0
  91. data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
  92. data/test/models/author.rb +3 -0
  93. data/test/models/category.rb +3 -0
  94. data/test/models/club.rb +6 -0
  95. data/test/models/company.rb +25 -1
  96. data/test/models/customer.rb +19 -1
  97. data/test/models/member.rb +2 -0
  98. data/test/models/member_detail.rb +4 -0
  99. data/test/models/organization.rb +4 -0
  100. data/test/models/parrot.rb +1 -0
  101. data/test/models/post.rb +3 -0
  102. data/test/models/reply.rb +0 -0
  103. data/test/models/topic.rb +3 -0
  104. data/test/schema/schema.rb +12 -1
  105. metadata +22 -10
  106. data/lib/active_record/vendor/mysql.rb +0 -1214
  107. data/test/cases/adapter_test_sqlserver.rb +0 -95
  108. data/test/cases/table_name_test_sqlserver.rb +0 -23
  109. data/test/cases/threaded_connections_test.rb +0 -48
  110. data/test/schema/sqlserver_specific_schema.rb +0 -5
data/CHANGELOG CHANGED
@@ -1,23 +1,49 @@
1
- *2.1.2 (October 23rd, 2008)*
1
+ *2.2 (November 21st, 2008)*
2
2
 
3
- * Added SQL escaping for :limit and :offset in MySQL [Jonathan Wiess]
3
+ * Ensure indices don't flip order in schema.rb #1266 [Jordi Bunster]
4
4
 
5
- * Multiparameter attributes skip time zone conversion for time-only columns #1030 [Geoff Buesing]
5
+ * Fixed that serialized strings should never be type-casted (i.e. turning "Yes" to a boolean) #857 [Andreas Korth]
6
6
 
7
+ * Skip collection ids reader optimization if using :finder_sql [Jeremy Kemper]
7
8
 
8
- *2.1.1 (September 4th, 2008)*
9
+ * Add Model#delete instance method, similar to Model.delete class method. #1086 [Hongli Lai]
9
10
 
10
- * Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
11
+ * MySQL: cope with quirky default values for not-null text columns. #1043 [Frederick Cheung]
11
12
 
12
- * Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
13
+ * Multiparameter attributes skip time zone conversion for time-only columns [#1030 state:resolved] [Geoff Buesing]
14
+
15
+ * Base.skip_time_zone_conversion_for_attributes uses class_inheritable_accessor, so that subclasses don't overwrite Base [#346 state:resolved] [miloops]
16
+
17
+ * Added find_last_by dynamic finder #762 [miloops]
18
+
19
+ * Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 [Hongli Lai]
20
+
21
+ * Changed benchmarks to be reported in milliseconds [DHH]
22
+
23
+ * Connection pooling. #936 [Nick Sieger]
24
+
25
+ * Merge scoped :joins together instead of overwriting them. May expose scoping bugs in your code! #501 [Andrew White]
26
+
27
+ * before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
28
+
29
+ * Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
30
+
31
+ * Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
13
32
 
14
33
  * change_column_default preserves the not-null constraint. #617 [Tarmo Tänav]
15
34
 
35
+ * Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
36
+
16
37
  * Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example :
17
38
 
18
39
  # Ensure essay contains at least 100 words.
19
40
  validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
20
41
 
42
+ * Allow conditions on multiple tables to be specified using hash. [Pratik Naik]. Example:
43
+
44
+ User.all :joins => :items, :conditions => { :age => 10, :items => { :color => 'black' } }
45
+ Item.first :conditions => { :items => { :color => 'red' } }
46
+
21
47
  * Always treat integer :limit as byte length. #420 [Tarmo Tänav]
22
48
 
23
49
  * Partial updates don't update lock_version if nothing changed. #426 [Daniel Morrison]
data/README CHANGED
File without changes
data/Rakefile CHANGED
@@ -5,7 +5,6 @@ require 'rake/rdoctask'
5
5
  require 'rake/packagetask'
6
6
  require 'rake/gempackagetask'
7
7
  require 'rake/contrib/sshpublisher'
8
- require 'rake/contrib/rubyforgepublisher'
9
8
 
10
9
  require File.join(File.dirname(__FILE__), 'lib', 'active_record', 'version')
11
10
  require File.expand_path(File.dirname(__FILE__)) + "/test/config"
@@ -31,7 +30,7 @@ desc 'Run mysql, sqlite, and postgresql tests by default'
31
30
  task :default => :test
32
31
 
33
32
  desc 'Run mysql, sqlite, and postgresql tests'
34
- task :test => %w(test_mysql test_sqlite test_sqlite3 test_postgresql)
33
+ task :test => %w(test_mysql test_sqlite3 test_postgresql)
35
34
 
36
35
  for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
37
36
  Rake::TestTask.new("test_#{adapter}") { |t|
@@ -172,7 +171,7 @@ spec = Gem::Specification.new do |s|
172
171
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
173
172
  end
174
173
 
175
- s.add_dependency('activesupport', '= 2.1.2' + PKG_BUILD)
174
+ s.add_dependency('activesupport', '= 2.2.2' + PKG_BUILD)
176
175
 
177
176
  s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
178
177
  s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
@@ -226,8 +225,8 @@ end
226
225
 
227
226
  desc "Publish the beta gem"
228
227
  task :pgem => [:package] do
229
- Rake::SshFilePublisher.new("david@greed.loudthinking.com", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
230
- `ssh david@greed.loudthinking.com '/u/sites/gems/gemupdate.sh'`
228
+ Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
229
+ `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
231
230
  end
232
231
 
233
232
  desc "Publish the API documentation"
@@ -21,17 +21,14 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- $:.unshift(File.dirname(__FILE__)) unless
25
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
-
27
- active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
28
- if File.exist?(active_support_path)
29
- $:.unshift active_support_path
30
- require 'active_support'
31
- else
32
- require 'rubygems'
33
- gem 'activesupport'
24
+ begin
34
25
  require 'active_support'
26
+ rescue LoadError
27
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
28
+ if File.directory?(activesupport_path)
29
+ $:.unshift activesupport_path
30
+ require 'active_support'
31
+ end
35
32
  end
36
33
 
37
34
  require 'active_record/base'
@@ -54,6 +51,7 @@ require 'active_record/calculations'
54
51
  require 'active_record/serialization'
55
52
  require 'active_record/attribute_methods'
56
53
  require 'active_record/dirty'
54
+ require 'active_record/dynamic_finder_match'
57
55
 
58
56
  ActiveRecord::Base.class_eval do
59
57
  extend ActiveRecord::QueryCache
@@ -78,3 +76,6 @@ end
78
76
  require 'active_record/connection_adapters/abstract_adapter'
79
77
 
80
78
  require 'active_record/schema_dumper'
79
+
80
+ require 'active_record/i18n_interpolation_deprecation'
81
+ I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
@@ -10,10 +10,10 @@ module ActiveRecord
10
10
  end unless self.new_record?
11
11
  end
12
12
 
13
- # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
13
+ # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
14
14
  # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
15
- # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
16
- # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
15
+ # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
16
+ # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
17
17
  # and how it can be turned back into attributes (when the entity is saved to the database). Example:
18
18
  #
19
19
  # class Customer < ActiveRecord::Base
@@ -30,10 +30,10 @@ module ActiveRecord
30
30
  # class Money
31
31
  # include Comparable
32
32
  # attr_reader :amount, :currency
33
- # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
34
- #
35
- # def initialize(amount, currency = "USD")
36
- # @amount, @currency = amount, currency
33
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
34
+ #
35
+ # def initialize(amount, currency = "USD")
36
+ # @amount, @currency = amount, currency
37
37
  # end
38
38
  #
39
39
  # def exchange_to(other_currency)
@@ -56,26 +56,26 @@ module ActiveRecord
56
56
  #
57
57
  # class Address
58
58
  # attr_reader :street, :city
59
- # def initialize(street, city)
60
- # @street, @city = street, city
59
+ # def initialize(street, city)
60
+ # @street, @city = street, city
61
61
  # end
62
62
  #
63
- # def close_to?(other_address)
64
- # city == other_address.city
63
+ # def close_to?(other_address)
64
+ # city == other_address.city
65
65
  # end
66
66
  #
67
67
  # def ==(other_address)
68
68
  # city == other_address.city && street == other_address.street
69
69
  # end
70
70
  # end
71
- #
71
+ #
72
72
  # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
73
73
  # composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
74
74
  # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
75
75
  #
76
76
  # customer.balance = Money.new(20) # sets the Money value object and the attribute
77
77
  # customer.balance # => Money value object
78
- # customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
78
+ # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
79
79
  # customer.balance > Money.new(10) # => true
80
80
  # customer.balance == Money.new(20) # => true
81
81
  # customer.balance < Money.new(5) # => false
@@ -87,8 +87,8 @@ module ActiveRecord
87
87
  # customer.address_city = "Copenhagen"
88
88
  # customer.address # => Address.new("Hyancintvej", "Copenhagen")
89
89
  # customer.address = Address.new("May Street", "Chicago")
90
- # customer.address_street # => "May Street"
91
- # customer.address_city # => "Chicago"
90
+ # customer.address_street # => "May Street"
91
+ # customer.address_city # => "Chicago"
92
92
  #
93
93
  # == Writing value objects
94
94
  #
@@ -99,16 +99,55 @@ module ActiveRecord
99
99
  # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
100
100
  #
101
101
  # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
102
- # creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchanged_to method that
102
+ # creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchange_to method that
103
103
  # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
104
104
  # changed through means other than the writer method.
105
105
  #
106
- # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
106
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
107
107
  # change it afterwards will result in a ActiveSupport::FrozenObjectError.
108
- #
108
+ #
109
109
  # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
110
110
  # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
111
111
  #
112
+ # == Custom constructors and converters
113
+ #
114
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value class passing each of the
115
+ # mapped attributes, in the order specified by the <tt>:mapping</tt> option, as arguments. If the value class doesn't support
116
+ # this convention then +composed_of+ allows a custom constructor to be specified.
117
+ #
118
+ # When a new value is assigned to the value object the default assumption is that the new value is an instance of the value
119
+ # class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if
120
+ # necessary.
121
+ #
122
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the
123
+ # NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it
124
+ # expects a CIDR address string as a parameter. New values can be assigned to the value object using either another
125
+ # NetAddr::CIDR object, a string or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to
126
+ # meet these requirements:
127
+ #
128
+ # class NetworkResource < ActiveRecord::Base
129
+ # composed_of :cidr,
130
+ # :class_name => 'NetAddr::CIDR',
131
+ # :mapping => [ %w(network_address network), %w(cidr_range bits) ],
132
+ # :allow_nil => true,
133
+ # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
134
+ # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
135
+ # end
136
+ #
137
+ # # This calls the :constructor
138
+ # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
139
+ #
140
+ # # These assignments will both use the :converter
141
+ # network_resource.cidr = [ '192.168.2.1', 8 ]
142
+ # network_resource.cidr = '192.168.0.1/24'
143
+ #
144
+ # # This assignment won't use the :converter as the value is already an instance of the value class
145
+ # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
146
+ #
147
+ # # Saving and then reloading will use the :constructor on reload
148
+ # network_resource.save
149
+ # network_resource.reload
150
+ #
112
151
  # == Finding records by a value object
113
152
  #
114
153
  # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance
@@ -122,47 +161,71 @@ module ActiveRecord
122
161
  # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
123
162
  #
124
163
  # Options are:
125
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
164
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name can't be inferred
126
165
  # from the part id. So <tt>composed_of :address</tt> will by default be linked to the Address class, but
127
166
  # if the real class name is CompanyAddress, you'll have to specify it with this option.
128
- # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
129
- # to a constructor parameter on the value class.
130
- # * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
131
- # attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
167
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value object. Each mapping
168
+ # is represented as an array where the first item is the name of the entity attribute and the second item is the
169
+ # name the attribute in the value object. The order in which mappings are defined determine the order in which
170
+ # attributes are sent to the value class constructor.
171
+ # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
172
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes.
132
173
  # This defaults to +false+.
133
- #
134
- # An optional block can be passed to convert the argument that is passed to the writer method into an instance of
135
- # <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
174
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that is called to
175
+ # initialize the value object. The constructor is passed all of the mapped attributes, in the order that they
176
+ # are defined in the <tt>:mapping option</tt>, as arguments and uses them to instantiate a <tt>:class_name</tt> object.
177
+ # The default is <tt>:new</tt>.
178
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> or a Proc that is
179
+ # called when a new value is assigned to the value object. The converter is passed the single value that is used
180
+ # in the assignment and is only called if the new value is not an instance of <tt>:class_name</tt>.
136
181
  #
137
182
  # Option examples:
138
183
  # composed_of :temperature, :mapping => %w(reading celsius)
139
- # composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
184
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
140
185
  # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
141
186
  # composed_of :gps_location
142
187
  # composed_of :gps_location, :allow_nil => true
188
+ # composed_of :ip_address,
189
+ # :class_name => 'IPAddr',
190
+ # :mapping => %w(ip to_i),
191
+ # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
192
+ # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
143
193
  #
144
194
  def composed_of(part_id, options = {}, &block)
145
- options.assert_valid_keys(:class_name, :mapping, :allow_nil)
195
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
146
196
 
147
197
  name = part_id.id2name
148
- class_name = options[:class_name] || name.camelize
149
- mapping = options[:mapping] || [ name, name ]
198
+ class_name = options[:class_name] || name.camelize
199
+ mapping = options[:mapping] || [ name, name ]
150
200
  mapping = [ mapping ] unless mapping.first.is_a?(Array)
151
- allow_nil = options[:allow_nil] || false
201
+ allow_nil = options[:allow_nil] || false
202
+ constructor = options[:constructor] || :new
203
+ converter = options[:converter] || block
204
+
205
+ ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
206
+
207
+ reader_method(name, class_name, mapping, allow_nil, constructor)
208
+ writer_method(name, class_name, mapping, allow_nil, converter)
152
209
 
153
- reader_method(name, class_name, mapping, allow_nil)
154
- writer_method(name, class_name, mapping, allow_nil, block)
155
-
156
210
  create_reflection(:composed_of, part_id, options, self)
157
211
  end
158
212
 
159
213
  private
160
- def reader_method(name, class_name, mapping, allow_nil)
214
+ def reader_method(name, class_name, mapping, allow_nil, constructor)
161
215
  module_eval do
162
216
  define_method(name) do |*args|
163
217
  force_reload = args.first || false
164
218
  if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
165
- instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
219
+ attrs = mapping.collect {|pair| read_attribute(pair.first)}
220
+ object = case constructor
221
+ when Symbol
222
+ class_name.constantize.send(constructor, *attrs)
223
+ when Proc, Method
224
+ constructor.call(*attrs)
225
+ else
226
+ raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
227
+ end
228
+ instance_variable_set("@#{name}", object)
166
229
  end
167
230
  instance_variable_get("@#{name}")
168
231
  end
@@ -170,14 +233,23 @@ module ActiveRecord
170
233
 
171
234
  end
172
235
 
173
- def writer_method(name, class_name, mapping, allow_nil, conversion)
236
+ def writer_method(name, class_name, mapping, allow_nil, converter)
174
237
  module_eval do
175
238
  define_method("#{name}=") do |part|
176
239
  if part.nil? && allow_nil
177
240
  mapping.each { |pair| self[pair.first] = nil }
178
241
  instance_variable_set("@#{name}", nil)
179
242
  else
180
- part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
243
+ unless part.is_a?(class_name.constantize) || converter.nil?
244
+ part = case converter
245
+ when Symbol
246
+ class_name.constantize.send(converter, part)
247
+ when Proc, Method
248
+ converter.call(part)
249
+ else
250
+ raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
251
+ end
252
+ end
181
253
  mapping.each { |pair| self[pair.first] = part.send(pair.last) }
182
254
  instance_variable_set("@#{name}", part.freeze)
183
255
  end
@@ -1,14 +1,88 @@
1
1
  module ActiveRecord
2
+ # See ActiveRecord::AssociationPreload::ClassMethods for documentation.
2
3
  module AssociationPreload #:nodoc:
3
4
  def self.included(base)
4
5
  base.extend(ClassMethods)
5
6
  end
6
7
 
8
+ # Implements the details of eager loading of ActiveRecord associations.
9
+ # Application developers should not use this module directly.
10
+ #
11
+ # ActiveRecord::Base is extended with this module. The source code in
12
+ # ActiveRecord::Base references methods defined in this module.
13
+ #
14
+ # Note that 'eager loading' and 'preloading' are actually the same thing.
15
+ # However, there are two different eager loading strategies.
16
+ #
17
+ # The first one is by using table joins. This was only strategy available
18
+ # prior to Rails 2.1. Suppose that you have an Author model with columns
19
+ # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
20
+ # this strategy, ActiveRecord would try to retrieve all data for an author
21
+ # and all of its books via a single query:
22
+ #
23
+ # SELECT * FROM authors
24
+ # LEFT OUTER JOIN books ON authors.id = books.id
25
+ # WHERE authors.name = 'Ken Akamatsu'
26
+ #
27
+ # However, this could result in many rows that contain redundant data. After
28
+ # having received the first row, we already have enough data to instantiate
29
+ # the Author object. In all subsequent rows, only the data for the joined
30
+ # 'books' table is useful; the joined 'authors' data is just redundant, and
31
+ # processing this redundant data takes memory and CPU time. The problem
32
+ # quickly becomes worse and worse as the level of eager loading increases
33
+ # (i.e. if ActiveRecord is to eager load the associations' assocations as
34
+ # well).
35
+ #
36
+ # The second strategy is to use multiple database queries, one for each
37
+ # level of association. Since Rails 2.1, this is the default strategy. In
38
+ # situations where a table join is necessary (e.g. when the +:conditions+
39
+ # option references an association's column), it will fallback to the table
40
+ # join strategy.
41
+ #
42
+ # See also ActiveRecord::Associations::ClassMethods, which explains eager
43
+ # loading in a more high-level (application developer-friendly) manner.
7
44
  module ClassMethods
8
-
9
- # Loads the named associations for the activerecord record (or records) given
10
- # preload_options is passed only one level deep: don't pass to the child associations when associations is a Hash
11
45
  protected
46
+
47
+ # Eager loads the named associations for the given ActiveRecord record(s).
48
+ #
49
+ # In this description, 'association name' shall refer to the name passed
50
+ # to an association creation method. For example, a model that specifies
51
+ # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
52
+ # names +:author+ and +:buyers+.
53
+ #
54
+ # == Parameters
55
+ # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
56
+ # i.e. +records+ itself may also contain arrays of records. In any case,
57
+ # +preload_associations+ will preload the associations all records by
58
+ # flattening +records+.
59
+ #
60
+ # +associations+ specifies one or more associations that you want to
61
+ # preload. It may be:
62
+ # - a Symbol or a String which specifies a single association name. For
63
+ # example, specifiying +:books+ allows this method to preload all books
64
+ # for an Author.
65
+ # - an Array which specifies multiple association names. This array
66
+ # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
67
+ # allows this method to preload an author's avatar as well as all of his
68
+ # books.
69
+ # - a Hash which specifies multiple association names, as well as
70
+ # association names for the to-be-preloaded association objects. For
71
+ # example, specifying <tt>{ :author => :avatar }</tt> will preload a
72
+ # book's author, as well as that author's avatar.
73
+ #
74
+ # +:associations+ has the same format as the +:include+ option for
75
+ # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
76
+ #
77
+ # :books
78
+ # [ :books, :author ]
79
+ # { :author => :avatar }
80
+ # [ :books, { :author => :avatar } ]
81
+ #
82
+ # +preload_options+ contains options that will be passed to ActiveRecord#find
83
+ # (which is called under the hood for preloading records). But it is passed
84
+ # only one level deep in the +associations+ argument, i.e. it's not passed
85
+ # to the child associations when +associations+ is a Hash.
12
86
  def preload_associations(records, associations, preload_options={})
13
87
  records = [records].flatten.compact.uniq
14
88
  return if records.empty?
@@ -30,13 +104,19 @@ module ActiveRecord
30
104
 
31
105
  private
32
106
 
107
+ # Preloads a specific named association for the given records. This is
108
+ # called by +preload_associations+ as its base case.
33
109
  def preload_one_association(records, association, preload_options={})
34
110
  class_to_reflection = {}
35
111
  # Not all records have the same class, so group then preload
36
112
  # group on the reflection itself so that if various subclass share the same association then we do not split them
37
- # unncessarily
113
+ # unnecessarily
38
114
  records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
39
115
  raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
116
+
117
+ # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
118
+ # the following could call 'preload_belongs_to_association',
119
+ # 'preload_has_many_association', etc.
40
120
  send("preload_#{reflection.macro}_association", records, reflection, preload_options)
41
121
  end
42
122
  end
@@ -77,12 +157,17 @@ module ActiveRecord
77
157
  end
78
158
  end
79
159
 
80
- def construct_id_map(records)
160
+ # Given a collection of ActiveRecord objects, constructs a Hash which maps
161
+ # the objects' IDs to the relevant objects. Returns a 2-tuple
162
+ # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
163
+ # and +ids+ is an Array of record IDs.
164
+ def construct_id_map(records, primary_key=nil)
81
165
  id_to_record_map = {}
82
166
  ids = []
83
167
  records.each do |record|
84
- ids << record.id
85
- mapped_records = (id_to_record_map[record.id.to_s] ||= [])
168
+ primary_key ||= record.class.primary_key
169
+ ids << record[primary_key]
170
+ mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
86
171
  mapped_records << record
87
172
  end
88
173
  ids.uniq!
@@ -108,6 +193,7 @@ module ActiveRecord
108
193
  end
109
194
 
110
195
  def preload_has_one_association(records, reflection, preload_options={})
196
+ return if records.first.send("loaded_#{reflection.name}?")
111
197
  id_to_record_map, ids = construct_id_map(records)
112
198
  options = reflection.options
113
199
  records.each {|record| record.send("set_#{reflection.name}_target", nil)}
@@ -129,23 +215,25 @@ module ActiveRecord
129
215
  end
130
216
 
131
217
  def preload_has_many_association(records, reflection, preload_options={})
132
- id_to_record_map, ids = construct_id_map(records)
133
- records.each {|record| record.send(reflection.name).loaded}
218
+ return if records.first.send(reflection.name).loaded?
134
219
  options = reflection.options
135
220
 
221
+ primary_key_name = reflection.through_reflection_primary_key_name
222
+ id_to_record_map, ids = construct_id_map(records, primary_key_name)
223
+ records.each {|record| record.send(reflection.name).loaded}
224
+
136
225
  if options[:through]
137
226
  through_records = preload_through_records(records, reflection, options[:through])
138
227
  through_reflection = reflections[options[:through]]
139
- through_primary_key = through_reflection.primary_key_name
140
228
  unless through_records.empty?
141
229
  source = reflection.source_reflection.name
142
- #add conditions from reflection!
143
- through_records.first.class.preload_associations(through_records, source, reflection.options)
230
+ through_records.first.class.preload_associations(through_records, source, options)
144
231
  through_records.each do |through_record|
145
- add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
146
- reflection.name, through_record.send(source))
232
+ through_record_id = through_record[reflection.through_reflection_primary_key].to_s
233
+ add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
147
234
  end
148
235
  end
236
+
149
237
  else
150
238
  set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
151
239
  reflection.primary_key_name)
@@ -185,6 +273,7 @@ module ActiveRecord
185
273
  end
186
274
 
187
275
  def preload_belongs_to_association(records, reflection, preload_options={})
276
+ return if records.first.send("loaded_#{reflection.name}?")
188
277
  options = reflection.options
189
278
  primary_key_name = reflection.primary_key_name
190
279
 
@@ -223,7 +312,7 @@ module ActiveRecord
223
312
  table_name = klass.quoted_table_name
224
313
  primary_key = klass.primary_key
225
314
  column_type = klass.columns.detect{|c| c.name == primary_key}.type
226
- ids = id_map.keys.uniq.map do |id|
315
+ ids = id_map.keys.map do |id|
227
316
  if column_type == :integer
228
317
  id.to_i
229
318
  elsif column_type == :float