square-activerecord 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
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