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.
- data/CHANGELOG +32 -6
- data/README +0 -0
- data/Rakefile +4 -5
- data/lib/active_record.rb +11 -10
- data/lib/active_record/aggregations.rb +110 -38
- data/lib/active_record/association_preload.rb +104 -15
- data/lib/active_record/associations.rb +427 -212
- data/lib/active_record/associations/association_collection.rb +101 -16
- data/lib/active_record/associations/association_proxy.rb +65 -13
- data/lib/active_record/associations/belongs_to_association.rb +2 -2
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -3
- data/lib/active_record/associations/has_many_association.rb +28 -28
- data/lib/active_record/associations/has_many_through_association.rb +21 -19
- data/lib/active_record/associations/has_one_association.rb +24 -7
- data/lib/active_record/associations/has_one_through_association.rb +3 -4
- data/lib/active_record/attribute_methods.rb +13 -5
- data/lib/active_record/base.rb +435 -212
- data/lib/active_record/calculations.rb +12 -5
- data/lib/active_record/callbacks.rb +28 -9
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +355 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +42 -215
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -5
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +48 -7
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +10 -4
- data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -26
- data/lib/active_record/connection_adapters/mysql_adapter.rb +71 -45
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +155 -84
- data/lib/active_record/dirty.rb +25 -7
- data/lib/active_record/dynamic_finder_match.rb +41 -0
- data/lib/active_record/fixtures.rb +10 -9
- data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
- data/lib/active_record/locale/en.yml +54 -0
- data/lib/active_record/migration.rb +47 -10
- data/lib/active_record/named_scope.rb +29 -16
- data/lib/active_record/reflection.rb +118 -54
- data/lib/active_record/schema_dumper.rb +13 -7
- data/lib/active_record/test_case.rb +18 -5
- data/lib/active_record/transactions.rb +89 -34
- data/lib/active_record/validations.rb +270 -180
- data/lib/active_record/version.rb +1 -1
- data/test/cases/active_schema_test_mysql.rb +5 -0
- data/test/cases/adapter_test.rb +6 -0
- data/test/cases/aggregations_test.rb +39 -0
- data/test/cases/associations/belongs_to_associations_test.rb +10 -0
- data/test/cases/associations/eager_load_nested_include_test.rb +30 -12
- data/test/cases/associations/eager_test.rb +54 -5
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +77 -10
- data/test/cases/associations/has_many_associations_test.rb +74 -7
- data/test/cases/associations/has_many_through_associations_test.rb +50 -3
- data/test/cases/associations/has_one_associations_test.rb +17 -0
- data/test/cases/associations/has_one_through_associations_test.rb +49 -1
- data/test/cases/associations_test.rb +0 -0
- data/test/cases/attribute_methods_test.rb +59 -4
- data/test/cases/base_test.rb +93 -21
- data/test/cases/binary_test.rb +1 -5
- data/test/cases/calculations_test.rb +5 -0
- data/test/cases/callbacks_observers_test.rb +38 -0
- data/test/cases/connection_test_mysql.rb +1 -1
- data/test/cases/defaults_test.rb +32 -1
- data/test/cases/deprecated_finder_test.rb +0 -0
- data/test/cases/dirty_test.rb +13 -0
- data/test/cases/finder_test.rb +162 -12
- data/test/cases/fixtures_test.rb +32 -3
- data/test/cases/helper.rb +15 -0
- data/test/cases/i18n_test.rb +41 -0
- data/test/cases/inheritance_test.rb +2 -2
- data/test/cases/lifecycle_test.rb +0 -0
- data/test/cases/locking_test.rb +4 -9
- data/test/cases/method_scoping_test.rb +109 -2
- data/test/cases/migration_test.rb +43 -8
- data/test/cases/multiple_db_test.rb +25 -0
- data/test/cases/named_scope_test.rb +74 -0
- data/test/cases/pooled_connections_test.rb +103 -0
- data/test/cases/readonly_test.rb +0 -0
- data/test/cases/reflection_test.rb +11 -3
- data/test/cases/reload_models_test.rb +20 -0
- data/test/cases/sanitize_test.rb +25 -0
- data/test/cases/schema_authorization_test_postgresql.rb +2 -2
- data/test/cases/transactions_test.rb +62 -12
- data/test/cases/unconnected_test.rb +0 -0
- data/test/cases/validations_i18n_test.rb +921 -0
- data/test/cases/validations_test.rb +44 -33
- data/test/connections/native_mysql/connection.rb +1 -3
- data/test/fixtures/companies.yml +1 -0
- data/test/fixtures/customers.yml +10 -1
- data/test/fixtures/fixture_database.sqlite3 +0 -0
- data/test/fixtures/fixture_database_2.sqlite3 +0 -0
- data/test/fixtures/organizations.yml +5 -0
- data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
- data/test/models/author.rb +3 -0
- data/test/models/category.rb +3 -0
- data/test/models/club.rb +6 -0
- data/test/models/company.rb +25 -1
- data/test/models/customer.rb +19 -1
- data/test/models/member.rb +2 -0
- data/test/models/member_detail.rb +4 -0
- data/test/models/organization.rb +4 -0
- data/test/models/parrot.rb +1 -0
- data/test/models/post.rb +3 -0
- data/test/models/reply.rb +0 -0
- data/test/models/topic.rb +3 -0
- data/test/schema/schema.rb +12 -1
- metadata +22 -10
- data/lib/active_record/vendor/mysql.rb +0 -1214
- data/test/cases/adapter_test_sqlserver.rb +0 -95
- data/test/cases/table_name_test_sqlserver.rb +0 -23
- data/test/cases/threaded_connections_test.rb +0 -48
- data/test/schema/sqlserver_specific_schema.rb +0 -5
data/CHANGELOG
CHANGED
@@ -1,23 +1,49 @@
|
|
1
|
-
*2.
|
1
|
+
*2.2 (November 21st, 2008)*
|
2
2
|
|
3
|
-
*
|
3
|
+
* Ensure indices don't flip order in schema.rb #1266 [Jordi Bunster]
|
4
4
|
|
5
|
-
*
|
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
|
-
*
|
9
|
+
* Add Model#delete instance method, similar to Model.delete class method. #1086 [Hongli Lai]
|
9
10
|
|
10
|
-
*
|
11
|
+
* MySQL: cope with quirky default values for not-null text columns. #1043 [Frederick Cheung]
|
11
12
|
|
12
|
-
*
|
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
|
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.
|
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("
|
230
|
-
`ssh
|
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"
|
data/lib/active_record.rb
CHANGED
@@ -21,17 +21,14 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
23
|
|
24
|
-
|
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.
|
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#
|
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>
|
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> -
|
129
|
-
#
|
130
|
-
#
|
131
|
-
# attributes are
|
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
|
-
#
|
135
|
-
# <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
|
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]
|
149
|
-
mapping = options[:mapping]
|
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]
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
146
|
-
|
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.
|
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
|