square-activerecord 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. data/CHANGELOG +6140 -0
  2. data/README.rdoc +222 -0
  3. data/examples/associations.png +0 -0
  4. data/examples/performance.rb +179 -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 +430 -0
  9. data/lib/active_record/associations.rb +2307 -0
  10. data/lib/active_record/associations/association_collection.rb +572 -0
  11. data/lib/active_record/associations/association_proxy.rb +299 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +82 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
  15. data/lib/active_record/associations/has_many_association.rb +128 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +115 -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 +30 -0
  22. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  23. data/lib/active_record/attribute_methods/primary_key.rb +56 -0
  24. data/lib/active_record/attribute_methods/query.rb +39 -0
  25. data/lib/active_record/attribute_methods/read.rb +145 -0
  26. data/lib/active_record/attribute_methods/time_zone_conversion.rb +64 -0
  27. data/lib/active_record/attribute_methods/write.rb +43 -0
  28. data/lib/active_record/autosave_association.rb +369 -0
  29. data/lib/active_record/base.rb +1904 -0
  30. data/lib/active_record/callbacks.rb +284 -0
  31. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +364 -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 +333 -0
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +73 -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 +539 -0
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +217 -0
  40. data/lib/active_record/connection_adapters/mysql_adapter.rb +657 -0
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1031 -0
  42. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -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 +56 -0
  46. data/lib/active_record/dynamic_scope_match.rb +23 -0
  47. data/lib/active_record/errors.rb +172 -0
  48. data/lib/active_record/fixtures.rb +1006 -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 +419 -0
  56. data/lib/active_record/observer.rb +125 -0
  57. data/lib/active_record/persistence.rb +290 -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 +411 -0
  63. data/lib/active_record/relation.rb +394 -0
  64. data/lib/active_record/relation/batches.rb +89 -0
  65. data/lib/active_record/relation/calculations.rb +295 -0
  66. data/lib/active_record/relation/finder_methods.rb +363 -0
  67. data/lib/active_record/relation/predicate_builder.rb +48 -0
  68. data/lib/active_record/relation/query_methods.rb +303 -0
  69. data/lib/active_record/relation/spawn_methods.rb +132 -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 +359 -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 +190 -0
  81. data/lib/active_record/version.rb +10 -0
  82. data/lib/rails/generators/active_record.rb +19 -0
  83. data/lib/rails/generators/active_record/migration.rb +15 -0
  84. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  85. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  86. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  87. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  88. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  89. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  90. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  91. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  92. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  93. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  94. metadata +223 -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.where(:balance => Money.new(20, "USD")).all
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,430 @@
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_proxy = reflection.klass.unscoped.
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])
201
+
202
+ all_associated_records = associated_records(ids) do |some_ids|
203
+ associated_records_proxy.where([conditions, ids]).to_a
204
+ end
205
+
206
+ set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id')
207
+ end
208
+
209
+ def preload_has_one_association(records, reflection, preload_options={})
210
+ return if records.first.send("loaded_#{reflection.name}?")
211
+ id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
212
+ options = reflection.options
213
+ records.each {|record| record.send("set_#{reflection.name}_target", nil)}
214
+ if options[:through]
215
+ through_records = preload_through_records(records, reflection, options[:through])
216
+ through_reflection = reflections[options[:through]]
217
+ through_primary_key = through_reflection.primary_key_name
218
+ unless through_records.empty?
219
+ source = reflection.source_reflection.name
220
+ through_records.first.class.preload_associations(through_records, source)
221
+ if through_reflection.macro == :belongs_to
222
+ rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
223
+ rev_primary_key = through_reflection.klass.primary_key
224
+ through_records.each do |through_record|
225
+ add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
226
+ reflection.name, through_record.send(source))
227
+ end
228
+ else
229
+ through_records.each do |through_record|
230
+ add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
231
+ reflection.name, through_record.send(source))
232
+ end
233
+ end
234
+ end
235
+ else
236
+ set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
237
+ end
238
+ end
239
+
240
+ def preload_has_many_association(records, reflection, preload_options={})
241
+ return if records.first.send(reflection.name).loaded?
242
+ options = reflection.options
243
+
244
+ primary_key_name = reflection.through_reflection_primary_key_name
245
+ id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
246
+ records.each {|record| record.send(reflection.name).loaded}
247
+
248
+ if options[:through]
249
+ through_records = preload_through_records(records, reflection, options[:through])
250
+ through_reflection = reflections[options[:through]]
251
+ unless through_records.empty?
252
+ source = reflection.source_reflection.name
253
+ through_records.first.class.preload_associations(through_records, source, options)
254
+ through_records.each do |through_record|
255
+ through_record_id = through_record[reflection.through_reflection_primary_key].to_s
256
+ add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
257
+ end
258
+ end
259
+
260
+ else
261
+ set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
262
+ reflection.primary_key_name)
263
+ end
264
+ end
265
+
266
+ def preload_through_records(records, reflection, through_association)
267
+ through_reflection = reflections[through_association]
268
+ through_primary_key = through_reflection.primary_key_name
269
+
270
+ through_records = []
271
+ if reflection.options[:source_type]
272
+ interface = reflection.source_reflection.options[:foreign_type]
273
+ preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
274
+
275
+ records.compact!
276
+ records.first.class.preload_associations(records, through_association, preload_options)
277
+
278
+ # Dont cache the association - we would only be caching a subset
279
+ records.each do |record|
280
+ proxy = record.send(through_association)
281
+
282
+ if proxy.respond_to?(:target)
283
+ through_records.concat Array.wrap(proxy.target)
284
+ proxy.reset
285
+ else # this is a has_one :through reflection
286
+ through_records << proxy if proxy
287
+ end
288
+ end
289
+ else
290
+ options = {}
291
+ options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] || reflection.options[:order]
292
+ options[:order] = reflection.options[:order]
293
+ options[:conditions] = reflection.options[:conditions]
294
+ records.first.class.preload_associations(records, through_association, options)
295
+
296
+ records.each do |record|
297
+ through_records.concat Array.wrap(record.send(through_association))
298
+ end
299
+ end
300
+ through_records
301
+ end
302
+
303
+ def preload_belongs_to_association(records, reflection, preload_options={})
304
+ return if records.first.send("loaded_#{reflection.name}?")
305
+ options = reflection.options
306
+ primary_key_name = reflection.primary_key_name
307
+
308
+ if options[:polymorphic]
309
+ polymorph_type = options[:foreign_type]
310
+ klasses_and_ids = {}
311
+
312
+ # Construct a mapping from klass to a list of ids to load and a mapping of those ids back
313
+ # to their parent_records
314
+ records.each do |record|
315
+ if klass = record.send(polymorph_type)
316
+ klass_id = record.send(primary_key_name)
317
+ if klass_id
318
+ id_map = klasses_and_ids[klass] ||= {}
319
+ id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
320
+ id_list_for_klass_id << record
321
+ end
322
+ end
323
+ end
324
+ klasses_and_ids = klasses_and_ids.to_a
325
+ else
326
+ id_map = {}
327
+ records.each do |record|
328
+ key = record.send(primary_key_name)
329
+ if key
330
+ mapped_records = (id_map[key.to_s] ||= [])
331
+ mapped_records << record
332
+ end
333
+ end
334
+ klasses_and_ids = [[reflection.klass.name, id_map]]
335
+ end
336
+
337
+ klasses_and_ids.each do |klass_and_id|
338
+ klass_name, id_map = *klass_and_id
339
+ next if id_map.empty?
340
+ klass = klass_name.constantize
341
+
342
+ table_name = klass.quoted_table_name
343
+ primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s
344
+ column_type = klass.columns.detect{|c| c.name == primary_key}.type
345
+ ids = id_map.keys.map do |id|
346
+ if column_type == :integer
347
+ id.to_i
348
+ elsif column_type == :float
349
+ id.to_f
350
+ else
351
+ id
352
+ end
353
+ end
354
+
355
+ conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
356
+ conditions << append_conditions(reflection, preload_options)
357
+
358
+ associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
359
+
360
+ set_association_single_records(id_map, reflection.name, associated_records, primary_key)
361
+ end
362
+ end
363
+
364
+ def find_associated_records(ids, reflection, preload_options)
365
+ options = reflection.options
366
+ table_name = reflection.klass.quoted_table_name
367
+
368
+ if interface = reflection.options[:as]
369
+ 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}'"
370
+ else
371
+ foreign_key = reflection.primary_key_name
372
+ conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
373
+ end
374
+
375
+ conditions << append_conditions(reflection, preload_options)
376
+
377
+ find_options = {
378
+ :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"),
379
+ :include => preload_options[:include] || options[:include],
380
+ :joins => options[:joins],
381
+ :group => preload_options[:group] || options[:group],
382
+ :order => preload_options[:order] || options[:order]
383
+ }
384
+
385
+ associated_records(ids) do |some_ids|
386
+ reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a
387
+ end
388
+ end
389
+
390
+ def process_conditions_for_preload(conditions, klass = self)
391
+ sanitized = klass.send(:sanitize_sql, conditions)
392
+
393
+ if sanitized =~ /\#\{.*\}/
394
+ ActiveSupport::Deprecation.warn(
395
+ 'String-based interpolation of association conditions is deprecated. Please use a ' \
396
+ 'proc instead. So, for example, has_many :older_friends, :conditions => \'age > #{age}\' ' \
397
+ 'should be changed to has_many :older_friends, :conditions => proc { "age > #{age}" }.'
398
+ )
399
+ instance_eval("%@#{sanitized.gsub('@', '\@')}@", __FILE__, __LINE__)
400
+ elsif conditions.respond_to?(:to_proc)
401
+ klass.send(:sanitize_sql, instance_eval(&conditions))
402
+ else
403
+ sanitized
404
+ end
405
+ end
406
+
407
+ def append_conditions(reflection, preload_options)
408
+ sql = ""
409
+ sql << " AND (#{process_conditions_for_preload(reflection.options[:conditions], reflection.klass)})" if reflection.options[:conditions]
410
+ sql << " AND (#{process_conditions_for_preload(preload_options[:conditions])})" if preload_options[:conditions]
411
+ sql
412
+ end
413
+
414
+ def in_or_equals_for_ids(ids)
415
+ ids.size > 1 ? "IN (?)" : "= ?"
416
+ end
417
+
418
+ # Some databases impose a limit on the number of ids in a list (in Oracle its 1000)
419
+ # Make several smaller queries if necessary or make one query if the adapter supports it
420
+ def associated_records(ids)
421
+ max_ids_in_a_list = connection.ids_in_list_limit || ids.size
422
+ records = []
423
+ ids.each_slice(max_ids_in_a_list) do |some_ids|
424
+ records += yield(some_ids)
425
+ end
426
+ records
427
+ end
428
+ end
429
+ end
430
+ end