activerecord 3.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 (93) hide show
  1. data/CHANGELOG +6023 -0
  2. data/README.rdoc +222 -0
  3. data/examples/associations.png +0 -0
  4. data/examples/performance.rb +162 -0
  5. data/examples/simple.rb +14 -0
  6. data/lib/active_record.rb +124 -0
  7. data/lib/active_record/aggregations.rb +277 -0
  8. data/lib/active_record/association_preload.rb +403 -0
  9. data/lib/active_record/associations.rb +2254 -0
  10. data/lib/active_record/associations/association_collection.rb +562 -0
  11. data/lib/active_record/associations/association_proxy.rb +295 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +137 -0
  15. data/lib/active_record/associations/has_many_association.rb +128 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  17. data/lib/active_record/associations/has_one_association.rb +143 -0
  18. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  19. data/lib/active_record/associations/through_association_scope.rb +154 -0
  20. data/lib/active_record/attribute_methods.rb +60 -0
  21. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  22. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  23. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  24. data/lib/active_record/attribute_methods/query.rb +39 -0
  25. data/lib/active_record/attribute_methods/read.rb +116 -0
  26. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  27. data/lib/active_record/attribute_methods/write.rb +37 -0
  28. data/lib/active_record/autosave_association.rb +369 -0
  29. data/lib/active_record/base.rb +1867 -0
  30. data/lib/active_record/callbacks.rb +288 -0
  31. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  32. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  33. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +212 -0
  40. data/lib/active_record/connection_adapters/mysql_adapter.rb +643 -0
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1030 -0
  42. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  43. data/lib/active_record/connection_adapters/sqlite_adapter.rb +401 -0
  44. data/lib/active_record/counter_cache.rb +115 -0
  45. data/lib/active_record/dynamic_finder_match.rb +53 -0
  46. data/lib/active_record/dynamic_scope_match.rb +32 -0
  47. data/lib/active_record/errors.rb +172 -0
  48. data/lib/active_record/fixtures.rb +1008 -0
  49. data/lib/active_record/locale/en.yml +40 -0
  50. data/lib/active_record/locking/optimistic.rb +172 -0
  51. data/lib/active_record/locking/pessimistic.rb +55 -0
  52. data/lib/active_record/log_subscriber.rb +48 -0
  53. data/lib/active_record/migration.rb +617 -0
  54. data/lib/active_record/named_scope.rb +138 -0
  55. data/lib/active_record/nested_attributes.rb +417 -0
  56. data/lib/active_record/observer.rb +140 -0
  57. data/lib/active_record/persistence.rb +291 -0
  58. data/lib/active_record/query_cache.rb +36 -0
  59. data/lib/active_record/railtie.rb +91 -0
  60. data/lib/active_record/railties/controller_runtime.rb +38 -0
  61. data/lib/active_record/railties/databases.rake +512 -0
  62. data/lib/active_record/reflection.rb +403 -0
  63. data/lib/active_record/relation.rb +393 -0
  64. data/lib/active_record/relation/batches.rb +89 -0
  65. data/lib/active_record/relation/calculations.rb +286 -0
  66. data/lib/active_record/relation/finder_methods.rb +355 -0
  67. data/lib/active_record/relation/predicate_builder.rb +41 -0
  68. data/lib/active_record/relation/query_methods.rb +261 -0
  69. data/lib/active_record/relation/spawn_methods.rb +112 -0
  70. data/lib/active_record/schema.rb +59 -0
  71. data/lib/active_record/schema_dumper.rb +195 -0
  72. data/lib/active_record/serialization.rb +60 -0
  73. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  74. data/lib/active_record/session_store.rb +340 -0
  75. data/lib/active_record/test_case.rb +67 -0
  76. data/lib/active_record/timestamp.rb +88 -0
  77. data/lib/active_record/transactions.rb +356 -0
  78. data/lib/active_record/validations.rb +84 -0
  79. data/lib/active_record/validations/associated.rb +48 -0
  80. data/lib/active_record/validations/uniqueness.rb +185 -0
  81. data/lib/active_record/version.rb +9 -0
  82. data/lib/rails/generators/active_record.rb +27 -0
  83. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  84. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  85. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  86. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  87. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  88. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  89. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  90. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  91. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  92. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  93. metadata +224 -0
@@ -0,0 +1,277 @@
1
+ module ActiveRecord
2
+ # = Active Record Aggregations
3
+ module Aggregations # :nodoc:
4
+ extend ActiveSupport::Concern
5
+
6
+ def clear_aggregation_cache #:nodoc:
7
+ self.class.reflect_on_all_aggregations.to_a.each do |assoc|
8
+ instance_variable_set "@#{assoc.name}", nil
9
+ end unless self.new_record?
10
+ end
11
+
12
+ # Active Record implements aggregation through a macro-like class method called +composed_of+
13
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
14
+ # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
15
+ # to the macro adds a description of how the value objects are created from the attributes of
16
+ # the entity object (when the entity is initialized either as a new object or from finding an
17
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
18
+ # the database).
19
+ #
20
+ # class Customer < ActiveRecord::Base
21
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
22
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
23
+ # end
24
+ #
25
+ # The customer class now has the following methods to manipulate the value objects:
26
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
27
+ # * <tt>Customer#address, Customer#address=(address)</tt>
28
+ #
29
+ # These methods will operate with value objects like the ones described below:
30
+ #
31
+ # class Money
32
+ # include Comparable
33
+ # attr_reader :amount, :currency
34
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
35
+ #
36
+ # def initialize(amount, currency = "USD")
37
+ # @amount, @currency = amount, currency
38
+ # end
39
+ #
40
+ # def exchange_to(other_currency)
41
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
42
+ # Money.new(exchanged_amount, other_currency)
43
+ # end
44
+ #
45
+ # def ==(other_money)
46
+ # amount == other_money.amount && currency == other_money.currency
47
+ # end
48
+ #
49
+ # def <=>(other_money)
50
+ # if currency == other_money.currency
51
+ # amount <=> amount
52
+ # else
53
+ # amount <=> other_money.exchange_to(currency).amount
54
+ # end
55
+ # end
56
+ # end
57
+ #
58
+ # class Address
59
+ # attr_reader :street, :city
60
+ # def initialize(street, city)
61
+ # @street, @city = street, city
62
+ # end
63
+ #
64
+ # def close_to?(other_address)
65
+ # city == other_address.city
66
+ # end
67
+ #
68
+ # def ==(other_address)
69
+ # city == other_address.city && street == other_address.street
70
+ # end
71
+ # end
72
+ #
73
+ # Now it's possible to access attributes from the database through the value objects instead. If
74
+ # you choose to name the composition the same as the attribute's name, it will be the only way to
75
+ # access that attribute. That's the case with our +balance+ attribute. You interact with the value
76
+ # objects just like you would any other attribute, though:
77
+ #
78
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
79
+ # customer.balance # => Money value object
80
+ # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
81
+ # customer.balance > Money.new(10) # => true
82
+ # customer.balance == Money.new(20) # => true
83
+ # customer.balance < Money.new(5) # => false
84
+ #
85
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order
86
+ # of the mappings will determine the order of the parameters.
87
+ #
88
+ # customer.address_street = "Hyancintvej"
89
+ # customer.address_city = "Copenhagen"
90
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
91
+ # customer.address = Address.new("May Street", "Chicago")
92
+ # customer.address_street # => "May Street"
93
+ # customer.address_city # => "Chicago"
94
+ #
95
+ # == Writing value objects
96
+ #
97
+ # Value objects are immutable and interchangeable objects that represent a given value, such as
98
+ # a Money object representing $5. Two Money objects both representing $5 should be equal (through
99
+ # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
100
+ # unlike entity objects where equality is determined by identity. An entity class such as Customer can
101
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is
102
+ # determined by object or relational unique identifiers (such as primary keys). Normal
103
+ # ActiveRecord::Base classes are entity objects.
104
+ #
105
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have
106
+ # its amount changed after creation. Create a new Money object with the new value instead. This
107
+ # is exemplified by the Money#exchange_to method that returns a new value object instead of changing
108
+ # its own values. Active Record won't persist value objects that have been changed through means
109
+ # other than the writer method.
110
+ #
111
+ # The immutable requirement is enforced by Active Record by freezing any object assigned as a value
112
+ # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
113
+ #
114
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
115
+ # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
116
+ #
117
+ # == Custom constructors and converters
118
+ #
119
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
120
+ # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
121
+ # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
122
+ # a custom constructor to be specified.
123
+ #
124
+ # When a new value is assigned to the value object the default assumption is that the new value
125
+ # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
126
+ # converted to an instance of value class if necessary.
127
+ #
128
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
129
+ # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
130
+ # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
131
+ # values can be assigned to the value object using either another NetAddr::CIDR object, a string
132
+ # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
133
+ # these requirements:
134
+ #
135
+ # class NetworkResource < ActiveRecord::Base
136
+ # composed_of :cidr,
137
+ # :class_name => 'NetAddr::CIDR',
138
+ # :mapping => [ %w(network_address network), %w(cidr_range bits) ],
139
+ # :allow_nil => true,
140
+ # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
141
+ # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
142
+ # end
143
+ #
144
+ # # This calls the :constructor
145
+ # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
146
+ #
147
+ # # These assignments will both use the :converter
148
+ # network_resource.cidr = [ '192.168.2.1', 8 ]
149
+ # network_resource.cidr = '192.168.0.1/24'
150
+ #
151
+ # # This assignment won't use the :converter as the value is already an instance of the value class
152
+ # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
153
+ #
154
+ # # Saving and then reloading will use the :constructor on reload
155
+ # network_resource.save
156
+ # network_resource.reload
157
+ #
158
+ # == Finding records by a value object
159
+ #
160
+ # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
161
+ # by specifying an instance of the value object in the conditions hash. The following example
162
+ # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
163
+ #
164
+ # Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")})
165
+ #
166
+ module ClassMethods
167
+ # Adds reader and writer methods for manipulating a value object:
168
+ # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
169
+ #
170
+ # Options are:
171
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
172
+ # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
173
+ # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
174
+ # with this option.
175
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
176
+ # object. Each mapping is represented as an array where the first item is the name of the
177
+ # entity attribute and the second item is the name the attribute in the value object. The
178
+ # order in which mappings are defined determine the order in which attributes are sent to the
179
+ # value class constructor.
180
+ # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
181
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
182
+ # mapped attributes.
183
+ # This defaults to +false+.
184
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
185
+ # is called to initialize the value object. The constructor is passed all of the mapped attributes,
186
+ # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
187
+ # to instantiate a <tt>:class_name</tt> object.
188
+ # The default is <tt>:new</tt>.
189
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
190
+ # or a Proc that is called when a new value is assigned to the value object. The converter is
191
+ # passed the single value that is used in the assignment and is only called if the new value is
192
+ # not an instance of <tt>:class_name</tt>.
193
+ #
194
+ # Option examples:
195
+ # composed_of :temperature, :mapping => %w(reading celsius)
196
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
197
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
198
+ # composed_of :gps_location
199
+ # composed_of :gps_location, :allow_nil => true
200
+ # composed_of :ip_address,
201
+ # :class_name => 'IPAddr',
202
+ # :mapping => %w(ip to_i),
203
+ # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
204
+ # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
205
+ #
206
+ def composed_of(part_id, options = {})
207
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
208
+
209
+ name = part_id.id2name
210
+ class_name = options[:class_name] || name.camelize
211
+ mapping = options[:mapping] || [ name, name ]
212
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
213
+ allow_nil = options[:allow_nil] || false
214
+ constructor = options[:constructor] || :new
215
+ converter = options[:converter]
216
+
217
+ reader_method(name, class_name, mapping, allow_nil, constructor)
218
+ writer_method(name, class_name, mapping, allow_nil, converter)
219
+
220
+ create_reflection(:composed_of, part_id, options, self)
221
+ end
222
+
223
+ private
224
+ def reader_method(name, class_name, mapping, allow_nil, constructor)
225
+ module_eval do
226
+ define_method(name) do |*args|
227
+ force_reload = args.first || false
228
+
229
+ unless instance_variable_defined?("@#{name}")
230
+ instance_variable_set("@#{name}", nil)
231
+ end
232
+
233
+ if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
234
+ attrs = mapping.collect {|pair| read_attribute(pair.first)}
235
+ object = case constructor
236
+ when Symbol
237
+ class_name.constantize.send(constructor, *attrs)
238
+ when Proc, Method
239
+ constructor.call(*attrs)
240
+ else
241
+ raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
242
+ end
243
+ instance_variable_set("@#{name}", object)
244
+ end
245
+ instance_variable_get("@#{name}")
246
+ end
247
+ end
248
+
249
+ end
250
+
251
+ def writer_method(name, class_name, mapping, allow_nil, converter)
252
+ module_eval do
253
+ define_method("#{name}=") do |part|
254
+ if part.nil? && allow_nil
255
+ mapping.each { |pair| self[pair.first] = nil }
256
+ instance_variable_set("@#{name}", nil)
257
+ else
258
+ unless part.is_a?(class_name.constantize) || converter.nil?
259
+ part = case converter
260
+ when Symbol
261
+ class_name.constantize.send(converter, part)
262
+ when Proc, Method
263
+ converter.call(part)
264
+ else
265
+ raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
266
+ end
267
+ end
268
+
269
+ mapping.each { |pair| self[pair.first] = part.send(pair.last) }
270
+ instance_variable_set("@#{name}", part.freeze)
271
+ end
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,403 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+ require 'active_support/core_ext/enumerable'
3
+
4
+ module ActiveRecord
5
+ # See ActiveRecord::AssociationPreload::ClassMethods for documentation.
6
+ module AssociationPreload #:nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ # Implements the details of eager loading of Active Record associations.
10
+ # Application developers should not use this module directly.
11
+ #
12
+ # <tt>ActiveRecord::Base</tt> is extended with this module. The source code in
13
+ # <tt>ActiveRecord::Base</tt> references methods defined in this module.
14
+ #
15
+ # Note that 'eager loading' and 'preloading' are actually the same thing.
16
+ # However, there are two different eager loading strategies.
17
+ #
18
+ # The first one is by using table joins. This was only strategy available
19
+ # prior to Rails 2.1. Suppose that you have an Author model with columns
20
+ # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
21
+ # this strategy, Active Record would try to retrieve all data for an author
22
+ # and all of its books via a single query:
23
+ #
24
+ # SELECT * FROM authors
25
+ # LEFT OUTER JOIN books ON authors.id = books.id
26
+ # WHERE authors.name = 'Ken Akamatsu'
27
+ #
28
+ # However, this could result in many rows that contain redundant data. After
29
+ # having received the first row, we already have enough data to instantiate
30
+ # the Author object. In all subsequent rows, only the data for the joined
31
+ # 'books' table is useful; the joined 'authors' data is just redundant, and
32
+ # processing this redundant data takes memory and CPU time. The problem
33
+ # quickly becomes worse and worse as the level of eager loading increases
34
+ # (i.e. if Active Record is to eager load the associations' associations as
35
+ # well).
36
+ #
37
+ # The second strategy is to use multiple database queries, one for each
38
+ # level of association. Since Rails 2.1, this is the default strategy. In
39
+ # situations where a table join is necessary (e.g. when the +:conditions+
40
+ # option references an association's column), it will fallback to the table
41
+ # join strategy.
42
+ #
43
+ # See also ActiveRecord::Associations::ClassMethods, which explains eager
44
+ # loading in a more high-level (application developer-friendly) manner.
45
+ module ClassMethods
46
+ protected
47
+
48
+ # Eager loads the named associations for the given Active Record record(s).
49
+ #
50
+ # In this description, 'association name' shall refer to the name passed
51
+ # to an association creation method. For example, a model that specifies
52
+ # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
53
+ # names +:author+ and +:buyers+.
54
+ #
55
+ # == Parameters
56
+ # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
57
+ # i.e. +records+ itself may also contain arrays of records. In any case,
58
+ # +preload_associations+ will preload the all associations records by
59
+ # flattening +records+.
60
+ #
61
+ # +associations+ specifies one or more associations that you want to
62
+ # preload. It may be:
63
+ # - a Symbol or a String which specifies a single association name. For
64
+ # example, specifying +:books+ allows this method to preload all books
65
+ # for an Author.
66
+ # - an Array which specifies multiple association names. This array
67
+ # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
68
+ # allows this method to preload an author's avatar as well as all of his
69
+ # books.
70
+ # - a Hash which specifies multiple association names, as well as
71
+ # association names for the to-be-preloaded association objects. For
72
+ # example, specifying <tt>{ :author => :avatar }</tt> will preload a
73
+ # book's author, as well as that author's avatar.
74
+ #
75
+ # +:associations+ has the same format as the +:include+ option for
76
+ # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
77
+ #
78
+ # :books
79
+ # [ :books, :author ]
80
+ # { :author => :avatar }
81
+ # [ :books, { :author => :avatar } ]
82
+ #
83
+ # +preload_options+ contains options that will be passed to ActiveRecord::Base#find
84
+ # (which is called under the hood for preloading records). But it is passed
85
+ # only one level deep in the +associations+ argument, i.e. it's not passed
86
+ # to the child associations when +associations+ is a Hash.
87
+ def preload_associations(records, associations, preload_options={})
88
+ records = Array.wrap(records).compact.uniq
89
+ return if records.empty?
90
+ case associations
91
+ when Array then associations.each {|association| preload_associations(records, association, preload_options)}
92
+ when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
93
+ when Hash then
94
+ associations.each do |parent, child|
95
+ raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
96
+ preload_associations(records, parent, preload_options)
97
+ reflection = reflections[parent]
98
+ parents = records.sum { |record| Array.wrap(record.send(reflection.name)) }
99
+ unless parents.empty?
100
+ parents.first.class.preload_associations(parents, child)
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # Preloads a specific named association for the given records. This is
109
+ # called by +preload_associations+ as its base case.
110
+ def preload_one_association(records, association, preload_options={})
111
+ class_to_reflection = {}
112
+ # Not all records have the same class, so group then preload
113
+ # group on the reflection itself so that if various subclass share the same association then
114
+ # we do not split them unnecessarily
115
+ records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records|
116
+ raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
117
+
118
+ # 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
119
+ # the following could call 'preload_belongs_to_association',
120
+ # 'preload_has_many_association', etc.
121
+ send("preload_#{reflection.macro}_association", _records, reflection, preload_options)
122
+ end
123
+ end
124
+
125
+ def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
126
+ parent_records.each do |parent_record|
127
+ association_proxy = parent_record.send(reflection_name)
128
+ association_proxy.loaded
129
+ association_proxy.target.push(*Array.wrap(associated_record))
130
+
131
+ association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
132
+ end
133
+ end
134
+
135
+ def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
136
+ parent_records.each do |parent_record|
137
+ parent_record.send("set_#{reflection_name}_target", associated_record)
138
+ end
139
+ end
140
+
141
+ def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
142
+ associated_records.each do |associated_record|
143
+ mapped_records = id_to_record_map[associated_record[key].to_s]
144
+ add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
145
+ end
146
+ end
147
+
148
+ def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
149
+ seen_keys = {}
150
+ associated_records.each do |associated_record|
151
+ #this is a has_one or belongs_to: there should only be one record.
152
+ #Unfortunately we can't (in portable way) ask the database for
153
+ #'all records where foo_id in (x,y,z), but please
154
+ # only one row per distinct foo_id' so this where we enforce that
155
+ next if seen_keys[associated_record[key].to_s]
156
+ seen_keys[associated_record[key].to_s] = true
157
+ mapped_records = id_to_record_map[associated_record[key].to_s]
158
+ mapped_records.each do |mapped_record|
159
+ association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
160
+ association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
161
+ end
162
+ end
163
+
164
+ id_to_record_map.each do |id, records|
165
+ next if seen_keys.include?(id.to_s)
166
+ records.each {|record| record.send("set_#{reflection_name}_target", nil) }
167
+ end
168
+ end
169
+
170
+ # Given a collection of Active Record objects, constructs a Hash which maps
171
+ # the objects' IDs to the relevant objects. Returns a 2-tuple
172
+ # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
173
+ # and +ids+ is an Array of record IDs.
174
+ def construct_id_map(records, primary_key=nil)
175
+ id_to_record_map = {}
176
+ ids = []
177
+ records.each do |record|
178
+ primary_key ||= record.class.primary_key
179
+ ids << record[primary_key]
180
+ mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
181
+ mapped_records << record
182
+ end
183
+ ids.uniq!
184
+ return id_to_record_map, ids
185
+ end
186
+
187
+ def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
188
+ table_name = reflection.klass.quoted_table_name
189
+ id_to_record_map, ids = construct_id_map(records)
190
+ records.each {|record| record.send(reflection.name).loaded}
191
+ options = reflection.options
192
+
193
+ conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
194
+ conditions << append_conditions(reflection, preload_options)
195
+
196
+ associated_records = reflection.klass.unscoped.where([conditions, ids]).
197
+ includes(options[:include]).
198
+ joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}").
199
+ select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id").
200
+ order(options[:order]).to_a
201
+
202
+ set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
203
+ end
204
+
205
+ def preload_has_one_association(records, reflection, preload_options={})
206
+ return if records.first.send("loaded_#{reflection.name}?")
207
+ id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
208
+ options = reflection.options
209
+ records.each {|record| record.send("set_#{reflection.name}_target", nil)}
210
+ if options[:through]
211
+ through_records = preload_through_records(records, reflection, options[:through])
212
+ through_reflection = reflections[options[:through]]
213
+ through_primary_key = through_reflection.primary_key_name
214
+ unless through_records.empty?
215
+ source = reflection.source_reflection.name
216
+ through_records.first.class.preload_associations(through_records, source)
217
+ if through_reflection.macro == :belongs_to
218
+ rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
219
+ rev_primary_key = through_reflection.klass.primary_key
220
+ through_records.each do |through_record|
221
+ add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
222
+ reflection.name, through_record.send(source))
223
+ end
224
+ else
225
+ through_records.each do |through_record|
226
+ add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
227
+ reflection.name, through_record.send(source))
228
+ end
229
+ end
230
+ end
231
+ else
232
+ set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
233
+ end
234
+ end
235
+
236
+ def preload_has_many_association(records, reflection, preload_options={})
237
+ return if records.first.send(reflection.name).loaded?
238
+ options = reflection.options
239
+
240
+ primary_key_name = reflection.through_reflection_primary_key_name
241
+ id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
242
+ records.each {|record| record.send(reflection.name).loaded}
243
+
244
+ if options[:through]
245
+ through_records = preload_through_records(records, reflection, options[:through])
246
+ through_reflection = reflections[options[:through]]
247
+ unless through_records.empty?
248
+ source = reflection.source_reflection.name
249
+ through_records.first.class.preload_associations(through_records, source, options)
250
+ through_records.each do |through_record|
251
+ through_record_id = through_record[reflection.through_reflection_primary_key].to_s
252
+ add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
253
+ end
254
+ end
255
+
256
+ else
257
+ set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
258
+ reflection.primary_key_name)
259
+ end
260
+ end
261
+
262
+ def preload_through_records(records, reflection, through_association)
263
+ through_reflection = reflections[through_association]
264
+ through_primary_key = through_reflection.primary_key_name
265
+
266
+ through_records = []
267
+ if reflection.options[:source_type]
268
+ interface = reflection.source_reflection.options[:foreign_type]
269
+ preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
270
+
271
+ records.compact!
272
+ records.first.class.preload_associations(records, through_association, preload_options)
273
+
274
+ # Dont cache the association - we would only be caching a subset
275
+ records.each do |record|
276
+ proxy = record.send(through_association)
277
+
278
+ if proxy.respond_to?(:target)
279
+ through_records.concat Array.wrap(proxy.target)
280
+ proxy.reset
281
+ else # this is a has_one :through reflection
282
+ through_records << proxy if proxy
283
+ end
284
+ end
285
+ else
286
+ options = {}
287
+ options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
288
+ options[:order] = reflection.options[:order]
289
+ options[:conditions] = reflection.options[:conditions]
290
+ records.first.class.preload_associations(records, through_association, options)
291
+
292
+ records.each do |record|
293
+ through_records.concat Array.wrap(record.send(through_association))
294
+ end
295
+ end
296
+ through_records
297
+ end
298
+
299
+ def preload_belongs_to_association(records, reflection, preload_options={})
300
+ return if records.first.send("loaded_#{reflection.name}?")
301
+ options = reflection.options
302
+ primary_key_name = reflection.primary_key_name
303
+
304
+ if options[:polymorphic]
305
+ polymorph_type = options[:foreign_type]
306
+ klasses_and_ids = {}
307
+
308
+ # Construct a mapping from klass to a list of ids to load and a mapping of those ids back
309
+ # to their parent_records
310
+ records.each do |record|
311
+ if klass = record.send(polymorph_type)
312
+ klass_id = record.send(primary_key_name)
313
+ if klass_id
314
+ id_map = klasses_and_ids[klass] ||= {}
315
+ id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
316
+ id_list_for_klass_id << record
317
+ end
318
+ end
319
+ end
320
+ klasses_and_ids = klasses_and_ids.to_a
321
+ else
322
+ id_map = {}
323
+ records.each do |record|
324
+ key = record.send(primary_key_name)
325
+ if key
326
+ mapped_records = (id_map[key.to_s] ||= [])
327
+ mapped_records << record
328
+ end
329
+ end
330
+ klasses_and_ids = [[reflection.klass.name, id_map]]
331
+ end
332
+
333
+ klasses_and_ids.each do |klass_and_id|
334
+ klass_name, id_map = *klass_and_id
335
+ next if id_map.empty?
336
+ klass = klass_name.constantize
337
+
338
+ table_name = klass.quoted_table_name
339
+ primary_key = reflection.options[:primary_key] || klass.primary_key
340
+ column_type = klass.columns.detect{|c| c.name == primary_key}.type
341
+
342
+ ids = id_map.keys.map do |id|
343
+ if column_type == :integer
344
+ id.to_i
345
+ elsif column_type == :float
346
+ id.to_f
347
+ else
348
+ id
349
+ end
350
+ end
351
+
352
+ conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
353
+ conditions << append_conditions(reflection, preload_options)
354
+
355
+ associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
356
+
357
+ set_association_single_records(id_map, reflection.name, associated_records, primary_key)
358
+ end
359
+ end
360
+
361
+ def find_associated_records(ids, reflection, preload_options)
362
+ options = reflection.options
363
+ table_name = reflection.klass.quoted_table_name
364
+
365
+ if interface = reflection.options[:as]
366
+ conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
367
+ else
368
+ foreign_key = reflection.primary_key_name
369
+ conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
370
+ end
371
+
372
+ conditions << append_conditions(reflection, preload_options)
373
+
374
+ find_options = {
375
+ :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"),
376
+ :include => preload_options[:include] || options[:include],
377
+ :conditions => [conditions, ids],
378
+ :joins => options[:joins],
379
+ :group => preload_options[:group] || options[:group],
380
+ :order => preload_options[:order] || options[:order]
381
+ }
382
+
383
+ reflection.klass.scoped.apply_finder_options(find_options).to_a
384
+ end
385
+
386
+
387
+ def interpolate_sql_for_preload(sql)
388
+ instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
389
+ end
390
+
391
+ def append_conditions(reflection, preload_options)
392
+ sql = ""
393
+ sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
394
+ sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
395
+ sql
396
+ end
397
+
398
+ def in_or_equals_for_ids(ids)
399
+ ids.size > 1 ? "IN (?)" : "= ?"
400
+ end
401
+ end
402
+ end
403
+ end