activerecord 3.0.20 → 3.1.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +220 -91
- data/README.rdoc +3 -3
- data/examples/performance.rb +88 -109
- data/lib/active_record.rb +6 -2
- data/lib/active_record/aggregations.rb +22 -45
- data/lib/active_record/associations.rb +264 -991
- data/lib/active_record/associations/alias_tracker.rb +85 -0
- data/lib/active_record/associations/association.rb +231 -0
- data/lib/active_record/associations/association_scope.rb +120 -0
- data/lib/active_record/associations/belongs_to_association.rb +40 -60
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +15 -63
- data/lib/active_record/associations/builder/association.rb +53 -0
- data/lib/active_record/associations/builder/belongs_to.rb +85 -0
- data/lib/active_record/associations/builder/collection_association.rb +75 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +63 -0
- data/lib/active_record/associations/builder/has_many.rb +65 -0
- data/lib/active_record/associations/builder/has_one.rb +63 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +524 -0
- data/lib/active_record/associations/collection_proxy.rb +125 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +27 -118
- data/lib/active_record/associations/has_many_association.rb +50 -79
- data/lib/active_record/associations/has_many_through_association.rb +98 -67
- data/lib/active_record/associations/has_one_association.rb +45 -115
- data/lib/active_record/associations/has_one_through_association.rb +21 -25
- data/lib/active_record/associations/join_dependency.rb +215 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +150 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_helper.rb +56 -0
- data/lib/active_record/associations/preloader.rb +177 -0
- data/lib/active_record/associations/preloader/association.rb +126 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +67 -0
- data/lib/active_record/associations/singular_association.rb +55 -0
- data/lib/active_record/associations/through_association.rb +80 -0
- data/lib/active_record/attribute_methods.rb +19 -5
- data/lib/active_record/attribute_methods/before_type_cast.rb +9 -8
- data/lib/active_record/attribute_methods/dirty.rb +8 -2
- data/lib/active_record/attribute_methods/primary_key.rb +33 -13
- data/lib/active_record/attribute_methods/read.rb +17 -17
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -4
- data/lib/active_record/attribute_methods/write.rb +2 -1
- data/lib/active_record/autosave_association.rb +66 -45
- data/lib/active_record/base.rb +445 -273
- data/lib/active_record/callbacks.rb +24 -33
- data/lib/active_record/coders/yaml_column.rb +41 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +106 -13
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +16 -2
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +83 -12
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +16 -16
- data/lib/active_record/connection_adapters/abstract/quoting.rb +61 -22
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +16 -273
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -42
- data/lib/active_record/connection_adapters/abstract_adapter.rb +44 -25
- data/lib/active_record/connection_adapters/column.rb +268 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +686 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +331 -88
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +295 -267
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +3 -7
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +108 -26
- data/lib/active_record/counter_cache.rb +7 -4
- data/lib/active_record/fixtures.rb +174 -192
- data/lib/active_record/identity_map.rb +131 -0
- data/lib/active_record/locking/optimistic.rb +20 -14
- data/lib/active_record/locking/pessimistic.rb +4 -4
- data/lib/active_record/log_subscriber.rb +24 -4
- data/lib/active_record/migration.rb +265 -144
- data/lib/active_record/migration/command_recorder.rb +103 -0
- data/lib/active_record/named_scope.rb +68 -25
- data/lib/active_record/nested_attributes.rb +58 -15
- data/lib/active_record/observer.rb +3 -7
- data/lib/active_record/persistence.rb +58 -38
- data/lib/active_record/query_cache.rb +25 -3
- data/lib/active_record/railtie.rb +21 -12
- data/lib/active_record/railties/console_sandbox.rb +6 -0
- data/lib/active_record/railties/databases.rake +147 -116
- data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
- data/lib/active_record/reflection.rb +176 -44
- data/lib/active_record/relation.rb +125 -49
- data/lib/active_record/relation/batches.rb +7 -5
- data/lib/active_record/relation/calculations.rb +50 -18
- data/lib/active_record/relation/finder_methods.rb +47 -26
- data/lib/active_record/relation/predicate_builder.rb +24 -21
- data/lib/active_record/relation/query_methods.rb +117 -101
- data/lib/active_record/relation/spawn_methods.rb +27 -20
- data/lib/active_record/result.rb +34 -0
- data/lib/active_record/schema.rb +5 -6
- data/lib/active_record/schema_dumper.rb +11 -13
- data/lib/active_record/serialization.rb +2 -2
- data/lib/active_record/serializers/xml_serializer.rb +10 -10
- data/lib/active_record/session_store.rb +8 -2
- data/lib/active_record/test_case.rb +9 -20
- data/lib/active_record/timestamp.rb +21 -9
- data/lib/active_record/transactions.rb +16 -15
- data/lib/active_record/validations.rb +21 -22
- data/lib/active_record/validations/associated.rb +3 -1
- data/lib/active_record/validations/uniqueness.rb +48 -58
- data/lib/active_record/version.rb +3 -3
- data/lib/rails/generators/active_record.rb +6 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +10 -2
- data/lib/rails/generators/active_record/model/model_generator.rb +2 -1
- data/lib/rails/generators/active_record/model/templates/migration.rb +6 -5
- data/lib/rails/generators/active_record/model/templates/model.rb +2 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +2 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +2 -1
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +2 -2
- metadata +106 -77
- checksums.yaml +0 -7
- data/lib/active_record/association_preload.rb +0 -431
- data/lib/active_record/associations/association_collection.rb +0 -572
- data/lib/active_record/associations/association_proxy.rb +0 -304
- data/lib/active_record/associations/through_association_scope.rb +0 -160
data/lib/active_record.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#--
|
2
|
-
# Copyright (c) 2004-
|
2
|
+
# Copyright (c) 2004-2011 David Heinemeier Hansson
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -43,7 +43,6 @@ module ActiveRecord
|
|
43
43
|
autoload :ConnectionNotEstablished, 'active_record/errors'
|
44
44
|
|
45
45
|
autoload :Aggregations
|
46
|
-
autoload :AssociationPreload
|
47
46
|
autoload :Associations
|
48
47
|
autoload :AttributeMethods
|
49
48
|
autoload :AutosaveAssociation
|
@@ -79,6 +78,11 @@ module ActiveRecord
|
|
79
78
|
autoload :Timestamp
|
80
79
|
autoload :Transactions
|
81
80
|
autoload :Validations
|
81
|
+
autoload :IdentityMap
|
82
|
+
end
|
83
|
+
|
84
|
+
module Coders
|
85
|
+
autoload :YAMLColumn, 'active_record/coders/yaml_column'
|
82
86
|
end
|
83
87
|
|
84
88
|
module AttributeMethods
|
@@ -4,9 +4,7 @@ module ActiveRecord
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
def clear_aggregation_cache #:nodoc:
|
7
|
-
|
8
|
-
instance_variable_set "@#{assoc.name}", nil
|
9
|
-
end unless self.new_record?
|
7
|
+
@aggregation_cache.clear if persisted?
|
10
8
|
end
|
11
9
|
|
12
10
|
# Active Record implements aggregation through a macro-like class method called +composed_of+
|
@@ -48,7 +46,7 @@ module ActiveRecord
|
|
48
46
|
#
|
49
47
|
# def <=>(other_money)
|
50
48
|
# if currency == other_money.currency
|
51
|
-
# amount <=>
|
49
|
+
# amount <=> amount
|
52
50
|
# else
|
53
51
|
# amount <=> other_money.exchange_to(currency).amount
|
54
52
|
# end
|
@@ -222,53 +220,32 @@ module ActiveRecord
|
|
222
220
|
|
223
221
|
private
|
224
222
|
def reader_method(name, class_name, mapping, allow_nil, constructor)
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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}")
|
223
|
+
define_method(name) do
|
224
|
+
if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
|
225
|
+
attrs = mapping.collect {|pair| read_attribute(pair.first)}
|
226
|
+
object = constructor.respond_to?(:call) ?
|
227
|
+
constructor.call(*attrs) :
|
228
|
+
class_name.constantize.send(constructor, *attrs)
|
229
|
+
@aggregation_cache[name] = object
|
246
230
|
end
|
231
|
+
@aggregation_cache[name]
|
247
232
|
end
|
248
|
-
|
249
233
|
end
|
250
234
|
|
251
235
|
def writer_method(name, class_name, mapping, allow_nil, converter)
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
part
|
260
|
-
|
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)
|
236
|
+
define_method("#{name}=") do |part|
|
237
|
+
if part.nil? && allow_nil
|
238
|
+
mapping.each { |pair| self[pair.first] = nil }
|
239
|
+
@aggregation_cache[name] = nil
|
240
|
+
else
|
241
|
+
unless part.is_a?(class_name.constantize) || converter.nil?
|
242
|
+
part = converter.respond_to?(:call) ?
|
243
|
+
converter.call(part) :
|
244
|
+
class_name.constantize.send(converter, part)
|
271
245
|
end
|
246
|
+
|
247
|
+
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
248
|
+
@aggregation_cache[name] = part.freeze
|
272
249
|
end
|
273
250
|
end
|
274
251
|
end
|
@@ -4,6 +4,7 @@ require 'active_support/core_ext/module/delegation'
|
|
4
4
|
require 'active_support/core_ext/object/blank'
|
5
5
|
require 'active_support/core_ext/string/conversions'
|
6
6
|
require 'active_support/core_ext/module/remove_method'
|
7
|
+
require 'active_support/core_ext/class/attribute'
|
7
8
|
|
8
9
|
module ActiveRecord
|
9
10
|
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
|
@@ -18,18 +19,30 @@ module ActiveRecord
|
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
|
-
class
|
22
|
+
class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
|
22
23
|
def initialize(owner_class_name, reflection, source_reflection)
|
23
24
|
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
28
|
+
class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
|
29
|
+
def initialize(owner_class_name, reflection)
|
30
|
+
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
27
34
|
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
|
28
35
|
def initialize(owner_class_name, reflection, source_reflection)
|
29
36
|
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
|
30
37
|
end
|
31
38
|
end
|
32
39
|
|
40
|
+
class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
|
41
|
+
def initialize(owner_class_name, reflection, through_reflection)
|
42
|
+
super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
33
46
|
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
|
34
47
|
def initialize(reflection)
|
35
48
|
through_reflection = reflection.through_reflection
|
@@ -39,14 +52,6 @@ module ActiveRecord
|
|
39
52
|
end
|
40
53
|
end
|
41
54
|
|
42
|
-
class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
|
43
|
-
def initialize(reflection)
|
44
|
-
through_reflection = reflection.through_reflection
|
45
|
-
source_reflection = reflection.source_reflection
|
46
|
-
super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
55
|
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
|
51
56
|
def initialize(owner, reflection)
|
52
57
|
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
|
@@ -65,6 +70,12 @@ module ActiveRecord
|
|
65
70
|
end
|
66
71
|
end
|
67
72
|
|
73
|
+
class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
|
74
|
+
def initialize(owner, reflection)
|
75
|
+
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
68
79
|
class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
|
69
80
|
def initialize(reflection)
|
70
81
|
super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
|
@@ -93,8 +104,8 @@ module ActiveRecord
|
|
93
104
|
# (has_many, has_one) when there is at least 1 child associated instance.
|
94
105
|
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
|
95
106
|
class DeleteRestrictionError < ActiveRecordError #:nodoc:
|
96
|
-
def initialize(
|
97
|
-
super("Cannot delete record because of dependent #{
|
107
|
+
def initialize(name)
|
108
|
+
super("Cannot delete record because of dependent #{name}")
|
98
109
|
end
|
99
110
|
end
|
100
111
|
|
@@ -104,36 +115,67 @@ module ActiveRecord
|
|
104
115
|
|
105
116
|
# These classes will be loaded when associations are created.
|
106
117
|
# So there is no need to eager load them.
|
107
|
-
autoload :
|
108
|
-
autoload :
|
109
|
-
autoload :
|
118
|
+
autoload :Association, 'active_record/associations/association'
|
119
|
+
autoload :SingularAssociation, 'active_record/associations/singular_association'
|
120
|
+
autoload :CollectionAssociation, 'active_record/associations/collection_association'
|
121
|
+
autoload :CollectionProxy, 'active_record/associations/collection_proxy'
|
122
|
+
|
123
|
+
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
|
110
124
|
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
|
111
|
-
autoload :HasAndBelongsToManyAssociation,
|
112
|
-
autoload :HasManyAssociation,
|
113
|
-
autoload :HasManyThroughAssociation,
|
114
|
-
autoload :HasOneAssociation,
|
115
|
-
autoload :HasOneThroughAssociation,
|
125
|
+
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
|
126
|
+
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
|
127
|
+
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
|
128
|
+
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
|
129
|
+
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
|
130
|
+
autoload :ThroughAssociation, 'active_record/associations/through_association'
|
131
|
+
|
132
|
+
module Builder #:nodoc:
|
133
|
+
autoload :Association, 'active_record/associations/builder/association'
|
134
|
+
autoload :SingularAssociation, 'active_record/associations/builder/singular_association'
|
135
|
+
autoload :CollectionAssociation, 'active_record/associations/builder/collection_association'
|
136
|
+
|
137
|
+
autoload :BelongsTo, 'active_record/associations/builder/belongs_to'
|
138
|
+
autoload :HasOne, 'active_record/associations/builder/has_one'
|
139
|
+
autoload :HasMany, 'active_record/associations/builder/has_many'
|
140
|
+
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
|
141
|
+
end
|
142
|
+
|
143
|
+
autoload :Preloader, 'active_record/associations/preloader'
|
144
|
+
autoload :JoinDependency, 'active_record/associations/join_dependency'
|
145
|
+
autoload :AssociationScope, 'active_record/associations/association_scope'
|
146
|
+
autoload :AliasTracker, 'active_record/associations/alias_tracker'
|
147
|
+
autoload :JoinHelper, 'active_record/associations/join_helper'
|
116
148
|
|
117
149
|
# Clears out the association cache.
|
118
150
|
def clear_association_cache #:nodoc:
|
119
|
-
|
120
|
-
|
121
|
-
|
151
|
+
@association_cache.clear if persisted?
|
152
|
+
end
|
153
|
+
|
154
|
+
# :nodoc:
|
155
|
+
attr_reader :association_cache
|
156
|
+
|
157
|
+
# Returns the association instance for the given name, instantiating it if it doesn't already exist
|
158
|
+
def association(name) #:nodoc:
|
159
|
+
association = association_instance_get(name)
|
160
|
+
|
161
|
+
if association.nil?
|
162
|
+
reflection = self.class.reflect_on_association(name)
|
163
|
+
association = reflection.association_class.new(self, reflection)
|
164
|
+
association_instance_set(name, association)
|
165
|
+
end
|
166
|
+
|
167
|
+
association
|
122
168
|
end
|
123
169
|
|
124
170
|
private
|
125
171
|
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
|
126
172
|
def association_instance_get(name)
|
127
|
-
|
128
|
-
if instance_variable_defined?(ivar)
|
129
|
-
association = instance_variable_get(ivar)
|
130
|
-
association if association.respond_to?(:loaded?)
|
131
|
-
end
|
173
|
+
@association_cache[name.to_sym]
|
132
174
|
end
|
133
175
|
|
134
176
|
# Set the specified association instance.
|
135
177
|
def association_instance_set(name, association)
|
136
|
-
|
178
|
+
@association_cache[name] = association
|
137
179
|
end
|
138
180
|
|
139
181
|
# Associations are a set of macro-like class methods for tying objects together through
|
@@ -177,7 +219,7 @@ module ActiveRecord
|
|
177
219
|
# other=(other) | X | X | X
|
178
220
|
# build_other(attributes={}) | X | | X
|
179
221
|
# create_other(attributes={}) | X | | X
|
180
|
-
#
|
222
|
+
# create_other!(attributes={}) | X | | X
|
181
223
|
#
|
182
224
|
# ===Collection associations (one-to-many / many-to-many)
|
183
225
|
# | | | has_many
|
@@ -200,10 +242,9 @@ module ActiveRecord
|
|
200
242
|
# others.empty? | X | X | X
|
201
243
|
# others.clear | X | X | X
|
202
244
|
# others.delete(other,other,...) | X | X | X
|
203
|
-
# others.delete_all | X | X |
|
245
|
+
# others.delete_all | X | X | X
|
204
246
|
# others.destroy_all | X | X | X
|
205
247
|
# others.find(*args) | X | X | X
|
206
|
-
# others.find_first | X | |
|
207
248
|
# others.exists? | X | X | X
|
208
249
|
# others.uniq | X | X | X
|
209
250
|
# others.reset | X | X | X
|
@@ -317,26 +358,31 @@ module ActiveRecord
|
|
317
358
|
# === One-to-one associations
|
318
359
|
#
|
319
360
|
# * Assigning an object to a +has_one+ association automatically saves that object and
|
320
|
-
#
|
321
|
-
#
|
322
|
-
# * If either of these saves fail (due to one of the objects being invalid)
|
323
|
-
#
|
361
|
+
# the object being replaced (if there is one), in order to update their foreign
|
362
|
+
# keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
|
363
|
+
# * If either of these saves fail (due to one of the objects being invalid), an
|
364
|
+
# <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
|
365
|
+
# cancelled.
|
324
366
|
# * If you wish to assign an object to a +has_one+ association without saving it,
|
325
|
-
#
|
367
|
+
# use the <tt>build_association</tt> method (documented below). The object being
|
368
|
+
# replaced will still be saved to update its foreign key.
|
326
369
|
# * Assigning an object to a +belongs_to+ association does not save the object, since
|
327
|
-
#
|
370
|
+
# the foreign key field belongs on the parent. It does not save the parent either.
|
328
371
|
#
|
329
372
|
# === Collections
|
330
373
|
#
|
331
374
|
# * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
|
332
|
-
#
|
333
|
-
#
|
375
|
+
# saves that object, except if the parent object (the owner of the collection) is not yet
|
376
|
+
# stored in the database.
|
334
377
|
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
|
335
|
-
#
|
378
|
+
# fails, then <tt>push</tt> returns +false+.
|
379
|
+
# * If saving fails while replacing the collection (via <tt>association=</tt>), an
|
380
|
+
# <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
|
381
|
+
# cancelled.
|
336
382
|
# * You can add an object to a collection without automatically saving it by using the
|
337
|
-
#
|
383
|
+
# <tt>collection.build</tt> method (documented below).
|
338
384
|
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
|
339
|
-
#
|
385
|
+
# saved when the parent is saved.
|
340
386
|
#
|
341
387
|
# === Association callbacks
|
342
388
|
#
|
@@ -487,6 +533,65 @@ module ActiveRecord
|
|
487
533
|
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
|
488
534
|
# @group.avatars.delete(@group.avatars.last) # so would this
|
489
535
|
#
|
536
|
+
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
|
537
|
+
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
|
538
|
+
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
|
539
|
+
#
|
540
|
+
# @post = Post.first
|
541
|
+
# @tag = @post.tags.build :name => "ruby"
|
542
|
+
# @tag.save
|
543
|
+
#
|
544
|
+
# The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
|
545
|
+
# <tt>:inverse_of</tt> is set:
|
546
|
+
#
|
547
|
+
# class Taggable < ActiveRecord::Base
|
548
|
+
# belongs_to :post
|
549
|
+
# belongs_to :tag, :inverse_of => :taggings
|
550
|
+
# end
|
551
|
+
#
|
552
|
+
# === Nested Associations
|
553
|
+
#
|
554
|
+
# You can actually specify *any* association with the <tt>:through</tt> option, including an
|
555
|
+
# association which has a <tt>:through</tt> option itself. For example:
|
556
|
+
#
|
557
|
+
# class Author < ActiveRecord::Base
|
558
|
+
# has_many :posts
|
559
|
+
# has_many :comments, :through => :posts
|
560
|
+
# has_many :commenters, :through => :comments
|
561
|
+
# end
|
562
|
+
#
|
563
|
+
# class Post < ActiveRecord::Base
|
564
|
+
# has_many :comments
|
565
|
+
# end
|
566
|
+
#
|
567
|
+
# class Comment < ActiveRecord::Base
|
568
|
+
# belongs_to :commenter
|
569
|
+
# end
|
570
|
+
#
|
571
|
+
# @author = Author.first
|
572
|
+
# @author.commenters # => People who commented on posts written by the author
|
573
|
+
#
|
574
|
+
# An equivalent way of setting up this association this would be:
|
575
|
+
#
|
576
|
+
# class Author < ActiveRecord::Base
|
577
|
+
# has_many :posts
|
578
|
+
# has_many :commenters, :through => :posts
|
579
|
+
# end
|
580
|
+
#
|
581
|
+
# class Post < ActiveRecord::Base
|
582
|
+
# has_many :comments
|
583
|
+
# has_many :commenters, :through => :comments
|
584
|
+
# end
|
585
|
+
#
|
586
|
+
# class Comment < ActiveRecord::Base
|
587
|
+
# belongs_to :commenter
|
588
|
+
# end
|
589
|
+
#
|
590
|
+
# When using nested association, you will not be able to modify the association because there
|
591
|
+
# is not enough information to know what modification to make. For example, if you tried to
|
592
|
+
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
|
593
|
+
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
|
594
|
+
#
|
490
595
|
# === Polymorphic Associations
|
491
596
|
#
|
492
597
|
# Polymorphic associations on models are not restricted on what types of models they
|
@@ -756,7 +861,7 @@ module ActiveRecord
|
|
756
861
|
# belongs_to :dungeon
|
757
862
|
# end
|
758
863
|
#
|
759
|
-
# The +traps+ association on +Dungeon+ and the
|
864
|
+
# The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
|
760
865
|
# the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
|
761
866
|
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
|
762
867
|
# Active Record doesn't know anything about these inverse relationships and so no object
|
@@ -796,6 +901,73 @@ module ActiveRecord
|
|
796
901
|
# * does not work with <tt>:polymorphic</tt> associations.
|
797
902
|
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.
|
798
903
|
#
|
904
|
+
# == Deleting from associations
|
905
|
+
#
|
906
|
+
# === Dependent associations
|
907
|
+
#
|
908
|
+
# +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option.
|
909
|
+
# This allows you to specify that associated records should be deleted when the owner is
|
910
|
+
# deleted.
|
911
|
+
#
|
912
|
+
# For example:
|
913
|
+
#
|
914
|
+
# class Author
|
915
|
+
# has_many :posts, :dependent => :destroy
|
916
|
+
# end
|
917
|
+
# Author.find(1).destroy # => Will destroy all of the author's posts, too
|
918
|
+
#
|
919
|
+
# The <tt>:dependent</tt> option can have different values which specify how the deletion
|
920
|
+
# is done. For more information, see the documentation for this option on the different
|
921
|
+
# specific association types.
|
922
|
+
#
|
923
|
+
# === Delete or destroy?
|
924
|
+
#
|
925
|
+
# +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
|
926
|
+
# <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
|
927
|
+
#
|
928
|
+
# For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
|
929
|
+
# cause the records in the join table to be removed.
|
930
|
+
#
|
931
|
+
# For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the
|
932
|
+
# record(s) being removed so that callbacks are run. However <tt>delete</tt> will either
|
933
|
+
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
|
934
|
+
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
|
935
|
+
# The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
|
936
|
+
# +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
|
937
|
+
# the join records, without running their callbacks).
|
938
|
+
#
|
939
|
+
# There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
|
940
|
+
# it returns the association rather than the records which have been deleted.
|
941
|
+
#
|
942
|
+
# === What gets deleted?
|
943
|
+
#
|
944
|
+
# There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>
|
945
|
+
# associations have records in join tables, as well as the associated records. So when we
|
946
|
+
# call one of these deletion methods, what exactly should be deleted?
|
947
|
+
#
|
948
|
+
# The answer is that it is assumed that deletion on an association is about removing the
|
949
|
+
# <i>link</i> between the owner and the associated object(s), rather than necessarily the
|
950
|
+
# associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
|
951
|
+
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
|
952
|
+
#
|
953
|
+
# This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
|
954
|
+
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
|
955
|
+
# to be removed from the database.
|
956
|
+
#
|
957
|
+
# However, there are examples where this strategy doesn't make sense. For example, suppose
|
958
|
+
# a person has many projects, and each project has many tasks. If we deleted one of a person's
|
959
|
+
# tasks, we would probably not want the project to be deleted. In this scenario, the delete method
|
960
|
+
# won't actually work: it can only be used if the association on the join model is a
|
961
|
+
# +belongs_to+. In other situations you are expected to perform operations directly on
|
962
|
+
# either the associated records or the <tt>:through</tt> association.
|
963
|
+
#
|
964
|
+
# With a regular +has_many+ there is no distinction between the "associated records"
|
965
|
+
# and the "link", so there is only one choice for what gets deleted.
|
966
|
+
#
|
967
|
+
# With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the
|
968
|
+
# associated records themselves, you can always do something along the lines of
|
969
|
+
# <tt>person.tasks.each(&:destroy)</tt>.
|
970
|
+
#
|
799
971
|
# == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
|
800
972
|
#
|
801
973
|
# If you attempt to assign an object to an association that doesn't match the inferred
|
@@ -820,6 +992,10 @@ module ActiveRecord
|
|
820
992
|
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
821
993
|
# Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>,
|
822
994
|
# and deleted if they're associated with <tt>:dependent => :delete_all</tt>.
|
995
|
+
#
|
996
|
+
# If the <tt>:through</tt> option is used, then the join records are deleted (rather than
|
997
|
+
# nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or
|
998
|
+
# <tt>:dependent => :nullify</tt> to override this.
|
823
999
|
# [collection=objects]
|
824
1000
|
# Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
|
825
1001
|
# option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
|
@@ -875,7 +1051,7 @@ module ActiveRecord
|
|
875
1051
|
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
876
1052
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
877
1053
|
#
|
878
|
-
# ===
|
1054
|
+
# === Options
|
879
1055
|
# [:class_name]
|
880
1056
|
# Specify the class name of the association. Use it only if that name can't be inferred
|
881
1057
|
# from the association name. So <tt>has_many :products</tt> will by default be linked
|
@@ -903,7 +1079,9 @@ module ActiveRecord
|
|
903
1079
|
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to
|
904
1080
|
# <tt>:restrict</tt> this object cannot be deleted if it has any associated object.
|
905
1081
|
#
|
906
|
-
#
|
1082
|
+
# If using with the <tt>:through</tt> option, the association on the join model must be
|
1083
|
+
# a +belongs_to+, and the records which get deleted are the join records, rather than
|
1084
|
+
# the associated records.
|
907
1085
|
#
|
908
1086
|
# [:finder_sql]
|
909
1087
|
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
@@ -934,13 +1112,21 @@ module ActiveRecord
|
|
934
1112
|
# [:as]
|
935
1113
|
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
936
1114
|
# [:through]
|
937
|
-
# Specifies
|
938
|
-
#
|
939
|
-
#
|
940
|
-
#
|
941
|
-
#
|
942
|
-
#
|
943
|
-
#
|
1115
|
+
# Specifies an association through which to perform the query. This can be any other type
|
1116
|
+
# of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
|
1117
|
+
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
|
1118
|
+
# source reflection.
|
1119
|
+
#
|
1120
|
+
# If the association on the join model is a +belongs_to+, the collection can be modified
|
1121
|
+
# and the records on the <tt>:through</tt> model will be automatically created and removed
|
1122
|
+
# as appropriate. Otherwise, the collection is read-only, so you should manipulate the
|
1123
|
+
# <tt>:through</tt> association directly.
|
1124
|
+
#
|
1125
|
+
# If you are going to modify the association (rather than just read from it), then it is
|
1126
|
+
# a good idea to set the <tt>:inverse_of</tt> option on the source association on the
|
1127
|
+
# join model. This allows associated records to be built which will automatically create
|
1128
|
+
# the appropriate join model records when they are saved. (See the 'Association Join Models'
|
1129
|
+
# section above.)
|
944
1130
|
# [:source]
|
945
1131
|
# Specifies the source association name used by <tt>has_many :through</tt> queries.
|
946
1132
|
# Only use it if the name cannot be inferred from the association.
|
@@ -979,16 +1165,8 @@ module ActiveRecord
|
|
979
1165
|
# 'FROM people p, post_subscriptions ps ' +
|
980
1166
|
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
981
1167
|
# 'ORDER BY p.first_name'
|
982
|
-
def has_many(
|
983
|
-
|
984
|
-
configure_dependency_for_has_many(reflection)
|
985
|
-
add_association_callbacks(reflection.name, reflection.options)
|
986
|
-
|
987
|
-
if options[:through]
|
988
|
-
collection_accessor_methods(reflection, HasManyThroughAssociation)
|
989
|
-
else
|
990
|
-
collection_accessor_methods(reflection, HasManyAssociation)
|
991
|
-
end
|
1168
|
+
def has_many(name, options = {}, &extension)
|
1169
|
+
Builder::HasMany.build(self, name, options, &extension)
|
992
1170
|
end
|
993
1171
|
|
994
1172
|
# Specifies a one-to-one association with another class. This method should only be used
|
@@ -1006,12 +1184,14 @@ module ActiveRecord
|
|
1006
1184
|
# [build_association(attributes = {})]
|
1007
1185
|
# Returns a new object of the associated type that has been instantiated
|
1008
1186
|
# with +attributes+ and linked to this object through a foreign key, but has not
|
1009
|
-
# yet been saved.
|
1010
|
-
# It will NOT work if the association is +nil+.
|
1187
|
+
# yet been saved.
|
1011
1188
|
# [create_association(attributes = {})]
|
1012
1189
|
# Returns a new object of the associated type that has been instantiated
|
1013
1190
|
# with +attributes+, linked to this object through a foreign key, and that
|
1014
1191
|
# has already been saved (if it passed the validation).
|
1192
|
+
# [create_association!(attributes = {})]
|
1193
|
+
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
|
1194
|
+
# if the record is invalid.
|
1015
1195
|
#
|
1016
1196
|
# (+association+ is replaced with the symbol passed as the first argument, so
|
1017
1197
|
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
|
@@ -1023,6 +1203,7 @@ module ActiveRecord
|
|
1023
1203
|
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
1024
1204
|
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
1025
1205
|
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
1206
|
+
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
|
1026
1207
|
#
|
1027
1208
|
# === Options
|
1028
1209
|
#
|
@@ -1061,10 +1242,10 @@ module ActiveRecord
|
|
1061
1242
|
# you want to do a join but not include the joined columns. Do not forget to include the
|
1062
1243
|
# primary and foreign keys, otherwise it will raise an error.
|
1063
1244
|
# [:through]
|
1064
|
-
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt
|
1065
|
-
# and <tt>:foreign_key</tt> are ignored, as the association uses the
|
1066
|
-
# can only use a <tt>:through</tt> query through a <tt>has_one</tt>
|
1067
|
-
# association on the join model.
|
1245
|
+
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>,
|
1246
|
+
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
|
1247
|
+
# source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt>
|
1248
|
+
# or <tt>belongs_to</tt> association on the join model.
|
1068
1249
|
# [:source]
|
1069
1250
|
# Specifies the source association name used by <tt>has_one :through</tt> queries.
|
1070
1251
|
# Only use it if the name cannot be inferred from the association.
|
@@ -1097,17 +1278,8 @@ module ActiveRecord
|
|
1097
1278
|
# has_one :boss, :readonly => :true
|
1098
1279
|
# has_one :club, :through => :membership
|
1099
1280
|
# has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
|
1100
|
-
def has_one(
|
1101
|
-
|
1102
|
-
reflection = create_has_one_through_reflection(association_id, options)
|
1103
|
-
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
|
1104
|
-
else
|
1105
|
-
reflection = create_has_one_reflection(association_id, options)
|
1106
|
-
association_accessor_methods(reflection, HasOneAssociation)
|
1107
|
-
association_constructor_method(:build, reflection, HasOneAssociation)
|
1108
|
-
association_constructor_method(:create, reflection, HasOneAssociation)
|
1109
|
-
configure_dependency_for_has_one(reflection)
|
1110
|
-
end
|
1281
|
+
def has_one(name, options = {})
|
1282
|
+
Builder::HasOne.build(self, name, options)
|
1111
1283
|
end
|
1112
1284
|
|
1113
1285
|
# Specifies a one-to-one association with another class. This method should only be used
|
@@ -1129,6 +1301,9 @@ module ActiveRecord
|
|
1129
1301
|
# Returns a new object of the associated type that has been instantiated
|
1130
1302
|
# with +attributes+, linked to this object through a foreign key, and that
|
1131
1303
|
# has already been saved (if it passed the validation).
|
1304
|
+
# [create_association!(attributes = {})]
|
1305
|
+
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
|
1306
|
+
# if the record is invalid.
|
1132
1307
|
#
|
1133
1308
|
# (+association+ is replaced with the symbol passed as the first argument, so
|
1134
1309
|
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
|
@@ -1140,6 +1315,7 @@ module ActiveRecord
|
|
1140
1315
|
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
1141
1316
|
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
1142
1317
|
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
1318
|
+
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
|
1143
1319
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
1144
1320
|
#
|
1145
1321
|
# === Options
|
@@ -1161,6 +1337,11 @@ module ActiveRecord
|
|
1161
1337
|
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
|
1162
1338
|
# <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
|
1163
1339
|
# of "favorite_person_id".
|
1340
|
+
# [:foreign_type]
|
1341
|
+
# Specify the column used to store the associated object's type, if this is a polymorphic
|
1342
|
+
# association. By default this is guessed to be the name of the association with a "_type"
|
1343
|
+
# suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt>
|
1344
|
+
# association will use "taggable_type" as the default <tt>:foreign_type</tt>.
|
1164
1345
|
# [:primary_key]
|
1165
1346
|
# Specify the method that returns the primary key of associated object used for the association.
|
1166
1347
|
# By default this is id.
|
@@ -1216,21 +1397,8 @@ module ActiveRecord
|
|
1216
1397
|
# belongs_to :post, :counter_cache => true
|
1217
1398
|
# belongs_to :company, :touch => true
|
1218
1399
|
# belongs_to :company, :touch => :employees_last_updated_at
|
1219
|
-
def belongs_to(
|
1220
|
-
|
1221
|
-
|
1222
|
-
if reflection.options[:polymorphic]
|
1223
|
-
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
|
1224
|
-
else
|
1225
|
-
association_accessor_methods(reflection, BelongsToAssociation)
|
1226
|
-
association_constructor_method(:build, reflection, BelongsToAssociation)
|
1227
|
-
association_constructor_method(:create, reflection, BelongsToAssociation)
|
1228
|
-
end
|
1229
|
-
|
1230
|
-
add_counter_cache_callbacks(reflection) if options[:counter_cache]
|
1231
|
-
add_touch_callbacks(reflection, options[:touch]) if options[:touch]
|
1232
|
-
|
1233
|
-
configure_dependency_for_belongs_to(reflection)
|
1400
|
+
def belongs_to(name, options = {})
|
1401
|
+
Builder::BelongsTo.build(self, name, options)
|
1234
1402
|
end
|
1235
1403
|
|
1236
1404
|
# Specifies a many-to-many relationship with another class. This associates two classes via an
|
@@ -1261,12 +1429,6 @@ module ActiveRecord
|
|
1261
1429
|
# end
|
1262
1430
|
# end
|
1263
1431
|
#
|
1264
|
-
# Deprecated: Any additional fields added to the join table will be placed as attributes when
|
1265
|
-
# pulling records out through +has_and_belongs_to_many+ associations. Records returned from join
|
1266
|
-
# tables with additional attributes will be marked as readonly (because we can't save changes
|
1267
|
-
# to the additional attributes). It's strongly recommended that you upgrade any
|
1268
|
-
# associations with attributes to a real join model (see introduction).
|
1269
|
-
#
|
1270
1432
|
# Adds the following methods for retrieval and query:
|
1271
1433
|
#
|
1272
1434
|
# [collection(force_reload = false)]
|
@@ -1407,898 +1569,9 @@ module ActiveRecord
|
|
1407
1569
|
# has_and_belongs_to_many :categories, :readonly => true
|
1408
1570
|
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
|
1409
1571
|
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
|
1410
|
-
def has_and_belongs_to_many(
|
1411
|
-
|
1412
|
-
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
|
1413
|
-
|
1414
|
-
configure_destroy_hook_for_has_and_belongs_to_many(reflection)
|
1415
|
-
|
1416
|
-
add_association_callbacks(reflection.name, options)
|
1572
|
+
def has_and_belongs_to_many(name, options = {}, &extension)
|
1573
|
+
Builder::HasAndBelongsToMany.build(self, name, options, &extension)
|
1417
1574
|
end
|
1418
|
-
|
1419
|
-
private
|
1420
|
-
# Generates a join table name from two provided table names.
|
1421
|
-
# The names in the join table names end up in lexicographic order.
|
1422
|
-
#
|
1423
|
-
# join_table_name("members", "clubs") # => "clubs_members"
|
1424
|
-
# join_table_name("members", "special_clubs") # => "members_special_clubs"
|
1425
|
-
def join_table_name(first_table_name, second_table_name)
|
1426
|
-
if first_table_name < second_table_name
|
1427
|
-
join_table = "#{first_table_name}_#{second_table_name}"
|
1428
|
-
else
|
1429
|
-
join_table = "#{second_table_name}_#{first_table_name}"
|
1430
|
-
end
|
1431
|
-
|
1432
|
-
table_name_prefix + join_table + table_name_suffix
|
1433
|
-
end
|
1434
|
-
|
1435
|
-
def association_accessor_methods(reflection, association_proxy_class)
|
1436
|
-
redefine_method(reflection.name) do |*params|
|
1437
|
-
force_reload = params.first unless params.empty?
|
1438
|
-
association = association_instance_get(reflection.name)
|
1439
|
-
|
1440
|
-
if association.nil? || force_reload
|
1441
|
-
association = association_proxy_class.new(self, reflection)
|
1442
|
-
retval = force_reload ? reflection.klass.uncached { association.reload } : association._load
|
1443
|
-
if retval.nil? and association_proxy_class == BelongsToAssociation
|
1444
|
-
association_instance_set(reflection.name, nil)
|
1445
|
-
return nil
|
1446
|
-
end
|
1447
|
-
association_instance_set(reflection.name, association)
|
1448
|
-
end
|
1449
|
-
|
1450
|
-
association.target.nil? ? nil : association
|
1451
|
-
end
|
1452
|
-
|
1453
|
-
redefine_method("loaded_#{reflection.name}?") do
|
1454
|
-
association = association_instance_get(reflection.name)
|
1455
|
-
association && association.loaded?
|
1456
|
-
end
|
1457
|
-
|
1458
|
-
redefine_method("#{reflection.name}=") do |new_value|
|
1459
|
-
association = association_instance_get(reflection.name)
|
1460
|
-
|
1461
|
-
if association.nil? || association.target != new_value
|
1462
|
-
association = association_proxy_class.new(self, reflection)
|
1463
|
-
end
|
1464
|
-
|
1465
|
-
association.replace(new_value)
|
1466
|
-
association_instance_set(reflection.name, new_value.nil? ? nil : association)
|
1467
|
-
end
|
1468
|
-
|
1469
|
-
redefine_method("set_#{reflection.name}_target") do |target|
|
1470
|
-
return if target.nil? and association_proxy_class == BelongsToAssociation
|
1471
|
-
association = association_proxy_class.new(self, reflection)
|
1472
|
-
association.target = target
|
1473
|
-
association_instance_set(reflection.name, association)
|
1474
|
-
end
|
1475
|
-
end
|
1476
|
-
|
1477
|
-
def collection_reader_method(reflection, association_proxy_class)
|
1478
|
-
redefine_method(reflection.name) do |*params|
|
1479
|
-
force_reload = params.first unless params.empty?
|
1480
|
-
association = association_instance_get(reflection.name)
|
1481
|
-
|
1482
|
-
unless association
|
1483
|
-
association = association_proxy_class.new(self, reflection)
|
1484
|
-
association_instance_set(reflection.name, association)
|
1485
|
-
end
|
1486
|
-
|
1487
|
-
reflection.klass.uncached { association.reload } if force_reload
|
1488
|
-
|
1489
|
-
association
|
1490
|
-
end
|
1491
|
-
|
1492
|
-
redefine_method("#{reflection.name.to_s.singularize}_ids") do
|
1493
|
-
if send(reflection.name).loaded? || reflection.options[:finder_sql]
|
1494
|
-
send(reflection.name).map { |r| r.id }
|
1495
|
-
else
|
1496
|
-
if reflection.through_reflection && reflection.source_reflection.belongs_to?
|
1497
|
-
through = reflection.through_reflection
|
1498
|
-
primary_key = reflection.source_reflection.primary_key_name
|
1499
|
-
send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map! { |r| r.send(primary_key) }
|
1500
|
-
else
|
1501
|
-
send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id }
|
1502
|
-
end
|
1503
|
-
end
|
1504
|
-
end
|
1505
|
-
|
1506
|
-
end
|
1507
|
-
|
1508
|
-
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
|
1509
|
-
collection_reader_method(reflection, association_proxy_class)
|
1510
|
-
|
1511
|
-
if writer
|
1512
|
-
redefine_method("#{reflection.name}=") do |new_value|
|
1513
|
-
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
|
1514
|
-
association = send(reflection.name)
|
1515
|
-
association.replace(new_value)
|
1516
|
-
association
|
1517
|
-
end
|
1518
|
-
|
1519
|
-
redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
|
1520
|
-
pk_column = reflection.primary_key_column
|
1521
|
-
ids = (new_value || []).reject { |nid| nid.blank? }
|
1522
|
-
ids.map!{ |i| pk_column.type_cast(i) }
|
1523
|
-
send("#{reflection.name}=", reflection.klass.find(ids).index_by{ |r| r.id }.values_at(*ids))
|
1524
|
-
end
|
1525
|
-
end
|
1526
|
-
end
|
1527
|
-
|
1528
|
-
def association_constructor_method(constructor, reflection, association_proxy_class)
|
1529
|
-
redefine_method("#{constructor}_#{reflection.name}") do |*params|
|
1530
|
-
attributees = params.first unless params.empty?
|
1531
|
-
replace_existing = params[1].nil? ? true : params[1]
|
1532
|
-
association = association_instance_get(reflection.name)
|
1533
|
-
|
1534
|
-
unless association
|
1535
|
-
association = association_proxy_class.new(self, reflection)
|
1536
|
-
association_instance_set(reflection.name, association)
|
1537
|
-
end
|
1538
|
-
|
1539
|
-
if association_proxy_class == HasOneAssociation
|
1540
|
-
association.send(constructor, attributees, replace_existing)
|
1541
|
-
else
|
1542
|
-
association.send(constructor, attributees)
|
1543
|
-
end
|
1544
|
-
end
|
1545
|
-
end
|
1546
|
-
|
1547
|
-
def add_counter_cache_callbacks(reflection)
|
1548
|
-
cache_column = reflection.counter_cache_column
|
1549
|
-
|
1550
|
-
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
|
1551
|
-
define_method(method_name) do
|
1552
|
-
association = send(reflection.name)
|
1553
|
-
association.class.increment_counter(cache_column, association.id) unless association.nil?
|
1554
|
-
end
|
1555
|
-
after_create(method_name)
|
1556
|
-
|
1557
|
-
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
|
1558
|
-
define_method(method_name) do
|
1559
|
-
association = send(reflection.name)
|
1560
|
-
association.class.decrement_counter(cache_column, association.id) unless association.nil?
|
1561
|
-
end
|
1562
|
-
before_destroy(method_name)
|
1563
|
-
|
1564
|
-
module_eval(
|
1565
|
-
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__
|
1566
|
-
)
|
1567
|
-
end
|
1568
|
-
|
1569
|
-
def add_touch_callbacks(reflection, touch_attribute)
|
1570
|
-
method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}"
|
1571
|
-
redefine_method(method_name) do
|
1572
|
-
association = send(reflection.name)
|
1573
|
-
|
1574
|
-
if touch_attribute == true
|
1575
|
-
association.touch unless association.nil?
|
1576
|
-
else
|
1577
|
-
association.touch(touch_attribute) unless association.nil?
|
1578
|
-
end
|
1579
|
-
end
|
1580
|
-
after_save(method_name)
|
1581
|
-
after_touch(method_name)
|
1582
|
-
after_destroy(method_name)
|
1583
|
-
end
|
1584
|
-
|
1585
|
-
# Creates before_destroy callback methods that nullify, delete or destroy
|
1586
|
-
# has_many associated objects, according to the defined :dependent rule.
|
1587
|
-
# If the association is marked as :dependent => :restrict, create a callback
|
1588
|
-
# that prevents deleting entirely.
|
1589
|
-
#
|
1590
|
-
# See HasManyAssociation#delete_records. Dependent associations
|
1591
|
-
# delete children, otherwise foreign key is set to NULL.
|
1592
|
-
# See HasManyAssociation#delete_records. Dependent associations
|
1593
|
-
# delete children if the option is set to :destroy or :delete_all, set the
|
1594
|
-
# foreign key to NULL if the option is set to :nullify, and do not touch the
|
1595
|
-
# child records if the option is set to :restrict.
|
1596
|
-
#
|
1597
|
-
# The +extra_conditions+ parameter, which is not used within the main
|
1598
|
-
# Active Record codebase, is meant to allow plugins to define extra
|
1599
|
-
# finder conditions.
|
1600
|
-
def configure_dependency_for_has_many(reflection, extra_conditions = nil)
|
1601
|
-
if reflection.options.include?(:dependent)
|
1602
|
-
case reflection.options[:dependent]
|
1603
|
-
when :destroy
|
1604
|
-
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
1605
|
-
define_method(method_name) do
|
1606
|
-
send(reflection.name).each do |o|
|
1607
|
-
# No point in executing the counter update since we're going to destroy the parent anyway
|
1608
|
-
counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
|
1609
|
-
if(o.respond_to? counter_method) then
|
1610
|
-
class << o
|
1611
|
-
self
|
1612
|
-
end.send(:define_method, counter_method, Proc.new {})
|
1613
|
-
end
|
1614
|
-
o.destroy
|
1615
|
-
end
|
1616
|
-
end
|
1617
|
-
before_destroy method_name
|
1618
|
-
when :delete_all
|
1619
|
-
before_destroy do |record|
|
1620
|
-
self.class.send(:delete_all_has_many_dependencies,
|
1621
|
-
record,
|
1622
|
-
reflection.name,
|
1623
|
-
reflection.klass,
|
1624
|
-
reflection.dependent_conditions(record, self.class, extra_conditions))
|
1625
|
-
end
|
1626
|
-
when :nullify
|
1627
|
-
before_destroy do |record|
|
1628
|
-
self.class.send(:nullify_has_many_dependencies,
|
1629
|
-
record,
|
1630
|
-
reflection.name,
|
1631
|
-
reflection.klass,
|
1632
|
-
reflection.primary_key_name,
|
1633
|
-
reflection.dependent_conditions(record, self.class, extra_conditions))
|
1634
|
-
end
|
1635
|
-
when :restrict
|
1636
|
-
method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym
|
1637
|
-
define_method(method_name) do
|
1638
|
-
unless send(reflection.name).empty?
|
1639
|
-
raise DeleteRestrictionError.new(reflection)
|
1640
|
-
end
|
1641
|
-
end
|
1642
|
-
before_destroy method_name
|
1643
|
-
else
|
1644
|
-
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
|
1645
|
-
end
|
1646
|
-
end
|
1647
|
-
end
|
1648
|
-
|
1649
|
-
# Creates before_destroy callback methods that nullify, delete or destroy
|
1650
|
-
# has_one associated objects, according to the defined :dependent rule.
|
1651
|
-
# If the association is marked as :dependent => :restrict, create a callback
|
1652
|
-
# that prevents deleting entirely.
|
1653
|
-
def configure_dependency_for_has_one(reflection)
|
1654
|
-
if reflection.options.include?(:dependent)
|
1655
|
-
name = reflection.options[:dependent]
|
1656
|
-
method_name = :"has_one_dependent_#{name}_for_#{reflection.name}"
|
1657
|
-
|
1658
|
-
case name
|
1659
|
-
when :destroy, :delete
|
1660
|
-
class_eval <<-eoruby, __FILE__, __LINE__ + 1
|
1661
|
-
def #{method_name}
|
1662
|
-
association = #{reflection.name}
|
1663
|
-
association.#{name} if association
|
1664
|
-
end
|
1665
|
-
eoruby
|
1666
|
-
when :nullify
|
1667
|
-
class_eval <<-eoruby, __FILE__, __LINE__ + 1
|
1668
|
-
def #{method_name}
|
1669
|
-
association = #{reflection.name}
|
1670
|
-
association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association
|
1671
|
-
end
|
1672
|
-
eoruby
|
1673
|
-
when :restrict
|
1674
|
-
method_name = "has_one_dependent_restrict_for_#{reflection.name}".to_sym
|
1675
|
-
define_method(method_name) do
|
1676
|
-
unless send(reflection.name).nil?
|
1677
|
-
raise DeleteRestrictionError.new(reflection)
|
1678
|
-
end
|
1679
|
-
end
|
1680
|
-
before_destroy method_name
|
1681
|
-
else
|
1682
|
-
raise ArgumentError, "The :dependent option expects either :destroy, :delete, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
|
1683
|
-
end
|
1684
|
-
|
1685
|
-
before_destroy method_name
|
1686
|
-
end
|
1687
|
-
end
|
1688
|
-
|
1689
|
-
def configure_dependency_for_belongs_to(reflection)
|
1690
|
-
if reflection.options.include?(:dependent)
|
1691
|
-
name = reflection.options[:dependent]
|
1692
|
-
|
1693
|
-
unless [:destroy, :delete].include?(name)
|
1694
|
-
raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
|
1695
|
-
end
|
1696
|
-
|
1697
|
-
method_name = :"belongs_to_dependent_#{name}_for_#{reflection.name}"
|
1698
|
-
class_eval <<-eoruby, __FILE__, __LINE__ + 1
|
1699
|
-
def #{method_name}
|
1700
|
-
association = #{reflection.name}
|
1701
|
-
association.#{name} if association
|
1702
|
-
end
|
1703
|
-
eoruby
|
1704
|
-
after_destroy method_name
|
1705
|
-
end
|
1706
|
-
end
|
1707
|
-
|
1708
|
-
def configure_destroy_hook_for_has_and_belongs_to_many(reflection)
|
1709
|
-
include(Module.new {
|
1710
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
1711
|
-
def destroy_associations
|
1712
|
-
association = #{reflection.name}
|
1713
|
-
association.delete_all if association
|
1714
|
-
super
|
1715
|
-
end
|
1716
|
-
RUBY
|
1717
|
-
})
|
1718
|
-
end
|
1719
|
-
|
1720
|
-
def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions)
|
1721
|
-
association_class.delete_all(dependent_conditions)
|
1722
|
-
end
|
1723
|
-
|
1724
|
-
def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
|
1725
|
-
association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
|
1726
|
-
end
|
1727
|
-
|
1728
|
-
mattr_accessor :valid_keys_for_has_many_association
|
1729
|
-
@@valid_keys_for_has_many_association = [
|
1730
|
-
:class_name, :table_name, :foreign_key, :primary_key,
|
1731
|
-
:dependent,
|
1732
|
-
:select, :conditions, :include, :order, :group, :having, :limit, :offset,
|
1733
|
-
:as, :through, :source, :source_type,
|
1734
|
-
:uniq,
|
1735
|
-
:finder_sql, :counter_sql,
|
1736
|
-
:before_add, :after_add, :before_remove, :after_remove,
|
1737
|
-
:extend, :readonly,
|
1738
|
-
:validate, :inverse_of
|
1739
|
-
]
|
1740
|
-
|
1741
|
-
def create_has_many_reflection(association_id, options, &extension)
|
1742
|
-
options.assert_valid_keys(valid_keys_for_has_many_association)
|
1743
|
-
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
1744
|
-
|
1745
|
-
create_reflection(:has_many, association_id, options, self)
|
1746
|
-
end
|
1747
|
-
|
1748
|
-
mattr_accessor :valid_keys_for_has_one_association
|
1749
|
-
@@valid_keys_for_has_one_association = [
|
1750
|
-
:class_name, :foreign_key, :remote, :select, :conditions, :order,
|
1751
|
-
:include, :dependent, :counter_cache, :extend, :as, :readonly,
|
1752
|
-
:validate, :primary_key, :inverse_of
|
1753
|
-
]
|
1754
|
-
|
1755
|
-
def create_has_one_reflection(association_id, options)
|
1756
|
-
options.assert_valid_keys(valid_keys_for_has_one_association)
|
1757
|
-
create_reflection(:has_one, association_id, options, self)
|
1758
|
-
end
|
1759
|
-
|
1760
|
-
def create_has_one_through_reflection(association_id, options)
|
1761
|
-
options.assert_valid_keys(
|
1762
|
-
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
|
1763
|
-
)
|
1764
|
-
create_reflection(:has_one, association_id, options, self)
|
1765
|
-
end
|
1766
|
-
|
1767
|
-
mattr_accessor :valid_keys_for_belongs_to_association
|
1768
|
-
@@valid_keys_for_belongs_to_association = [
|
1769
|
-
:class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
|
1770
|
-
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
|
1771
|
-
:validate, :touch, :inverse_of
|
1772
|
-
]
|
1773
|
-
|
1774
|
-
def create_belongs_to_reflection(association_id, options)
|
1775
|
-
options.assert_valid_keys(valid_keys_for_belongs_to_association)
|
1776
|
-
reflection = create_reflection(:belongs_to, association_id, options, self)
|
1777
|
-
|
1778
|
-
if options[:polymorphic]
|
1779
|
-
reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
|
1780
|
-
end
|
1781
|
-
|
1782
|
-
reflection
|
1783
|
-
end
|
1784
|
-
|
1785
|
-
mattr_accessor :valid_keys_for_has_and_belongs_to_many_association
|
1786
|
-
@@valid_keys_for_has_and_belongs_to_many_association = [
|
1787
|
-
:class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
|
1788
|
-
:select, :conditions, :include, :order, :group, :having, :limit, :offset,
|
1789
|
-
:uniq,
|
1790
|
-
:finder_sql, :counter_sql, :delete_sql, :insert_sql,
|
1791
|
-
:before_add, :after_add, :before_remove, :after_remove,
|
1792
|
-
:extend, :readonly,
|
1793
|
-
:validate
|
1794
|
-
]
|
1795
|
-
|
1796
|
-
def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
1797
|
-
options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association)
|
1798
|
-
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
1799
|
-
|
1800
|
-
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
|
1801
|
-
|
1802
|
-
if reflection.association_foreign_key == reflection.primary_key_name
|
1803
|
-
raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
|
1804
|
-
end
|
1805
|
-
|
1806
|
-
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
|
1807
|
-
if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false)
|
1808
|
-
raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
|
1809
|
-
end
|
1810
|
-
|
1811
|
-
reflection
|
1812
|
-
end
|
1813
|
-
|
1814
|
-
def add_association_callbacks(association_name, options)
|
1815
|
-
callbacks = %w(before_add after_add before_remove after_remove)
|
1816
|
-
callbacks.each do |callback_name|
|
1817
|
-
full_callback_name = "#{callback_name}_for_#{association_name}"
|
1818
|
-
defined_callbacks = options[callback_name.to_sym]
|
1819
|
-
if options.has_key?(callback_name.to_sym)
|
1820
|
-
class_inheritable_reader full_callback_name.to_sym
|
1821
|
-
write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten)
|
1822
|
-
else
|
1823
|
-
write_inheritable_attribute(full_callback_name.to_sym, [])
|
1824
|
-
end
|
1825
|
-
end
|
1826
|
-
end
|
1827
|
-
|
1828
|
-
def create_extension_modules(association_id, block_extension, extensions)
|
1829
|
-
if block_extension
|
1830
|
-
extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension"
|
1831
|
-
|
1832
|
-
silence_warnings do
|
1833
|
-
self.parent.const_set(extension_module_name, Module.new(&block_extension))
|
1834
|
-
end
|
1835
|
-
Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize)
|
1836
|
-
else
|
1837
|
-
Array.wrap(extensions)
|
1838
|
-
end
|
1839
|
-
end
|
1840
|
-
|
1841
|
-
class JoinDependency # :nodoc:
|
1842
|
-
attr_reader :joins, :reflections, :table_aliases
|
1843
|
-
|
1844
|
-
def initialize(base, associations, joins)
|
1845
|
-
@joins = [JoinBase.new(base, joins)]
|
1846
|
-
@associations = {}
|
1847
|
-
@reflections = []
|
1848
|
-
@base_records_hash = {}
|
1849
|
-
@base_records_in_order = []
|
1850
|
-
@table_aliases = Hash.new { |aliases, table| aliases[table] = 0 }
|
1851
|
-
@table_aliases[base.table_name] = 1
|
1852
|
-
build(associations)
|
1853
|
-
end
|
1854
|
-
|
1855
|
-
def graft(*associations)
|
1856
|
-
associations.each do |association|
|
1857
|
-
join_associations.detect {|a| association == a} ||
|
1858
|
-
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
|
1859
|
-
end
|
1860
|
-
self
|
1861
|
-
end
|
1862
|
-
|
1863
|
-
def join_associations
|
1864
|
-
@joins[1..-1].to_a
|
1865
|
-
end
|
1866
|
-
|
1867
|
-
def join_base
|
1868
|
-
@joins[0]
|
1869
|
-
end
|
1870
|
-
|
1871
|
-
def count_aliases_from_table_joins(name)
|
1872
|
-
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
|
1873
|
-
quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase
|
1874
|
-
join_sql = join_base.table_joins.to_s.downcase
|
1875
|
-
join_sql.blank? ? 0 :
|
1876
|
-
# Table names
|
1877
|
-
join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
|
1878
|
-
# Table aliases
|
1879
|
-
join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
|
1880
|
-
end
|
1881
|
-
|
1882
|
-
def instantiate(rows)
|
1883
|
-
rows.each_with_index do |row, i|
|
1884
|
-
primary_id = join_base.record_id(row)
|
1885
|
-
unless @base_records_hash[primary_id]
|
1886
|
-
@base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
|
1887
|
-
end
|
1888
|
-
construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
|
1889
|
-
end
|
1890
|
-
remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations)
|
1891
|
-
return @base_records_in_order
|
1892
|
-
end
|
1893
|
-
|
1894
|
-
def remove_duplicate_results!(base, records, associations)
|
1895
|
-
case associations
|
1896
|
-
when Symbol, String
|
1897
|
-
reflection = base.reflections[associations]
|
1898
|
-
remove_uniq_by_reflection(reflection, records)
|
1899
|
-
when Array
|
1900
|
-
associations.each do |association|
|
1901
|
-
remove_duplicate_results!(base, records, association)
|
1902
|
-
end
|
1903
|
-
when Hash
|
1904
|
-
associations.keys.each do |name|
|
1905
|
-
reflection = base.reflections[name]
|
1906
|
-
remove_uniq_by_reflection(reflection, records)
|
1907
|
-
|
1908
|
-
parent_records = []
|
1909
|
-
records.each do |record|
|
1910
|
-
if descendant = record.send(reflection.name)
|
1911
|
-
if reflection.collection?
|
1912
|
-
parent_records.concat descendant.target.uniq
|
1913
|
-
else
|
1914
|
-
parent_records << descendant
|
1915
|
-
end
|
1916
|
-
end
|
1917
|
-
end
|
1918
|
-
|
1919
|
-
remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
|
1920
|
-
end
|
1921
|
-
end
|
1922
|
-
end
|
1923
|
-
|
1924
|
-
protected
|
1925
|
-
|
1926
|
-
def cache_joined_association(association)
|
1927
|
-
associations = []
|
1928
|
-
parent = association.parent
|
1929
|
-
while parent != join_base
|
1930
|
-
associations.unshift(parent.reflection.name)
|
1931
|
-
parent = parent.parent
|
1932
|
-
end
|
1933
|
-
ref = @associations
|
1934
|
-
associations.each do |key|
|
1935
|
-
ref = ref[key]
|
1936
|
-
end
|
1937
|
-
ref[association.reflection.name] ||= {}
|
1938
|
-
end
|
1939
|
-
|
1940
|
-
def build(associations, parent = nil, join_type = Arel::InnerJoin)
|
1941
|
-
parent ||= @joins.last
|
1942
|
-
case associations
|
1943
|
-
when Symbol, String
|
1944
|
-
reflection = parent.reflections[associations.to_s.intern] or
|
1945
|
-
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
|
1946
|
-
unless join_association = find_join_association(reflection, parent)
|
1947
|
-
@reflections << reflection
|
1948
|
-
join_association = build_join_association(reflection, parent)
|
1949
|
-
join_association.join_type = join_type
|
1950
|
-
@joins << join_association
|
1951
|
-
cache_joined_association(join_association)
|
1952
|
-
end
|
1953
|
-
join_association
|
1954
|
-
when Array
|
1955
|
-
associations.each do |association|
|
1956
|
-
build(association, parent, join_type)
|
1957
|
-
end
|
1958
|
-
when Hash
|
1959
|
-
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
1960
|
-
join_association = build(name, parent, join_type)
|
1961
|
-
build(associations[name], join_association, join_type)
|
1962
|
-
end
|
1963
|
-
else
|
1964
|
-
raise ConfigurationError, associations.inspect
|
1965
|
-
end
|
1966
|
-
end
|
1967
|
-
|
1968
|
-
def find_join_association(name_or_reflection, parent)
|
1969
|
-
case name_or_reflection
|
1970
|
-
when Symbol, String
|
1971
|
-
join_associations.detect {|j| (j.reflection.name == name_or_reflection.to_s.intern) && (j.parent == parent)}
|
1972
|
-
else
|
1973
|
-
join_associations.detect {|j| (j.reflection == name_or_reflection) && (j.parent == parent)}
|
1974
|
-
end
|
1975
|
-
end
|
1976
|
-
|
1977
|
-
def remove_uniq_by_reflection(reflection, records)
|
1978
|
-
if reflection && reflection.collection?
|
1979
|
-
records.each { |record| record.send(reflection.name).target.uniq! }
|
1980
|
-
end
|
1981
|
-
end
|
1982
|
-
|
1983
|
-
def build_join_association(reflection, parent)
|
1984
|
-
JoinAssociation.new(reflection, self, parent)
|
1985
|
-
end
|
1986
|
-
|
1987
|
-
def construct(parent, associations, joins, row)
|
1988
|
-
case associations
|
1989
|
-
when Symbol, String
|
1990
|
-
join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
|
1991
|
-
raise(ConfigurationError, "No such association") if join.nil?
|
1992
|
-
|
1993
|
-
joins.delete(join)
|
1994
|
-
construct_association(parent, join, row)
|
1995
|
-
when Array
|
1996
|
-
associations.each do |association|
|
1997
|
-
construct(parent, association, joins, row)
|
1998
|
-
end
|
1999
|
-
when Hash
|
2000
|
-
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
2001
|
-
join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
|
2002
|
-
raise(ConfigurationError, "No such association") if join.nil?
|
2003
|
-
|
2004
|
-
association = construct_association(parent, join, row)
|
2005
|
-
joins.delete(join)
|
2006
|
-
construct(association, associations[name], joins, row) if association
|
2007
|
-
end
|
2008
|
-
else
|
2009
|
-
raise ConfigurationError, associations.inspect
|
2010
|
-
end
|
2011
|
-
end
|
2012
|
-
|
2013
|
-
def construct_association(record, join, row)
|
2014
|
-
case join.reflection.macro
|
2015
|
-
when :has_many, :has_and_belongs_to_many
|
2016
|
-
collection = record.send(join.reflection.name)
|
2017
|
-
collection.loaded
|
2018
|
-
|
2019
|
-
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
2020
|
-
association = join.instantiate(row)
|
2021
|
-
collection.target.push(association)
|
2022
|
-
collection.__send__(:set_inverse_instance, association, record)
|
2023
|
-
when :has_one
|
2024
|
-
return if record.id.to_s != join.parent.record_id(row).to_s
|
2025
|
-
return if record.instance_variable_defined?("@#{join.reflection.name}")
|
2026
|
-
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
|
2027
|
-
set_target_and_inverse(join, association, record)
|
2028
|
-
when :belongs_to
|
2029
|
-
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
2030
|
-
association = join.instantiate(row)
|
2031
|
-
set_target_and_inverse(join, association, record)
|
2032
|
-
else
|
2033
|
-
raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
|
2034
|
-
end
|
2035
|
-
return association
|
2036
|
-
end
|
2037
|
-
|
2038
|
-
def set_target_and_inverse(join, association, record)
|
2039
|
-
association_proxy = record.send("set_#{join.reflection.name}_target", association)
|
2040
|
-
association_proxy.__send__(:set_inverse_instance, association, record)
|
2041
|
-
end
|
2042
|
-
|
2043
|
-
class JoinBase # :nodoc:
|
2044
|
-
attr_reader :active_record, :table_joins
|
2045
|
-
delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record
|
2046
|
-
|
2047
|
-
def initialize(active_record, joins = nil)
|
2048
|
-
@active_record = active_record
|
2049
|
-
@cached_record = {}
|
2050
|
-
@table_joins = joins
|
2051
|
-
end
|
2052
|
-
|
2053
|
-
def ==(other)
|
2054
|
-
other.class == self.class &&
|
2055
|
-
other.active_record == active_record
|
2056
|
-
end
|
2057
|
-
|
2058
|
-
def aliased_prefix
|
2059
|
-
"t0"
|
2060
|
-
end
|
2061
|
-
|
2062
|
-
def aliased_primary_key
|
2063
|
-
"#{aliased_prefix}_r0"
|
2064
|
-
end
|
2065
|
-
|
2066
|
-
def aliased_table_name
|
2067
|
-
active_record.table_name
|
2068
|
-
end
|
2069
|
-
|
2070
|
-
def column_names_with_alias
|
2071
|
-
unless defined?(@column_names_with_alias)
|
2072
|
-
@column_names_with_alias = []
|
2073
|
-
|
2074
|
-
([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
|
2075
|
-
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
|
2076
|
-
end
|
2077
|
-
end
|
2078
|
-
|
2079
|
-
@column_names_with_alias
|
2080
|
-
end
|
2081
|
-
|
2082
|
-
def extract_record(row)
|
2083
|
-
Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
|
2084
|
-
end
|
2085
|
-
|
2086
|
-
def record_id(row)
|
2087
|
-
row[aliased_primary_key]
|
2088
|
-
end
|
2089
|
-
|
2090
|
-
def instantiate(row)
|
2091
|
-
@cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
|
2092
|
-
end
|
2093
|
-
end
|
2094
|
-
|
2095
|
-
class JoinAssociation < JoinBase # :nodoc:
|
2096
|
-
attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name
|
2097
|
-
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
|
2098
|
-
attr_accessor :join_type
|
2099
|
-
delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
|
2100
|
-
|
2101
|
-
def initialize(reflection, join_dependency, parent = nil)
|
2102
|
-
reflection.check_validity!
|
2103
|
-
if reflection.options[:polymorphic]
|
2104
|
-
raise EagerLoadPolymorphicError.new(reflection)
|
2105
|
-
end
|
2106
|
-
|
2107
|
-
super(reflection.klass)
|
2108
|
-
@join_dependency = join_dependency
|
2109
|
-
@parent = parent
|
2110
|
-
@reflection = reflection
|
2111
|
-
@aliased_prefix = "t#{ join_dependency.joins.size }"
|
2112
|
-
@parent_table_name = parent.active_record.table_name
|
2113
|
-
@aliased_table_name = aliased_table_name_for(table_name)
|
2114
|
-
@join = nil
|
2115
|
-
@join_type = Arel::InnerJoin
|
2116
|
-
|
2117
|
-
if reflection.macro == :has_and_belongs_to_many
|
2118
|
-
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
|
2119
|
-
end
|
2120
|
-
|
2121
|
-
if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
|
2122
|
-
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
|
2123
|
-
end
|
2124
|
-
end
|
2125
|
-
|
2126
|
-
def ==(other)
|
2127
|
-
other.class == self.class &&
|
2128
|
-
other.reflection == reflection &&
|
2129
|
-
other.parent == parent
|
2130
|
-
end
|
2131
|
-
|
2132
|
-
def find_parent_in(other_join_dependency)
|
2133
|
-
other_join_dependency.joins.detect do |join|
|
2134
|
-
self.parent == join
|
2135
|
-
end
|
2136
|
-
end
|
2137
|
-
|
2138
|
-
def association_join
|
2139
|
-
return @join if @join
|
2140
|
-
|
2141
|
-
aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name,
|
2142
|
-
:engine => arel_engine,
|
2143
|
-
:columns => klass.columns)
|
2144
|
-
|
2145
|
-
parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name,
|
2146
|
-
:engine => arel_engine,
|
2147
|
-
:columns => parent.active_record.columns)
|
2148
|
-
as_conditions = reflection.options[:conditions] && process_conditions(reflection.options[:conditions], aliased_table_name)
|
2149
|
-
|
2150
|
-
@join = case reflection.macro
|
2151
|
-
when :has_and_belongs_to_many
|
2152
|
-
join_table = Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine)
|
2153
|
-
fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
|
2154
|
-
klass_fk = options[:association_foreign_key] || klass.to_s.foreign_key
|
2155
|
-
|
2156
|
-
[
|
2157
|
-
join_table[fk].eq(parent_table[reflection.active_record.primary_key]),
|
2158
|
-
[aliased_table[klass.primary_key].eq(join_table[klass_fk]), as_conditions].reject{ |x| x.blank? }
|
2159
|
-
]
|
2160
|
-
when :has_many, :has_one
|
2161
|
-
if reflection.options[:through]
|
2162
|
-
join_table = Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine)
|
2163
|
-
jt_as_conditions = through_reflection.options[:conditions] && process_conditions(through_reflection.options[:conditions], aliased_table_name)
|
2164
|
-
jt_as_extra = jt_source_extra = jt_sti_extra = nil
|
2165
|
-
first_key = second_key = as_extra = nil
|
2166
|
-
|
2167
|
-
if through_reflection.macro == :belongs_to
|
2168
|
-
jt_primary_key = through_reflection.primary_key_name
|
2169
|
-
jt_foreign_key = through_reflection.association_primary_key
|
2170
|
-
else
|
2171
|
-
jt_primary_key = through_reflection.active_record_primary_key
|
2172
|
-
jt_foreign_key = through_reflection.primary_key_name
|
2173
|
-
|
2174
|
-
if through_reflection.options[:as] # has_many :through against a polymorphic join
|
2175
|
-
jt_as_extra = join_table[through_reflection.options[:as].to_s + '_type'].eq(parent.active_record.base_class.name)
|
2176
|
-
end
|
2177
|
-
end
|
2178
|
-
|
2179
|
-
case source_reflection.macro
|
2180
|
-
when :has_many, :has_one
|
2181
|
-
if source_reflection.options[:as]
|
2182
|
-
first_key = "#{source_reflection.options[:as]}_id"
|
2183
|
-
second_key = options[:foreign_key] || primary_key
|
2184
|
-
as_extra = aliased_table["#{source_reflection.options[:as]}_type"].eq(source_reflection.active_record.base_class.name)
|
2185
|
-
else
|
2186
|
-
first_key = through_reflection.klass.base_class.to_s.foreign_key
|
2187
|
-
second_key = options[:foreign_key] || primary_key
|
2188
|
-
end
|
2189
|
-
|
2190
|
-
unless through_reflection.klass.descends_from_active_record?
|
2191
|
-
jt_sti_extra = join_table[through_reflection.active_record.inheritance_column].eq(through_reflection.klass.sti_name)
|
2192
|
-
end
|
2193
|
-
when :belongs_to
|
2194
|
-
first_key = primary_key
|
2195
|
-
if reflection.options[:source_type]
|
2196
|
-
second_key = source_reflection.association_foreign_key
|
2197
|
-
jt_source_extra = join_table[reflection.source_reflection.options[:foreign_type]].eq(reflection.options[:source_type])
|
2198
|
-
else
|
2199
|
-
second_key = source_reflection.primary_key_name
|
2200
|
-
end
|
2201
|
-
end
|
2202
|
-
|
2203
|
-
[
|
2204
|
-
[parent_table[jt_primary_key].eq(join_table[jt_foreign_key]), jt_as_extra, jt_source_extra, jt_sti_extra, jt_as_conditions].reject{|x| x.blank? },
|
2205
|
-
[aliased_table[first_key].eq(join_table[second_key]), as_extra, as_conditions].reject{ |x| x.blank? }
|
2206
|
-
]
|
2207
|
-
elsif reflection.options[:as]
|
2208
|
-
id_rel = aliased_table["#{reflection.options[:as]}_id"].eq(parent_table[parent.primary_key])
|
2209
|
-
type_rel = aliased_table["#{reflection.options[:as]}_type"].eq(parent.active_record.base_class.name)
|
2210
|
-
[id_rel, type_rel, as_conditions].reject{ |x| x.blank?}
|
2211
|
-
else
|
2212
|
-
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
|
2213
|
-
[aliased_table[foreign_key].eq(parent_table[reflection.options[:primary_key] || parent.primary_key]), as_conditions].reject{ |x| x.blank? }
|
2214
|
-
end
|
2215
|
-
when :belongs_to
|
2216
|
-
[aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name]), as_conditions].reject{ |x| x.blank? }
|
2217
|
-
end
|
2218
|
-
|
2219
|
-
unless klass.descends_from_active_record?
|
2220
|
-
sti_column = aliased_table[klass.inheritance_column]
|
2221
|
-
sti_condition = sti_column.eq(klass.sti_name)
|
2222
|
-
klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
|
2223
|
-
|
2224
|
-
@join << sti_condition
|
2225
|
-
end
|
2226
|
-
|
2227
|
-
@join
|
2228
|
-
end
|
2229
|
-
|
2230
|
-
def relation
|
2231
|
-
aliased = Arel::Table.new(table_name, :as => @aliased_table_name,
|
2232
|
-
:engine => arel_engine,
|
2233
|
-
:columns => klass.columns)
|
2234
|
-
|
2235
|
-
if reflection.macro == :has_and_belongs_to_many
|
2236
|
-
[Arel::Table.new(options[:join_table], :as => aliased_join_table_name, :engine => arel_engine), aliased]
|
2237
|
-
elsif reflection.options[:through]
|
2238
|
-
[Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name, :engine => arel_engine), aliased]
|
2239
|
-
else
|
2240
|
-
aliased
|
2241
|
-
end
|
2242
|
-
end
|
2243
|
-
|
2244
|
-
def join_relation(joining_relation)
|
2245
|
-
self.join_type = Arel::OuterJoin
|
2246
|
-
joining_relation.joins(self)
|
2247
|
-
end
|
2248
|
-
|
2249
|
-
protected
|
2250
|
-
|
2251
|
-
def aliased_table_name_for(name, suffix = nil)
|
2252
|
-
if @join_dependency.table_aliases[name].zero?
|
2253
|
-
@join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
|
2254
|
-
end
|
2255
|
-
|
2256
|
-
if !@join_dependency.table_aliases[name].zero? # We need an alias
|
2257
|
-
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
2258
|
-
@join_dependency.table_aliases[name] += 1
|
2259
|
-
if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
|
2260
|
-
# Also need to count the aliases from the table_aliases to avoid incorrect count
|
2261
|
-
@join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
|
2262
|
-
end
|
2263
|
-
table_index = @join_dependency.table_aliases[name]
|
2264
|
-
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
|
2265
|
-
else
|
2266
|
-
@join_dependency.table_aliases[name] += 1
|
2267
|
-
end
|
2268
|
-
|
2269
|
-
name
|
2270
|
-
end
|
2271
|
-
|
2272
|
-
def pluralize(table_name)
|
2273
|
-
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
2274
|
-
end
|
2275
|
-
|
2276
|
-
def table_alias_for(table_name, table_alias)
|
2277
|
-
"#{table_name} #{table_alias if table_name != table_alias}".strip
|
2278
|
-
end
|
2279
|
-
|
2280
|
-
def table_name_and_alias
|
2281
|
-
table_alias_for table_name, @aliased_table_name
|
2282
|
-
end
|
2283
|
-
|
2284
|
-
def process_conditions(conditions, table_name)
|
2285
|
-
sanitized = sanitize_sql(conditions, table_name)
|
2286
|
-
|
2287
|
-
if sanitized =~ /\#\{.*\}/
|
2288
|
-
ActiveSupport::Deprecation.warn(
|
2289
|
-
'String-based interpolation of association conditions is deprecated. Please use a ' \
|
2290
|
-
'proc instead. So, for example, has_many :older_friends, :conditions => \'age > #{age}\' ' \
|
2291
|
-
'should be changed to has_many :older_friends, :conditions => proc { "age > #{age}" }.'
|
2292
|
-
)
|
2293
|
-
instance_eval("%@#{sanitized.gsub('@', '\@')}@", __FILE__, __LINE__)
|
2294
|
-
elsif conditions.respond_to?(:to_proc)
|
2295
|
-
conditions = sanitize_sql(instance_eval(&conditions), table_name)
|
2296
|
-
else
|
2297
|
-
sanitized
|
2298
|
-
end
|
2299
|
-
end
|
2300
|
-
end
|
2301
|
-
end
|
2302
1575
|
end
|
2303
1576
|
end
|
2304
1577
|
end
|