activerecord 2.0.5 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +168 -6
- data/README +27 -22
- data/RUNNING_UNIT_TESTS +7 -4
- data/Rakefile +22 -25
- data/lib/active_record.rb +8 -2
- data/lib/active_record/aggregations.rb +21 -12
- data/lib/active_record/association_preload.rb +277 -0
- data/lib/active_record/associations.rb +481 -295
- data/lib/active_record/associations/association_collection.rb +162 -37
- data/lib/active_record/associations/association_proxy.rb +71 -7
- data/lib/active_record/associations/belongs_to_association.rb +5 -3
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +5 -6
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +12 -64
- data/lib/active_record/associations/has_many_association.rb +8 -73
- data/lib/active_record/associations/has_many_through_association.rb +68 -117
- data/lib/active_record/associations/has_one_association.rb +7 -5
- data/lib/active_record/associations/has_one_through_association.rb +28 -0
- data/lib/active_record/attribute_methods.rb +69 -19
- data/lib/active_record/base.rb +496 -275
- data/lib/active_record/calculations.rb +28 -21
- data/lib/active_record/callbacks.rb +9 -38
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -2
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +232 -45
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +141 -27
- data/lib/active_record/connection_adapters/abstract_adapter.rb +9 -13
- data/lib/active_record/connection_adapters/mysql_adapter.rb +57 -24
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +143 -42
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +18 -10
- data/lib/active_record/dirty.rb +158 -0
- data/lib/active_record/fixtures.rb +121 -156
- data/lib/active_record/locking/optimistic.rb +14 -11
- data/lib/active_record/locking/pessimistic.rb +2 -2
- data/lib/active_record/migration.rb +157 -77
- data/lib/active_record/named_scope.rb +163 -0
- data/lib/active_record/observer.rb +19 -5
- data/lib/active_record/reflection.rb +34 -14
- data/lib/active_record/schema.rb +7 -14
- data/lib/active_record/schema_dumper.rb +4 -4
- data/lib/active_record/serialization.rb +5 -5
- data/lib/active_record/serializers/json_serializer.rb +37 -28
- data/lib/active_record/serializers/xml_serializer.rb +52 -29
- data/lib/active_record/test_case.rb +36 -0
- data/lib/active_record/timestamp.rb +4 -4
- data/lib/active_record/transactions.rb +3 -3
- data/lib/active_record/validations.rb +182 -248
- data/lib/active_record/version.rb +2 -2
- data/test/{fixtures → assets}/example.log +0 -0
- data/test/{fixtures → assets}/flowers.jpg +0 -0
- data/test/cases/aaa_create_tables_test.rb +24 -0
- data/test/cases/active_schema_test_mysql.rb +95 -0
- data/test/cases/active_schema_test_postgresql.rb +24 -0
- data/test/{adapter_test.rb → cases/adapter_test.rb} +15 -14
- data/test/{adapter_test_sqlserver.rb → cases/adapter_test_sqlserver.rb} +95 -95
- data/test/{aggregations_test.rb → cases/aggregations_test.rb} +20 -20
- data/test/{ar_schema_test.rb → cases/ar_schema_test.rb} +6 -6
- data/test/cases/associations/belongs_to_associations_test.rb +412 -0
- data/test/{associations → cases/associations}/callbacks_test.rb +24 -10
- data/test/{associations → cases/associations}/cascaded_eager_loading_test.rb +18 -17
- data/test/cases/associations/eager_load_nested_include_test.rb +83 -0
- data/test/{associations → cases/associations}/eager_singularization_test.rb +5 -5
- data/test/{associations → cases/associations}/eager_test.rb +216 -51
- data/test/{associations → cases/associations}/extension_test.rb +8 -8
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +684 -0
- data/test/cases/associations/has_many_associations_test.rb +932 -0
- data/test/cases/associations/has_many_through_associations_test.rb +190 -0
- data/test/cases/associations/has_one_associations_test.rb +323 -0
- data/test/cases/associations/has_one_through_associations_test.rb +74 -0
- data/test/{associations → cases/associations}/inner_join_association_test.rb +20 -20
- data/test/{associations → cases/associations}/join_model_test.rb +175 -35
- data/test/cases/associations_test.rb +262 -0
- data/test/{attribute_methods_test.rb → cases/attribute_methods_test.rb} +103 -11
- data/test/{base_test.rb → cases/base_test.rb} +338 -191
- data/test/{binary_test.rb → cases/binary_test.rb} +6 -4
- data/test/{calculations_test.rb → cases/calculations_test.rb} +35 -23
- data/test/{callbacks_test.rb → cases/callbacks_test.rb} +7 -7
- data/test/{class_inheritable_attributes_test.rb → cases/class_inheritable_attributes_test.rb} +3 -3
- data/test/{column_alias_test.rb → cases/column_alias_test.rb} +3 -3
- data/test/{connection_test_firebird.rb → cases/connection_test_firebird.rb} +2 -2
- data/test/{connection_test_mysql.rb → cases/connection_test_mysql.rb} +2 -2
- data/test/{copy_table_test_sqlite.rb → cases/copy_table_test_sqlite.rb} +13 -13
- data/test/{datatype_test_postgresql.rb → cases/datatype_test_postgresql.rb} +8 -8
- data/test/{date_time_test.rb → cases/date_time_test.rb} +5 -5
- data/test/{default_test_firebird.rb → cases/default_test_firebird.rb} +3 -3
- data/test/{defaults_test.rb → cases/defaults_test.rb} +8 -6
- data/test/{deprecated_finder_test.rb → cases/deprecated_finder_test.rb} +3 -3
- data/test/cases/dirty_test.rb +163 -0
- data/test/cases/finder_respond_to_test.rb +76 -0
- data/test/{finder_test.rb → cases/finder_test.rb} +266 -33
- data/test/{fixtures_test.rb → cases/fixtures_test.rb} +88 -72
- data/test/cases/helper.rb +47 -0
- data/test/{inheritance_test.rb → cases/inheritance_test.rb} +61 -17
- data/test/cases/invalid_date_test.rb +24 -0
- data/test/{json_serialization_test.rb → cases/json_serialization_test.rb} +36 -11
- data/test/{lifecycle_test.rb → cases/lifecycle_test.rb} +16 -13
- data/test/{locking_test.rb → cases/locking_test.rb} +17 -10
- data/test/{method_scoping_test.rb → cases/method_scoping_test.rb} +75 -39
- data/test/{migration_test.rb → cases/migration_test.rb} +420 -80
- data/test/{migration_test_firebird.rb → cases/migration_test_firebird.rb} +3 -3
- data/test/{mixin_test.rb → cases/mixin_test.rb} +7 -6
- data/test/{modules_test.rb → cases/modules_test.rb} +11 -6
- data/test/{multiple_db_test.rb → cases/multiple_db_test.rb} +5 -5
- data/test/cases/named_scope_test.rb +157 -0
- data/test/{pk_test.rb → cases/pk_test.rb} +10 -10
- data/test/{query_cache_test.rb → cases/query_cache_test.rb} +12 -10
- data/test/{readonly_test.rb → cases/readonly_test.rb} +11 -11
- data/test/{reflection_test.rb → cases/reflection_test.rb} +15 -14
- data/test/{reserved_word_test_mysql.rb → cases/reserved_word_test_mysql.rb} +4 -5
- data/test/{schema_authorization_test_postgresql.rb → cases/schema_authorization_test_postgresql.rb} +5 -5
- data/test/cases/schema_dumper_test.rb +138 -0
- data/test/cases/schema_test_postgresql.rb +102 -0
- data/test/{serialization_test.rb → cases/serialization_test.rb} +7 -7
- data/test/{synonym_test_oracle.rb → cases/synonym_test_oracle.rb} +5 -5
- data/test/{table_name_test_sqlserver.rb → cases/table_name_test_sqlserver.rb} +3 -3
- data/test/{threaded_connections_test.rb → cases/threaded_connections_test.rb} +7 -7
- data/test/{transactions_test.rb → cases/transactions_test.rb} +31 -5
- data/test/{unconnected_test.rb → cases/unconnected_test.rb} +2 -2
- data/test/{validations_test.rb → cases/validations_test.rb} +141 -39
- data/test/{xml_serialization_test.rb → cases/xml_serialization_test.rb} +12 -12
- data/test/config.rb +5 -0
- data/test/connections/native_db2/connection.rb +1 -1
- data/test/connections/native_firebird/connection.rb +1 -1
- data/test/connections/native_frontbase/connection.rb +1 -1
- data/test/connections/native_mysql/connection.rb +1 -1
- data/test/connections/native_openbase/connection.rb +1 -1
- data/test/connections/native_oracle/connection.rb +1 -1
- data/test/connections/native_postgresql/connection.rb +1 -3
- data/test/connections/native_sqlite/connection.rb +2 -2
- data/test/connections/native_sqlite3/connection.rb +2 -2
- data/test/connections/native_sqlite3/in_memory_connection.rb +3 -3
- data/test/connections/native_sybase/connection.rb +1 -1
- data/test/fixtures/author_addresses.yml +5 -0
- data/test/fixtures/authors.yml +2 -0
- data/test/fixtures/clubs.yml +6 -0
- data/test/fixtures/jobs.yml +7 -0
- data/test/fixtures/members.yml +4 -0
- data/test/fixtures/memberships.yml +20 -0
- data/test/fixtures/owners.yml +7 -0
- data/test/fixtures/people.yml +4 -1
- data/test/fixtures/pets.yml +14 -0
- data/test/fixtures/posts.yml +1 -0
- data/test/fixtures/price_estimates.yml +7 -0
- data/test/fixtures/readers.yml +5 -0
- data/test/fixtures/references.yml +17 -0
- data/test/fixtures/sponsors.yml +9 -0
- data/test/fixtures/subscribers.yml +7 -0
- data/test/fixtures/subscriptions.yml +12 -0
- data/test/fixtures/taggings.yml +4 -1
- data/test/fixtures/topics.yml +22 -2
- data/test/fixtures/warehouse-things.yml +3 -0
- data/test/{fixtures/migrations_with_decimal → migrations/decimal}/1_give_me_big_numbers.rb +0 -0
- data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/1_people_have_last_names.rb +1 -1
- data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/2_we_need_reminders.rb +1 -1
- data/test/{fixtures/migrations_with_duplicate → migrations/duplicate}/3_foo.rb +0 -0
- data/test/{fixtures/migrations → migrations/duplicate}/3_innocent_jointable.rb +0 -0
- data/test/migrations/duplicate_names/20080507052938_chunky.rb +7 -0
- data/test/migrations/duplicate_names/20080507053028_chunky.rb +7 -0
- data/test/{fixtures/migrations_with_duplicate → migrations/interleaved/pass_1}/3_innocent_jointable.rb +0 -0
- data/test/{fixtures/migrations → migrations/interleaved/pass_2}/1_people_have_last_names.rb +1 -1
- data/test/{fixtures/migrations_with_missing_versions/4_innocent_jointable.rb → migrations/interleaved/pass_2/3_innocent_jointable.rb} +0 -0
- data/test/{fixtures/migrations_with_missing_versions → migrations/interleaved/pass_3}/1_people_have_last_names.rb +1 -1
- data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +8 -0
- data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +12 -0
- data/test/{fixtures/migrations_with_missing_versions → migrations/missing}/1000_people_have_middle_names.rb +1 -1
- data/test/migrations/missing/1_people_have_last_names.rb +9 -0
- data/test/{fixtures/migrations_with_missing_versions → migrations/missing}/3_we_need_reminders.rb +1 -1
- data/test/migrations/missing/4_innocent_jointable.rb +12 -0
- data/test/migrations/valid/1_people_have_last_names.rb +9 -0
- data/test/{fixtures/migrations → migrations/valid}/2_we_need_reminders.rb +1 -1
- data/test/migrations/valid/3_innocent_jointable.rb +12 -0
- data/test/{fixtures → models}/author.rb +28 -4
- data/test/{fixtures → models}/auto_id.rb +0 -0
- data/test/{fixtures → models}/binary.rb +0 -0
- data/test/{fixtures → models}/book.rb +0 -0
- data/test/{fixtures → models}/categorization.rb +0 -0
- data/test/{fixtures → models}/category.rb +8 -5
- data/test/{fixtures → models}/citation.rb +0 -0
- data/test/models/club.rb +7 -0
- data/test/{fixtures → models}/column_name.rb +0 -0
- data/test/{fixtures → models}/comment.rb +5 -3
- data/test/{fixtures → models}/company.rb +15 -6
- data/test/{fixtures → models}/company_in_module.rb +5 -3
- data/test/{fixtures → models}/computer.rb +0 -1
- data/test/{fixtures → models}/contact.rb +1 -1
- data/test/{fixtures → models}/course.rb +0 -0
- data/test/{fixtures → models}/customer.rb +8 -8
- data/test/{fixtures → models}/default.rb +0 -0
- data/test/{fixtures → models}/developer.rb +14 -10
- data/test/{fixtures → models}/edge.rb +0 -0
- data/test/{fixtures → models}/entrant.rb +0 -0
- data/test/models/guid.rb +2 -0
- data/test/{fixtures → models}/item.rb +0 -0
- data/test/models/job.rb +5 -0
- data/test/{fixtures → models}/joke.rb +0 -0
- data/test/{fixtures → models}/keyboard.rb +0 -0
- data/test/{fixtures → models}/legacy_thing.rb +0 -0
- data/test/{fixtures → models}/matey.rb +0 -0
- data/test/models/member.rb +9 -0
- data/test/models/membership.rb +9 -0
- data/test/{fixtures → models}/minimalistic.rb +0 -0
- data/test/{fixtures → models}/mixed_case_monkey.rb +0 -0
- data/test/{fixtures → models}/movie.rb +0 -0
- data/test/{fixtures → models}/order.rb +2 -2
- data/test/models/owner.rb +4 -0
- data/test/{fixtures → models}/parrot.rb +0 -0
- data/test/models/person.rb +10 -0
- data/test/models/pet.rb +4 -0
- data/test/models/pirate.rb +9 -0
- data/test/{fixtures → models}/post.rb +23 -2
- data/test/models/price_estimate.rb +3 -0
- data/test/{fixtures → models}/project.rb +1 -0
- data/test/{fixtures → models}/reader.rb +0 -0
- data/test/models/reference.rb +4 -0
- data/test/{fixtures → models}/reply.rb +7 -5
- data/test/{fixtures → models}/ship.rb +0 -0
- data/test/models/sponsor.rb +4 -0
- data/test/{fixtures → models}/subject.rb +0 -0
- data/test/{fixtures → models}/subscriber.rb +2 -0
- data/test/models/subscription.rb +4 -0
- data/test/{fixtures → models}/tag.rb +0 -0
- data/test/{fixtures → models}/tagging.rb +0 -0
- data/test/{fixtures → models}/task.rb +0 -0
- data/test/{fixtures → models}/topic.rb +32 -4
- data/test/{fixtures → models}/treasure.rb +2 -0
- data/test/{fixtures → models}/vertex.rb +0 -0
- data/test/models/warehouse_thing.rb +5 -0
- data/test/schema/mysql_specific_schema.rb +12 -0
- data/test/schema/postgresql_specific_schema.rb +103 -0
- data/test/schema/schema.rb +421 -0
- data/test/schema/schema2.rb +6 -0
- data/test/schema/sqlite_specific_schema.rb +25 -0
- data/test/schema/sqlserver_specific_schema.rb +5 -0
- metadata +192 -176
- data/test/aaa_create_tables_test.rb +0 -72
- data/test/abstract_unit.rb +0 -84
- data/test/active_schema_test_mysql.rb +0 -46
- data/test/all.sh +0 -8
- data/test/association_inheritance_reload.rb +0 -14
- data/test/associations_test.rb +0 -2177
- data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +0 -1
- data/test/fixtures/bad_fixtures/attr_with_spaces +0 -1
- data/test/fixtures/bad_fixtures/blank_line +0 -3
- data/test/fixtures/bad_fixtures/duplicate_attributes +0 -3
- data/test/fixtures/bad_fixtures/missing_value +0 -1
- data/test/fixtures/db_definitions/db2.drop.sql +0 -33
- data/test/fixtures/db_definitions/db2.sql +0 -235
- data/test/fixtures/db_definitions/db22.drop.sql +0 -2
- data/test/fixtures/db_definitions/db22.sql +0 -5
- data/test/fixtures/db_definitions/firebird.drop.sql +0 -65
- data/test/fixtures/db_definitions/firebird.sql +0 -310
- data/test/fixtures/db_definitions/firebird2.drop.sql +0 -2
- data/test/fixtures/db_definitions/firebird2.sql +0 -6
- data/test/fixtures/db_definitions/frontbase.drop.sql +0 -33
- data/test/fixtures/db_definitions/frontbase.sql +0 -273
- data/test/fixtures/db_definitions/frontbase2.drop.sql +0 -1
- data/test/fixtures/db_definitions/frontbase2.sql +0 -4
- data/test/fixtures/db_definitions/openbase.drop.sql +0 -2
- data/test/fixtures/db_definitions/openbase.sql +0 -318
- data/test/fixtures/db_definitions/openbase2.drop.sql +0 -2
- data/test/fixtures/db_definitions/openbase2.sql +0 -7
- data/test/fixtures/db_definitions/oracle.drop.sql +0 -67
- data/test/fixtures/db_definitions/oracle.sql +0 -330
- data/test/fixtures/db_definitions/oracle2.drop.sql +0 -2
- data/test/fixtures/db_definitions/oracle2.sql +0 -6
- data/test/fixtures/db_definitions/postgresql.drop.sql +0 -44
- data/test/fixtures/db_definitions/postgresql.sql +0 -292
- data/test/fixtures/db_definitions/postgresql2.drop.sql +0 -2
- data/test/fixtures/db_definitions/postgresql2.sql +0 -4
- data/test/fixtures/db_definitions/schema.rb +0 -354
- data/test/fixtures/db_definitions/schema2.rb +0 -11
- data/test/fixtures/db_definitions/sqlite.drop.sql +0 -33
- data/test/fixtures/db_definitions/sqlite.sql +0 -219
- data/test/fixtures/db_definitions/sqlite2.drop.sql +0 -2
- data/test/fixtures/db_definitions/sqlite2.sql +0 -5
- data/test/fixtures/db_definitions/sybase.drop.sql +0 -35
- data/test/fixtures/db_definitions/sybase.sql +0 -222
- data/test/fixtures/db_definitions/sybase2.drop.sql +0 -4
- data/test/fixtures/db_definitions/sybase2.sql +0 -5
- data/test/fixtures/developers_projects/david_action_controller +0 -3
- data/test/fixtures/developers_projects/david_active_record +0 -3
- data/test/fixtures/developers_projects/jamis_active_record +0 -2
- data/test/fixtures/person.rb +0 -4
- data/test/fixtures/pirate.rb +0 -5
- data/test/fixtures/subscribers/first +0 -2
- data/test/fixtures/subscribers/second +0 -2
- data/test/schema_dumper_test.rb +0 -131
- data/test/schema_test_postgresql.rb +0 -64
@@ -92,30 +92,39 @@ module ActiveRecord
|
|
92
92
|
#
|
93
93
|
# == Writing value objects
|
94
94
|
#
|
95
|
-
# Value objects are immutable and interchangeable objects that represent a given value, such as a
|
96
|
-
# $5. Two
|
97
|
-
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as
|
95
|
+
# Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
|
96
|
+
# $5. Two Money objects both representing $5 should be equal (through methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking
|
97
|
+
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as Customer can
|
98
98
|
# easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
|
99
|
-
# relational unique identifiers (such as primary keys). Normal
|
99
|
+
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
|
100
100
|
#
|
101
|
-
# It's also important to treat the value objects as immutable. Don't allow the
|
102
|
-
# creation. Create a new
|
101
|
+
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
|
102
|
+
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchanged_to method that
|
103
103
|
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
|
104
104
|
# changed through means other than the writer method.
|
105
105
|
#
|
106
106
|
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
|
107
|
-
# change it afterwards will result in a
|
107
|
+
# change it afterwards will result in a ActiveSupport::FrozenObjectError.
|
108
108
|
#
|
109
109
|
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
|
110
110
|
# immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
|
111
|
+
#
|
112
|
+
# == Finding records by a value object
|
113
|
+
#
|
114
|
+
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance
|
115
|
+
# of the value object in the conditions hash. The following example finds all customers with +balance_amount+ equal to 20 and
|
116
|
+
# +balance_currency+ equal to "USD":
|
117
|
+
#
|
118
|
+
# Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")})
|
119
|
+
#
|
111
120
|
module ClassMethods
|
112
121
|
# Adds reader and writer methods for manipulating a value object:
|
113
122
|
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
|
114
123
|
#
|
115
124
|
# Options are:
|
116
125
|
# * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
|
117
|
-
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the
|
118
|
-
# if the real class name is
|
126
|
+
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the Address class, but
|
127
|
+
# if the real class name is CompanyAddress, you'll have to specify it with this option.
|
119
128
|
# * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
|
120
129
|
# to a constructor parameter on the value class.
|
121
130
|
# * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
|
@@ -155,7 +164,7 @@ module ActiveRecord
|
|
155
164
|
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
|
156
165
|
instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
|
157
166
|
end
|
158
|
-
|
167
|
+
instance_variable_get("@#{name}")
|
159
168
|
end
|
160
169
|
end
|
161
170
|
|
@@ -165,11 +174,11 @@ module ActiveRecord
|
|
165
174
|
module_eval do
|
166
175
|
define_method("#{name}=") do |part|
|
167
176
|
if part.nil? && allow_nil
|
168
|
-
mapping.each { |pair|
|
177
|
+
mapping.each { |pair| self[pair.first] = nil }
|
169
178
|
instance_variable_set("@#{name}", nil)
|
170
179
|
else
|
171
180
|
part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
|
172
|
-
mapping.each { |pair|
|
181
|
+
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
173
182
|
instance_variable_set("@#{name}", part.freeze)
|
174
183
|
end
|
175
184
|
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module AssociationPreload #:nodoc:
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
|
9
|
+
# Loads the named associations for the activerecord record (or records) given
|
10
|
+
# preload_options is passed only one level deep: don't pass to the child associations when associations is a Hash
|
11
|
+
protected
|
12
|
+
def preload_associations(records, associations, preload_options={})
|
13
|
+
records = [records].flatten.compact.uniq
|
14
|
+
return if records.empty?
|
15
|
+
case associations
|
16
|
+
when Array then associations.each {|association| preload_associations(records, association, preload_options)}
|
17
|
+
when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
|
18
|
+
when Hash then
|
19
|
+
associations.each do |parent, child|
|
20
|
+
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
|
21
|
+
preload_associations(records, parent, preload_options)
|
22
|
+
reflection = reflections[parent]
|
23
|
+
parents = records.map {|record| record.send(reflection.name)}.flatten
|
24
|
+
unless parents.empty? || parents.first.nil?
|
25
|
+
parents.first.class.preload_associations(parents, child)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def preload_one_association(records, association, preload_options={})
|
34
|
+
class_to_reflection = {}
|
35
|
+
# Not all records have the same class, so group then preload
|
36
|
+
# group on the reflection itself so that if various subclass share the same association then we do not split them
|
37
|
+
# unncessarily
|
38
|
+
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
|
39
|
+
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
|
40
|
+
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
|
45
|
+
parent_records.each do |parent_record|
|
46
|
+
association_proxy = parent_record.send(reflection_name)
|
47
|
+
association_proxy.loaded
|
48
|
+
association_proxy.target.push(*[associated_record].flatten)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
|
53
|
+
parent_records.each do |parent_record|
|
54
|
+
association_proxy = parent_record.send(reflection_name)
|
55
|
+
association_proxy.loaded
|
56
|
+
association_proxy.target = associated_record
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
|
61
|
+
associated_records.each do |associated_record|
|
62
|
+
mapped_records = id_to_record_map[associated_record[key].to_s]
|
63
|
+
add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
|
68
|
+
seen_keys = {}
|
69
|
+
associated_records.each do |associated_record|
|
70
|
+
#this is a has_one or belongs_to: there should only be one record.
|
71
|
+
#Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
|
72
|
+
# only one row per distinct foo_id' so this where we enforce that
|
73
|
+
next if seen_keys[associated_record[key].to_s]
|
74
|
+
seen_keys[associated_record[key].to_s] = true
|
75
|
+
mapped_records = id_to_record_map[associated_record[key].to_s]
|
76
|
+
mapped_records.each do |mapped_record|
|
77
|
+
mapped_record.send("set_#{reflection_name}_target", associated_record)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def construct_id_map(records)
|
83
|
+
id_to_record_map = {}
|
84
|
+
ids = []
|
85
|
+
records.each do |record|
|
86
|
+
ids << record.id
|
87
|
+
mapped_records = (id_to_record_map[record.id.to_s] ||= [])
|
88
|
+
mapped_records << record
|
89
|
+
end
|
90
|
+
ids.uniq!
|
91
|
+
return id_to_record_map, ids
|
92
|
+
end
|
93
|
+
|
94
|
+
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
|
95
|
+
table_name = reflection.klass.quoted_table_name
|
96
|
+
id_to_record_map, ids = construct_id_map(records)
|
97
|
+
records.each {|record| record.send(reflection.name).loaded}
|
98
|
+
options = reflection.options
|
99
|
+
|
100
|
+
conditions = "t0.#{reflection.primary_key_name} IN (?)"
|
101
|
+
conditions << append_conditions(options, preload_options)
|
102
|
+
|
103
|
+
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
|
104
|
+
:include => options[:include],
|
105
|
+
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} as t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
106
|
+
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as _parent_record_id",
|
107
|
+
:order => options[:order])
|
108
|
+
|
109
|
+
set_association_collection_records(id_to_record_map, reflection.name, associated_records, '_parent_record_id')
|
110
|
+
end
|
111
|
+
|
112
|
+
def preload_has_one_association(records, reflection, preload_options={})
|
113
|
+
id_to_record_map, ids = construct_id_map(records)
|
114
|
+
options = reflection.options
|
115
|
+
if options[:through]
|
116
|
+
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
|
117
|
+
through_records = preload_through_records(records, reflection, options[:through])
|
118
|
+
through_reflection = reflections[options[:through]]
|
119
|
+
through_primary_key = through_reflection.primary_key_name
|
120
|
+
unless through_records.empty?
|
121
|
+
source = reflection.source_reflection.name
|
122
|
+
through_records.first.class.preload_associations(through_records, source)
|
123
|
+
through_records.each do |through_record|
|
124
|
+
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
125
|
+
reflection.name, through_record.send(source))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
else
|
129
|
+
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
130
|
+
|
131
|
+
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def preload_has_many_association(records, reflection, preload_options={})
|
136
|
+
id_to_record_map, ids = construct_id_map(records)
|
137
|
+
records.each {|record| record.send(reflection.name).loaded}
|
138
|
+
options = reflection.options
|
139
|
+
|
140
|
+
if options[:through]
|
141
|
+
through_records = preload_through_records(records, reflection, options[:through])
|
142
|
+
through_reflection = reflections[options[:through]]
|
143
|
+
through_primary_key = through_reflection.primary_key_name
|
144
|
+
unless through_records.empty?
|
145
|
+
source = reflection.source_reflection.name
|
146
|
+
#add conditions from reflection!
|
147
|
+
through_records.first.class.preload_associations(through_records, source, reflection.options)
|
148
|
+
through_records.each do |through_record|
|
149
|
+
add_preloaded_records_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
150
|
+
reflection.name, through_record.send(source))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
else
|
154
|
+
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
|
155
|
+
reflection.primary_key_name)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def preload_through_records(records, reflection, through_association)
|
160
|
+
through_reflection = reflections[through_association]
|
161
|
+
through_primary_key = through_reflection.primary_key_name
|
162
|
+
|
163
|
+
if reflection.options[:source_type]
|
164
|
+
interface = reflection.source_reflection.options[:foreign_type]
|
165
|
+
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
|
166
|
+
|
167
|
+
records.compact!
|
168
|
+
records.first.class.preload_associations(records, through_association, preload_options)
|
169
|
+
|
170
|
+
# Dont cache the association - we would only be caching a subset
|
171
|
+
through_records = []
|
172
|
+
records.each do |record|
|
173
|
+
proxy = record.send(through_association)
|
174
|
+
|
175
|
+
if proxy.respond_to?(:target)
|
176
|
+
through_records << proxy.target
|
177
|
+
proxy.reset
|
178
|
+
else # this is a has_one :through reflection
|
179
|
+
through_records << proxy if proxy
|
180
|
+
end
|
181
|
+
end
|
182
|
+
through_records.flatten!
|
183
|
+
else
|
184
|
+
records.first.class.preload_associations(records, through_association)
|
185
|
+
through_records = records.map {|record| record.send(through_association)}.flatten
|
186
|
+
end
|
187
|
+
through_records.compact!
|
188
|
+
through_records
|
189
|
+
end
|
190
|
+
|
191
|
+
# FIXME: quoting
|
192
|
+
def preload_belongs_to_association(records, reflection, preload_options={})
|
193
|
+
options = reflection.options
|
194
|
+
primary_key_name = reflection.primary_key_name
|
195
|
+
|
196
|
+
if options[:polymorphic]
|
197
|
+
polymorph_type = options[:foreign_type]
|
198
|
+
klasses_and_ids = {}
|
199
|
+
|
200
|
+
# Construct a mapping from klass to a list of ids to load and a mapping of those ids back to their parent_records
|
201
|
+
records.each do |record|
|
202
|
+
if klass = record.send(polymorph_type)
|
203
|
+
klass_id = record.send(primary_key_name)
|
204
|
+
if klass_id
|
205
|
+
id_map = klasses_and_ids[klass] ||= {}
|
206
|
+
id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
|
207
|
+
id_list_for_klass_id << record
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
klasses_and_ids = klasses_and_ids.to_a
|
212
|
+
else
|
213
|
+
id_map = {}
|
214
|
+
records.each do |record|
|
215
|
+
key = record.send(primary_key_name)
|
216
|
+
if key
|
217
|
+
mapped_records = (id_map[key.to_s] ||= [])
|
218
|
+
mapped_records << record
|
219
|
+
end
|
220
|
+
end
|
221
|
+
klasses_and_ids = [[reflection.klass.name, id_map]]
|
222
|
+
end
|
223
|
+
|
224
|
+
klasses_and_ids.each do |klass_and_id|
|
225
|
+
klass_name, id_map = *klass_and_id
|
226
|
+
klass = klass_name.constantize
|
227
|
+
|
228
|
+
table_name = klass.quoted_table_name
|
229
|
+
primary_key = klass.primary_key
|
230
|
+
conditions = "#{table_name}.#{primary_key} IN (?)"
|
231
|
+
conditions << append_conditions(options, preload_options)
|
232
|
+
associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq],
|
233
|
+
:include => options[:include],
|
234
|
+
:select => options[:select],
|
235
|
+
:joins => options[:joins],
|
236
|
+
:order => options[:order])
|
237
|
+
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def find_associated_records(ids, reflection, preload_options)
|
242
|
+
options = reflection.options
|
243
|
+
table_name = reflection.klass.quoted_table_name
|
244
|
+
|
245
|
+
if interface = reflection.options[:as]
|
246
|
+
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'"
|
247
|
+
else
|
248
|
+
foreign_key = reflection.primary_key_name
|
249
|
+
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
|
250
|
+
end
|
251
|
+
|
252
|
+
conditions << append_conditions(options, preload_options)
|
253
|
+
|
254
|
+
reflection.klass.find(:all,
|
255
|
+
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
256
|
+
:include => preload_options[:include] || options[:include],
|
257
|
+
:conditions => [conditions, ids],
|
258
|
+
:joins => options[:joins],
|
259
|
+
:group => preload_options[:group] || options[:group],
|
260
|
+
:order => preload_options[:order] || options[:order])
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
def interpolate_sql_for_preload(sql)
|
265
|
+
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
266
|
+
end
|
267
|
+
|
268
|
+
def append_conditions(options, preload_options)
|
269
|
+
sql = ""
|
270
|
+
sql << " AND (#{interpolate_sql_for_preload(sanitize_sql(options[:conditions]))})" if options[:conditions]
|
271
|
+
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
|
272
|
+
sql
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -6,6 +6,7 @@ require 'active_record/associations/has_one_association'
|
|
6
6
|
require 'active_record/associations/has_many_association'
|
7
7
|
require 'active_record/associations/has_many_through_association'
|
8
8
|
require 'active_record/associations/has_and_belongs_to_many_association'
|
9
|
+
require 'active_record/associations/has_one_through_association'
|
9
10
|
|
10
11
|
module ActiveRecord
|
11
12
|
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
|
@@ -43,6 +44,11 @@ module ActiveRecord
|
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
47
|
+
class HasManyThroughCantAssociateThroughHasManyReflection < ActiveRecordError #:nodoc:
|
48
|
+
def initialize(owner, reflection)
|
49
|
+
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}.")
|
50
|
+
end
|
51
|
+
end
|
46
52
|
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
|
47
53
|
def initialize(owner, reflection)
|
48
54
|
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
|
@@ -104,11 +110,11 @@ module ActiveRecord
|
|
104
110
|
#
|
105
111
|
# Don't create associations that have the same name as instance methods of ActiveRecord::Base. Since the association
|
106
112
|
# adds a method with that name to its model, it will override the inherited method and break things.
|
107
|
-
# For instance,
|
113
|
+
# For instance, +attributes+ and +connection+ would be bad choices for association names.
|
108
114
|
#
|
109
115
|
# == Auto-generated methods
|
110
116
|
#
|
111
|
-
# ===Singular associations (one-to-one)
|
117
|
+
# === Singular associations (one-to-one)
|
112
118
|
# | | belongs_to |
|
113
119
|
# generated methods | belongs_to | :polymorphic | has_one
|
114
120
|
# ----------------------------------+------------+--------------+---------
|
@@ -124,34 +130,34 @@ module ActiveRecord
|
|
124
130
|
# generated methods | habtm | has_many | :through
|
125
131
|
# ----------------------------------+-------+----------+----------
|
126
132
|
# #others | X | X | X
|
127
|
-
# #others=(other,other,...) | X | X |
|
133
|
+
# #others=(other,other,...) | X | X | X
|
128
134
|
# #other_ids | X | X | X
|
129
|
-
# #other_ids=(id,id,...) | X | X |
|
135
|
+
# #other_ids=(id,id,...) | X | X | X
|
130
136
|
# #others<< | X | X | X
|
131
137
|
# #others.push | X | X | X
|
132
138
|
# #others.concat | X | X | X
|
133
|
-
# #others.build(attributes={}) | X | X |
|
134
|
-
# #others.create(attributes={}) | X | X |
|
139
|
+
# #others.build(attributes={}) | X | X | X
|
140
|
+
# #others.create(attributes={}) | X | X | X
|
135
141
|
# #others.create!(attributes={}) | X | X | X
|
136
142
|
# #others.size | X | X | X
|
137
143
|
# #others.length | X | X | X
|
138
|
-
# #others.count |
|
144
|
+
# #others.count | X | X | X
|
139
145
|
# #others.sum(args*,&block) | X | X | X
|
140
146
|
# #others.empty? | X | X | X
|
141
|
-
# #others.clear | X | X |
|
147
|
+
# #others.clear | X | X | X
|
142
148
|
# #others.delete(other,other,...) | X | X | X
|
143
149
|
# #others.delete_all | X | X |
|
144
150
|
# #others.destroy_all | X | X | X
|
145
151
|
# #others.find(*args) | X | X | X
|
146
152
|
# #others.find_first | X | |
|
147
|
-
# #others.uniq | X | X |
|
153
|
+
# #others.uniq | X | X | X
|
148
154
|
# #others.reset | X | X | X
|
149
155
|
#
|
150
156
|
# == Cardinality and associations
|
151
157
|
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
# the relation.
|
158
|
+
# Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
|
159
|
+
# relationships between models. Each model uses an association to describe its role in
|
160
|
+
# the relation. The +belongs_to+ association is always used in the model that has
|
155
161
|
# the foreign key.
|
156
162
|
#
|
157
163
|
# === One-to-one
|
@@ -252,7 +258,7 @@ module ActiveRecord
|
|
252
258
|
# order to update their primary keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
|
253
259
|
# * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns +false+ and the assignment
|
254
260
|
# is cancelled.
|
255
|
-
# * If you wish to assign an object to a +has_one+ association without saving it, use the <tt
|
261
|
+
# * If you wish to assign an object to a +has_one+ association without saving it, use the <tt>association.build</tt> method (documented below).
|
256
262
|
# * Assigning an object to a +belongs_to+ association does not save the object, since the foreign key field belongs on the parent. It
|
257
263
|
# does not save the parent either.
|
258
264
|
#
|
@@ -260,8 +266,8 @@ module ActiveRecord
|
|
260
266
|
#
|
261
267
|
# * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically saves that object, except if the parent object
|
262
268
|
# (the owner of the collection) is not yet stored in the database.
|
263
|
-
# * If saving any of the objects being added to a collection (via <tt
|
264
|
-
# * You can add an object to a collection without automatically saving it by using the <tt
|
269
|
+
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) fails, then <tt>push</tt> returns +false+.
|
270
|
+
# * You can add an object to a collection without automatically saving it by using the <tt>collection.build</tt> method (documented below).
|
265
271
|
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically saved when the parent is saved.
|
266
272
|
#
|
267
273
|
# === Association callbacks
|
@@ -435,9 +441,9 @@ module ActiveRecord
|
|
435
441
|
#
|
436
442
|
# == Eager loading of associations
|
437
443
|
#
|
438
|
-
# Eager loading is a way to find objects of a certain class and a number of named associations
|
444
|
+
# Eager loading is a way to find objects of a certain class and a number of named associations. This is
|
439
445
|
# one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each need to display their author
|
440
|
-
# triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to
|
446
|
+
# triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 2. Example:
|
441
447
|
#
|
442
448
|
# class Post < ActiveRecord::Base
|
443
449
|
# belongs_to :author
|
@@ -446,7 +452,7 @@ module ActiveRecord
|
|
446
452
|
#
|
447
453
|
# Consider the following loop using the class above:
|
448
454
|
#
|
449
|
-
# for post in Post.
|
455
|
+
# for post in Post.all
|
450
456
|
# puts "Post: " + post.title
|
451
457
|
# puts "Written by: " + post.author.name
|
452
458
|
# puts "Last comment on: " + post.comments.first.created_on
|
@@ -456,14 +462,15 @@ module ActiveRecord
|
|
456
462
|
#
|
457
463
|
# for post in Post.find(:all, :include => :author)
|
458
464
|
#
|
459
|
-
# This references the name of the +belongs_to+ association that also used the <tt>:author</tt> symbol
|
460
|
-
#
|
465
|
+
# This references the name of the +belongs_to+ association that also used the <tt>:author</tt> symbol. After loading the posts, find
|
466
|
+
# will collect the +author_id+ from each one and load all the referenced authors with one query. Doing so will cut down the number of queries from 201 to 102.
|
461
467
|
#
|
462
468
|
# We can improve upon the situation further by referencing both associations in the finder with:
|
463
469
|
#
|
464
470
|
# for post in Post.find(:all, :include => [ :author, :comments ])
|
465
471
|
#
|
466
|
-
#
|
472
|
+
# This will load all comments with a single query. This reduces the total number of queries to 3. More generally the number of queries
|
473
|
+
# will be 1 plus the number of associations named (except if some of the associations are polymorphic +belongs_to+ - see below).
|
467
474
|
#
|
468
475
|
# To include a deep hierarchy of associations, use a hash:
|
469
476
|
#
|
@@ -476,77 +483,91 @@ module ActiveRecord
|
|
476
483
|
# the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
|
477
484
|
# catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
|
478
485
|
#
|
479
|
-
# Since
|
480
|
-
#
|
481
|
-
#
|
486
|
+
# Since only one table is loaded at a time, conditions or orders cannot reference tables other than the main one. If this is the case
|
487
|
+
# Active Record falls back to the previously used LEFT OUTER JOIN based strategy. For example
|
488
|
+
#
|
489
|
+
# Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true])
|
490
|
+
#
|
491
|
+
# will result in a single SQL query with joins along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
|
492
|
+
# <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions like this can have unintended consequences.
|
493
|
+
# In the above example posts with no approved comments are not returned at all, because the conditions apply to the SQL statement as a whole
|
494
|
+
# and not just to the association. You must disambiguate column references for this fallback to happen, for example
|
495
|
+
# <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not.
|
496
|
+
#
|
497
|
+
# If you do want eagerload only some members of an association it is usually more natural to <tt>:include</tt> an association
|
498
|
+
# which has conditions defined on it:
|
499
|
+
#
|
500
|
+
# class Post < ActiveRecord::Base
|
501
|
+
# has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true]
|
502
|
+
# end
|
482
503
|
#
|
483
|
-
#
|
484
|
-
#
|
485
|
-
#
|
504
|
+
# Post.find(:all, :include => :approved_comments)
|
505
|
+
#
|
506
|
+
# will load posts and eager load the +approved_comments+ association, which contains only those comments that have been approved.
|
486
507
|
#
|
487
508
|
# When eager loaded, conditions are interpolated in the context of the model class, not the model instance. Conditions are lazily interpolated
|
488
509
|
# before the actual model exists.
|
489
510
|
#
|
490
|
-
# Eager loading is
|
511
|
+
# Eager loading is supported with polymorphic associations.
|
491
512
|
#
|
492
513
|
# class Address < ActiveRecord::Base
|
493
514
|
# belongs_to :addressable, :polymorphic => true
|
494
515
|
# end
|
495
516
|
#
|
496
|
-
#
|
497
|
-
#
|
498
|
-
# Address.find(:all, :include => :addressable) # INVALID
|
517
|
+
# A call that tries to eager load the addressable model
|
499
518
|
#
|
500
|
-
#
|
501
|
-
# is a column value so its corresponding table name cannot be put in the FROM/JOIN clauses of that early query.
|
519
|
+
# Address.find(:all, :include => :addressable)
|
502
520
|
#
|
503
|
-
#
|
504
|
-
#
|
521
|
+
# will execute one query to load the addresses and load the addressables with one query per addressable type.
|
522
|
+
# For example if all the addressables are either of class Person or Company then a total of 3 queries will be executed. The list of
|
523
|
+
# addressable types to load is determined on the back of the addresses loaded. This is not supported if Active Record has to fallback
|
524
|
+
# to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. The reason is that the parent
|
525
|
+
# model's type is a column value so its corresponding table name cannot be put in the +FROM+/+JOIN+ clauses of that query.
|
505
526
|
#
|
506
527
|
# == Table Aliasing
|
507
528
|
#
|
508
|
-
#
|
529
|
+
# Active Record uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once,
|
509
530
|
# the standard table name is used. The second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. Indexes are appended
|
510
531
|
# for any more successive uses of the table name.
|
511
532
|
#
|
512
|
-
# Post.find :all, :
|
513
|
-
# # => SELECT ... FROM posts
|
514
|
-
# Post.find :all, :
|
515
|
-
# # => SELECT ... FROM posts
|
516
|
-
# Post.find :all, :
|
517
|
-
# # => SELECT ... FROM posts
|
533
|
+
# Post.find :all, :joins => :comments
|
534
|
+
# # => SELECT ... FROM posts INNER JOIN comments ON ...
|
535
|
+
# Post.find :all, :joins => :special_comments # STI
|
536
|
+
# # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
|
537
|
+
# Post.find :all, :joins => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name
|
538
|
+
# # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
|
518
539
|
#
|
519
540
|
# Acts as tree example:
|
520
541
|
#
|
521
|
-
# TreeMixin.find :all, :
|
522
|
-
# # => SELECT ... FROM mixins
|
523
|
-
# TreeMixin.find :all, :
|
524
|
-
# # => SELECT ... FROM mixins
|
525
|
-
#
|
526
|
-
# TreeMixin.find :all, :
|
527
|
-
# # => SELECT ... FROM mixins
|
528
|
-
#
|
529
|
-
#
|
542
|
+
# TreeMixin.find :all, :joins => :children
|
543
|
+
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
|
544
|
+
# TreeMixin.find :all, :joins => {:children => :parent}
|
545
|
+
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
|
546
|
+
# INNER JOIN parents_mixins ...
|
547
|
+
# TreeMixin.find :all, :joins => {:children => {:parent => :children}}
|
548
|
+
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
|
549
|
+
# INNER JOIN parents_mixins ...
|
550
|
+
# INNER JOIN mixins childrens_mixins_2
|
530
551
|
#
|
531
552
|
# Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
|
532
553
|
#
|
533
|
-
# Post.find :all, :
|
534
|
-
# # => SELECT ... FROM posts
|
535
|
-
# Post.find :all, :
|
536
|
-
# # => SELECT ... FROM posts
|
537
|
-
#
|
538
|
-
# Post.find :all, :
|
539
|
-
# # => SELECT ... FROM posts
|
540
|
-
#
|
541
|
-
#
|
554
|
+
# Post.find :all, :joins => :categories
|
555
|
+
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
|
556
|
+
# Post.find :all, :joins => {:categories => :posts}
|
557
|
+
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
|
558
|
+
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
|
559
|
+
# Post.find :all, :joins => {:categories => {:posts => :categories}}
|
560
|
+
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
|
561
|
+
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
|
562
|
+
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
|
542
563
|
#
|
543
564
|
# If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table names will take precedence over the eager associations:
|
544
565
|
#
|
545
|
-
# Post.find :all, :
|
546
|
-
# # => SELECT ... FROM posts
|
547
|
-
# Post.find :all, :
|
548
|
-
# # => SELECT ... FROM posts
|
549
|
-
#
|
566
|
+
# Post.find :all, :joins => :comments, :joins => "inner join comments ..."
|
567
|
+
# # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
|
568
|
+
# Post.find :all, :joins => [:comments, :special_comments], :joins => "inner join comments ..."
|
569
|
+
# # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
|
570
|
+
# INNER JOIN comments special_comments_posts ...
|
550
571
|
# INNER JOIN comments ...
|
551
572
|
#
|
552
573
|
# Table aliases are automatically truncated according to the maximum length of table identifiers according to the specific database.
|
@@ -565,7 +586,7 @@ module ActiveRecord
|
|
565
586
|
# end
|
566
587
|
# end
|
567
588
|
#
|
568
|
-
# When
|
589
|
+
# When Firm#clients is called, it will in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
|
569
590
|
# with a class in another module scope, this can be done by specifying the complete class name. Example:
|
570
591
|
#
|
571
592
|
# module MyApplication
|
@@ -593,28 +614,28 @@ module ActiveRecord
|
|
593
614
|
# Adds the following methods for retrieval and query of collections of associated objects:
|
594
615
|
# +collection+ is replaced with the symbol passed as the first argument, so
|
595
616
|
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
|
596
|
-
# * <tt>collection(force_reload = false)</tt> -
|
617
|
+
# * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
|
597
618
|
# An empty array is returned if none are found.
|
598
|
-
# * <tt>collection<<(object, ...)</tt> -
|
599
|
-
# * <tt>collection.delete(object, ...)</tt> -
|
619
|
+
# * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
620
|
+
# * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
600
621
|
# This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
|
601
|
-
# * <tt>collection=objects</tt> -
|
602
|
-
# * <tt>collection_singular_ids</tt> -
|
603
|
-
# * <tt>collection_singular_ids=ids</tt> -
|
604
|
-
# * <tt>collection.clear</tt> -
|
622
|
+
# * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate.
|
623
|
+
# * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids
|
624
|
+
# * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+
|
625
|
+
# * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they
|
605
626
|
# are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
|
606
|
-
# otherwise sets their foreign keys to NULL
|
607
|
-
# * <tt>collection.empty?</tt> -
|
608
|
-
# * <tt>collection.size</tt> -
|
609
|
-
# * <tt>collection.find</tt> -
|
610
|
-
# * <tt>collection.build(attributes = {}, ...)</tt> -
|
627
|
+
# otherwise sets their foreign keys to +NULL+.
|
628
|
+
# * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
|
629
|
+
# * <tt>collection.size</tt> - Returns the number of associated objects.
|
630
|
+
# * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find.
|
631
|
+
# * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated
|
611
632
|
# with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
|
612
633
|
# associated object already exists, not if it's +nil+!
|
613
|
-
# * <tt>collection.create(attributes = {})</tt> -
|
634
|
+
# * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
|
614
635
|
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
615
636
|
# *Note:* This only works if an associated object already exists, not if it's +nil+!
|
616
637
|
#
|
617
|
-
# Example: A
|
638
|
+
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
618
639
|
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
|
619
640
|
# * <tt>Firm#clients<<</tt>
|
620
641
|
# * <tt>Firm#clients.delete</tt>
|
@@ -630,41 +651,45 @@ module ActiveRecord
|
|
630
651
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
631
652
|
#
|
632
653
|
# Options are:
|
633
|
-
# * <tt>:class_name</tt>
|
634
|
-
# from the association name. So <tt>has_many :products</tt> will by default be linked to the
|
635
|
-
# if the real class name is
|
636
|
-
# * <tt>:conditions</tt>
|
637
|
-
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>.
|
638
|
-
#
|
639
|
-
#
|
640
|
-
# * <tt>:
|
641
|
-
#
|
642
|
-
#
|
643
|
-
#
|
644
|
-
#
|
645
|
-
#
|
646
|
-
#
|
647
|
-
#
|
654
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
655
|
+
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
|
656
|
+
# if the real class name is SpecialProduct, you'll have to specify it with this option.
|
657
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
|
658
|
+
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
|
659
|
+
# is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
|
660
|
+
# or <tt>@blog.posts.build</tt>.
|
661
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
662
|
+
# such as <tt>last_name, first_name DESC</tt>.
|
663
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
664
|
+
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id"
|
665
|
+
# as the default <tt>:foreign_key</tt>.
|
666
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed
|
667
|
+
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
|
668
|
+
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
|
669
|
+
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
|
670
|
+
# the <tt>:through</tt> option.
|
671
|
+
# * <tt>:finder_sql</tt> - Specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
648
672
|
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
649
|
-
# * <tt>:counter_sql</tt>
|
673
|
+
# * <tt>:counter_sql</tt> - Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
|
650
674
|
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
|
651
|
-
# * <tt>:extend</tt>
|
652
|
-
# * <tt>:include</tt>
|
653
|
-
# * <tt>:group</tt
|
654
|
-
# * <tt>:limit</tt
|
655
|
-
# * <tt>:offset</tt
|
656
|
-
# * <tt>:select</tt
|
657
|
-
# but not include the joined columns.
|
658
|
-
# * <tt>:as</tt
|
659
|
-
# * <tt>:through</tt
|
675
|
+
# * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions".
|
676
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
|
677
|
+
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
678
|
+
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
|
679
|
+
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
680
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
|
681
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will rise an error.
|
682
|
+
# * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
683
|
+
# * <tt>:through</tt> - Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
|
660
684
|
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
|
661
685
|
# or <tt>has_many</tt> association on the join model.
|
662
|
-
# * <tt>:source</tt
|
686
|
+
# * <tt>:source</tt> - Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
|
663
687
|
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
|
664
|
-
# <tt>:subscriber</tt> on
|
665
|
-
# * <tt>:source_type</tt
|
688
|
+
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
|
689
|
+
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
|
666
690
|
# association is a polymorphic +belongs_to+.
|
667
|
-
# * <tt>:uniq</tt> -
|
691
|
+
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
|
692
|
+
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
668
693
|
#
|
669
694
|
# Option examples:
|
670
695
|
# has_many :comments, :order => "posted_on"
|
@@ -673,6 +698,7 @@ module ActiveRecord
|
|
673
698
|
# has_many :tracks, :order => "position", :dependent => :destroy
|
674
699
|
# has_many :comments, :dependent => :nullify
|
675
700
|
# has_many :tags, :as => :taggable
|
701
|
+
# has_many :reports, :readonly => true
|
676
702
|
# has_many :subscribers, :through => :subscriptions, :source => :user
|
677
703
|
# has_many :subscribers, :class_name => "Person", :finder_sql =>
|
678
704
|
# 'SELECT DISTINCT people.* ' +
|
@@ -684,11 +710,12 @@ module ActiveRecord
|
|
684
710
|
|
685
711
|
configure_dependency_for_has_many(reflection)
|
686
712
|
|
713
|
+
add_multiple_associated_save_callbacks(reflection.name)
|
714
|
+
add_association_callbacks(reflection.name, reflection.options)
|
715
|
+
|
687
716
|
if options[:through]
|
688
|
-
collection_accessor_methods(reflection, HasManyThroughAssociation
|
717
|
+
collection_accessor_methods(reflection, HasManyThroughAssociation)
|
689
718
|
else
|
690
|
-
add_multiple_associated_save_callbacks(reflection.name)
|
691
|
-
add_association_callbacks(reflection.name, reflection.options)
|
692
719
|
collection_accessor_methods(reflection, HasManyAssociation)
|
693
720
|
end
|
694
721
|
end
|
@@ -696,14 +723,14 @@ module ActiveRecord
|
|
696
723
|
# Adds the following methods for retrieval and query of a single associated object:
|
697
724
|
# +association+ is replaced with the symbol passed as the first argument, so
|
698
725
|
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
|
699
|
-
# * <tt>association(force_reload = false)</tt> -
|
700
|
-
# * <tt>association=(associate)</tt> -
|
726
|
+
# * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
|
727
|
+
# * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, sets it as the foreign key,
|
701
728
|
# and saves the associate object.
|
702
|
-
# * <tt>association.nil?</tt> -
|
703
|
-
# * <tt>build_association(attributes = {})</tt> -
|
729
|
+
# * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
|
730
|
+
# * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
704
731
|
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved. Note: This ONLY works if
|
705
732
|
# an association already exists. It will NOT work if the association is +nil+.
|
706
|
-
# * <tt>create_association(attributes = {})</tt> -
|
733
|
+
# * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
707
734
|
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
708
735
|
#
|
709
736
|
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
@@ -716,60 +743,80 @@ module ActiveRecord
|
|
716
743
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
717
744
|
#
|
718
745
|
# Options are:
|
719
|
-
# * <tt>:class_name</tt>
|
720
|
-
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the
|
721
|
-
# if the real class name is
|
722
|
-
# * <tt>:conditions</tt>
|
746
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
747
|
+
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
|
748
|
+
# if the real class name is Person, you'll have to specify it with this option.
|
749
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
723
750
|
# SQL fragment, such as <tt>rank = 5</tt>.
|
724
|
-
# * <tt>:order</tt>
|
725
|
-
# such as <tt>last_name, first_name DESC</tt
|
726
|
-
# * <tt>:dependent</tt>
|
751
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
752
|
+
# such as <tt>last_name, first_name DESC</tt>.
|
753
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
727
754
|
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated
|
728
755
|
# object's foreign key is set to +NULL+. Also, association is assigned.
|
729
|
-
# * <tt>:foreign_key</tt> -
|
730
|
-
# of this class in lower-case and
|
731
|
-
# as the default
|
732
|
-
# * <tt>:include</tt>
|
733
|
-
# * <tt>:as</tt
|
734
|
-
|
756
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
757
|
+
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association will use "person_id"
|
758
|
+
# as the default <tt>:foreign_key</tt>.
|
759
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
|
760
|
+
# * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
761
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
762
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
763
|
+
# * <tt>:through</tt>: Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
|
764
|
+
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a
|
765
|
+
# <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model.
|
766
|
+
# * <tt>:source</tt> - Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
|
767
|
+
# inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a
|
768
|
+
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
|
769
|
+
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
|
770
|
+
# association is a polymorphic +belongs_to+.
|
771
|
+
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
772
|
+
#
|
735
773
|
# Option examples:
|
736
774
|
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
|
737
775
|
# has_one :credit_card, :dependent => :nullify # updates the associated records foreign key value to NULL rather than destroying it
|
738
776
|
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
|
739
777
|
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
|
740
778
|
# has_one :attachment, :as => :attachable
|
779
|
+
# has_one :boss, :readonly => :true
|
780
|
+
# has_one :club, :through => :membership
|
781
|
+
# has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
|
741
782
|
def has_one(association_id, options = {})
|
742
|
-
|
783
|
+
if options[:through]
|
784
|
+
reflection = create_has_one_through_reflection(association_id, options)
|
785
|
+
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
|
786
|
+
else
|
787
|
+
reflection = create_has_one_reflection(association_id, options)
|
743
788
|
|
744
|
-
|
789
|
+
ivar = "@#{reflection.name}"
|
745
790
|
|
746
|
-
|
747
|
-
|
791
|
+
method_name = "has_one_after_save_for_#{reflection.name}".to_sym
|
792
|
+
define_method(method_name) do
|
748
793
|
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
749
794
|
|
750
795
|
if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
|
751
796
|
association["#{reflection.primary_key_name}"] = id
|
752
797
|
association.save(true)
|
753
798
|
end
|
754
|
-
|
755
|
-
|
799
|
+
end
|
800
|
+
after_save method_name
|
756
801
|
|
757
|
-
|
758
|
-
|
759
|
-
|
802
|
+
add_single_associated_save_callbacks(reflection.name)
|
803
|
+
association_accessor_methods(reflection, HasOneAssociation)
|
804
|
+
association_constructor_method(:build, reflection, HasOneAssociation)
|
805
|
+
association_constructor_method(:create, reflection, HasOneAssociation)
|
760
806
|
|
761
|
-
|
807
|
+
configure_dependency_for_has_one(reflection)
|
808
|
+
end
|
762
809
|
end
|
763
810
|
|
764
811
|
# Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
|
765
812
|
# +association+ is replaced with the symbol passed as the first argument, so
|
766
813
|
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
|
767
|
-
# * <tt>association(force_reload = false)</tt> -
|
768
|
-
# * <tt>association=(associate)</tt> -
|
769
|
-
# * <tt>association.nil?</tt> -
|
770
|
-
# * <tt>build_association(attributes = {})</tt> -
|
814
|
+
# * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
|
815
|
+
# * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
816
|
+
# * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
|
817
|
+
# * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
771
818
|
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
|
772
|
-
# * <tt>create_association(attributes = {})</tt> -
|
819
|
+
# * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
773
820
|
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
774
821
|
#
|
775
822
|
# Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
|
@@ -782,27 +829,34 @@ module ActiveRecord
|
|
782
829
|
# The declaration can also include an options hash to specialize the behavior of the association.
|
783
830
|
#
|
784
831
|
# Options are:
|
785
|
-
# * <tt>:class_name</tt>
|
786
|
-
# from the association name. So <tt>has_one :author</tt> will by default be linked to the
|
787
|
-
# if the real class name is
|
788
|
-
# * <tt>:conditions</tt>
|
832
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
833
|
+
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
|
834
|
+
# if the real class name is Person, you'll have to specify it with this option.
|
835
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
789
836
|
# SQL fragment, such as <tt>authorized = 1</tt>.
|
790
|
-
# * <tt>:
|
791
|
-
#
|
792
|
-
# * <tt>:foreign_key</tt> -
|
793
|
-
# of the association with an
|
794
|
-
# Similarly,
|
795
|
-
#
|
837
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
838
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
839
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
840
|
+
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
|
841
|
+
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
|
842
|
+
# will use a foreign key of "favorite_person_id".
|
843
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
844
|
+
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
|
845
|
+
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
|
846
|
+
# orphaned records behind.
|
847
|
+
# * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+
|
796
848
|
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
|
797
|
-
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging
|
798
|
-
# is used on the associate class (such as a
|
849
|
+
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
|
850
|
+
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing
|
799
851
|
# a column name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
|
800
|
-
#
|
801
|
-
#
|
802
|
-
#
|
803
|
-
# * <tt>:
|
852
|
+
# When creating a counter cache column, the database statement or migration must specify a default value of <tt>0</tt>, failing to do
|
853
|
+
# this results in a counter with +NULL+ value, which will never increment.
|
854
|
+
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
|
855
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
|
856
|
+
# * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+.
|
804
857
|
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
|
805
|
-
# to the attr_readonly list in the associated classes (e.g. class Post; attr_readonly :comments_count; end).
|
858
|
+
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
|
859
|
+
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
806
860
|
#
|
807
861
|
# Option examples:
|
808
862
|
# belongs_to :firm, :foreign_key => "client_of"
|
@@ -810,6 +864,8 @@ module ActiveRecord
|
|
810
864
|
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
811
865
|
# :conditions => 'discounts > #{payments_count}'
|
812
866
|
# belongs_to :attachable, :polymorphic => true
|
867
|
+
# belongs_to :project, :readonly => true
|
868
|
+
# belongs_to :post, :counter_cache => true
|
813
869
|
def belongs_to(association_id, options = {})
|
814
870
|
reflection = create_belongs_to_reflection(association_id, options)
|
815
871
|
|
@@ -818,42 +874,42 @@ module ActiveRecord
|
|
818
874
|
if reflection.options[:polymorphic]
|
819
875
|
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
|
820
876
|
|
821
|
-
|
822
|
-
|
823
|
-
|
877
|
+
method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
|
878
|
+
define_method(method_name) do
|
879
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
824
880
|
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
881
|
+
if association && association.target
|
882
|
+
if association.new_record?
|
883
|
+
association.save(true)
|
884
|
+
end
|
829
885
|
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
end
|
886
|
+
if association.updated?
|
887
|
+
self["#{reflection.primary_key_name}"] = association.id
|
888
|
+
self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
|
834
889
|
end
|
835
|
-
|
890
|
+
end
|
836
891
|
end
|
892
|
+
before_save method_name
|
837
893
|
else
|
838
894
|
association_accessor_methods(reflection, BelongsToAssociation)
|
839
895
|
association_constructor_method(:build, reflection, BelongsToAssociation)
|
840
896
|
association_constructor_method(:create, reflection, BelongsToAssociation)
|
841
897
|
|
842
|
-
|
843
|
-
|
844
|
-
|
898
|
+
method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
|
899
|
+
define_method(method_name) do
|
900
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
845
901
|
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
902
|
+
if !association.nil?
|
903
|
+
if association.new_record?
|
904
|
+
association.save(true)
|
905
|
+
end
|
850
906
|
|
851
|
-
|
852
|
-
|
853
|
-
end
|
907
|
+
if association.updated?
|
908
|
+
self["#{reflection.primary_key_name}"] = association.id
|
854
909
|
end
|
855
|
-
|
910
|
+
end
|
856
911
|
end
|
912
|
+
before_save method_name
|
857
913
|
end
|
858
914
|
|
859
915
|
# Create the callbacks to update counter cache
|
@@ -862,57 +918,63 @@ module ActiveRecord
|
|
862
918
|
"#{self.to_s.underscore.pluralize}_count" :
|
863
919
|
options[:counter_cache]
|
864
920
|
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
921
|
+
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
|
922
|
+
define_method(method_name) do
|
923
|
+
association = send("#{reflection.name}")
|
924
|
+
association.class.increment_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
|
925
|
+
end
|
926
|
+
after_create method_name
|
869
927
|
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
928
|
+
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
|
929
|
+
define_method(method_name) do
|
930
|
+
association = send("#{reflection.name}")
|
931
|
+
association.class.decrement_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
|
932
|
+
end
|
933
|
+
before_destroy method_name
|
874
934
|
|
875
935
|
module_eval(
|
876
936
|
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
|
877
937
|
)
|
878
938
|
end
|
939
|
+
|
940
|
+
configure_dependency_for_belongs_to(reflection)
|
879
941
|
end
|
880
942
|
|
881
943
|
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
|
882
|
-
# an option, it is guessed using the lexical order of the class names. So a join between
|
883
|
-
# will give the default join table name of
|
884
|
-
# is calculated using the <tt><</tt> operator for
|
944
|
+
# an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
|
945
|
+
# will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence
|
946
|
+
# is calculated using the <tt><</tt> operator for String. This means that if the strings are of different lengths,
|
885
947
|
# and the strings are equal when compared up to the shortest length, then the longer string is considered of higher
|
886
|
-
# lexical precedence than the shorter one. For example, one would expect the tables
|
887
|
-
# to generate a join table name of
|
888
|
-
# but it in fact generates a join table name of
|
889
|
-
# custom <tt
|
948
|
+
# lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
|
949
|
+
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
|
950
|
+
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
|
951
|
+
# custom <tt>:join_table</tt> option if you need to.
|
890
952
|
#
|
891
953
|
# Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through
|
892
954
|
# +has_and_belongs_to_many+ associations. Records returned from join tables with additional attributes will be marked as
|
893
|
-
#
|
955
|
+
# readonly (because we can't save changes to the additional attributes). It's strongly recommended that you upgrade any
|
894
956
|
# associations with attributes to a real join model (see introduction).
|
895
957
|
#
|
896
958
|
# Adds the following methods for retrieval and query:
|
897
959
|
# +collection+ is replaced with the symbol passed as the first argument, so
|
898
960
|
# <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
|
899
|
-
# * <tt>collection(force_reload = false)</tt> -
|
961
|
+
# * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
|
900
962
|
# An empty array is returned if none are found.
|
901
|
-
# * <tt>collection<<(object, ...)</tt> -
|
963
|
+
# * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by creating associations in the join table
|
902
964
|
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
|
903
|
-
# * <tt>collection.delete(object, ...)</tt> -
|
965
|
+
# * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by removing their associations from the join table.
|
904
966
|
# This does not destroy the objects.
|
905
|
-
# * <tt>collection=objects</tt> -
|
906
|
-
# * <tt>collection_singular_ids</tt> -
|
907
|
-
# * <tt>collection_singular_ids=ids</tt> -
|
908
|
-
# * <tt>collection.clear</tt> -
|
909
|
-
# * <tt>collection.empty?</tt> -
|
910
|
-
# * <tt>collection.size</tt> -
|
911
|
-
# * <tt>collection.find(id)</tt> -
|
967
|
+
# * <tt>collection=objects</tt> - Replaces the collection's content by deleting and adding objects as appropriate.
|
968
|
+
# * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids.
|
969
|
+
# * <tt>collection_singular_ids=ids</tt> - Replace the collection by the objects identified by the primary keys in +ids+.
|
970
|
+
# * <tt>collection.clear</tt> - Removes every object from the collection. This does not destroy the objects.
|
971
|
+
# * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
|
972
|
+
# * <tt>collection.size</tt> - Returns the number of associated objects.
|
973
|
+
# * <tt>collection.find(id)</tt> - Finds an associated object responding to the +id+ and that
|
912
974
|
# meets the condition that it has to be associated with this object.
|
913
|
-
# * <tt>collection.build(attributes = {})</tt> -
|
975
|
+
# * <tt>collection.build(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
|
914
976
|
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
|
915
|
-
# * <tt>collection.create(attributes = {})</tt> -
|
977
|
+
# * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
|
916
978
|
# with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation).
|
917
979
|
#
|
918
980
|
# Example: A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
|
@@ -931,41 +993,45 @@ module ActiveRecord
|
|
931
993
|
# The declaration may include an options hash to specialize the behavior of the association.
|
932
994
|
#
|
933
995
|
# Options are:
|
934
|
-
# * <tt>:class_name</tt> -
|
996
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
935
997
|
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
|
936
|
-
#
|
937
|
-
# * <tt>:join_table</tt> -
|
998
|
+
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
|
999
|
+
# * <tt>:join_table</tt> - Specify the name of the join table if the default based on lexical order isn't what you want.
|
938
1000
|
# WARNING: If you're overwriting the table name of either class, the +table_name+ method MUST be declared underneath any
|
939
1001
|
# +has_and_belongs_to_many+ declaration in order to work.
|
940
|
-
# * <tt>:foreign_key</tt> -
|
941
|
-
# of this class in lower-case and
|
942
|
-
# will use
|
943
|
-
# * <tt>:association_foreign_key</tt> -
|
944
|
-
# guessed to be the name of the associated class in lower-case and
|
945
|
-
# the +has_and_belongs_to_many+ association will use
|
946
|
-
# * <tt>:conditions</tt>
|
947
|
-
# SQL fragment, such as <tt>authorized = 1</tt>.
|
948
|
-
#
|
1002
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
1003
|
+
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association
|
1004
|
+
# will use "person_id" as the default <tt>:foreign_key</tt>.
|
1005
|
+
# * <tt>:association_foreign_key</tt> - Specify the association foreign key used for the association. By default this is
|
1006
|
+
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project,
|
1007
|
+
# the +has_and_belongs_to_many+ association will use "project_id" as the default <tt>:association_foreign_key</tt>.
|
1008
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
1009
|
+
# SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used.
|
1010
|
+
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
|
1011
|
+
# or <tt>@blog.posts.build</tt>.
|
1012
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
949
1013
|
# such as <tt>last_name, first_name DESC</tt>
|
950
|
-
# * <tt>:uniq</tt> -
|
951
|
-
# * <tt>:finder_sql</tt> -
|
952
|
-
# * <tt>:delete_sql</tt> -
|
953
|
-
# classes with a manual statement
|
954
|
-
# * <tt>:insert_sql</tt> -
|
955
|
-
# with a manual statement
|
956
|
-
# * <tt>:extend</tt>
|
957
|
-
# * <tt>:include</tt>
|
958
|
-
# * <tt>:group</tt
|
959
|
-
# * <tt>:limit</tt
|
960
|
-
# * <tt>:offset</tt
|
961
|
-
# * <tt>:select</tt
|
962
|
-
# but not include the joined columns.
|
1014
|
+
# * <tt>:uniq</tt> - If true, duplicate associated objects will be ignored by accessors and query methods.
|
1015
|
+
# * <tt>:finder_sql</tt> - Overwrite the default generated SQL statement used to fetch the association with a manual statement
|
1016
|
+
# * <tt>:delete_sql</tt> - Overwrite the default generated SQL statement used to remove links between the associated
|
1017
|
+
# classes with a manual statement.
|
1018
|
+
# * <tt>:insert_sql</tt> - Overwrite the default generated SQL statement used to add links between the associated classes
|
1019
|
+
# with a manual statement.
|
1020
|
+
# * <tt>:extend</tt> - Anonymous module for extending the proxy, see "Association extensions".
|
1021
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
|
1022
|
+
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
1023
|
+
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
|
1024
|
+
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
1025
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
1026
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
1027
|
+
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
963
1028
|
#
|
964
1029
|
# Option examples:
|
965
1030
|
# has_and_belongs_to_many :projects
|
966
1031
|
# has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
|
967
1032
|
# has_and_belongs_to_many :nations, :class_name => "Country"
|
968
1033
|
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
|
1034
|
+
# has_and_belongs_to_many :categories, :readonly => true
|
969
1035
|
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
|
970
1036
|
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
|
971
1037
|
def has_and_belongs_to_many(association_id, options = {}, &extension)
|
@@ -1033,7 +1099,12 @@ module ActiveRecord
|
|
1033
1099
|
association = association_proxy_class.new(self, reflection)
|
1034
1100
|
end
|
1035
1101
|
|
1036
|
-
|
1102
|
+
if association_proxy_class == HasOneThroughAssociation
|
1103
|
+
association.create_through_record(new_value)
|
1104
|
+
self.send(reflection.name, new_value)
|
1105
|
+
else
|
1106
|
+
association.replace(new_value)
|
1107
|
+
end
|
1037
1108
|
|
1038
1109
|
instance_variable_set(ivar, new_value.nil? ? nil : association)
|
1039
1110
|
end
|
@@ -1085,7 +1156,19 @@ module ActiveRecord
|
|
1085
1156
|
end
|
1086
1157
|
end
|
1087
1158
|
end
|
1088
|
-
|
1159
|
+
|
1160
|
+
def add_single_associated_save_callbacks(association_name)
|
1161
|
+
method_name = "validate_associated_records_for_#{association_name}".to_sym
|
1162
|
+
define_method(method_name) do
|
1163
|
+
association = instance_variable_get("@#{association_name}")
|
1164
|
+
if !association.nil?
|
1165
|
+
errors.add "#{association_name}" unless association.target.nil? || association.valid?
|
1166
|
+
end
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
validate method_name
|
1170
|
+
end
|
1171
|
+
|
1089
1172
|
def add_multiple_associated_save_callbacks(association_name)
|
1090
1173
|
method_name = "validate_associated_records_for_#{association_name}".to_sym
|
1091
1174
|
ivar = "@#{association_name}"
|
@@ -1107,9 +1190,16 @@ module ActiveRecord
|
|
1107
1190
|
end
|
1108
1191
|
|
1109
1192
|
validate method_name
|
1110
|
-
before_save("@new_record_before_save = new_record?; true")
|
1111
1193
|
|
1112
|
-
|
1194
|
+
method_name = "before_save_associated_records_for_#{association_name}".to_sym
|
1195
|
+
define_method(method_name) do
|
1196
|
+
@new_record_before_save = new_record?
|
1197
|
+
true
|
1198
|
+
end
|
1199
|
+
before_save method_name
|
1200
|
+
|
1201
|
+
method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
|
1202
|
+
define_method(method_name) do
|
1113
1203
|
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
1114
1204
|
|
1115
1205
|
records_to_save = if @new_record_before_save
|
@@ -1126,11 +1216,11 @@ module ActiveRecord
|
|
1126
1216
|
|
1127
1217
|
# reconstruct the SQL queries now that we know the owner's id
|
1128
1218
|
association.send(:construct_sql) if association.respond_to?(:construct_sql)
|
1129
|
-
|
1219
|
+
end
|
1130
1220
|
|
1131
1221
|
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
1132
|
-
after_create
|
1133
|
-
after_update
|
1222
|
+
after_create method_name
|
1223
|
+
after_update method_name
|
1134
1224
|
end
|
1135
1225
|
|
1136
1226
|
def association_constructor_method(constructor, reflection, association_proxy_class)
|
@@ -1176,7 +1266,11 @@ module ActiveRecord
|
|
1176
1266
|
|
1177
1267
|
case reflection.options[:dependent]
|
1178
1268
|
when :destroy
|
1179
|
-
|
1269
|
+
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
1270
|
+
define_method(method_name) do
|
1271
|
+
send("#{reflection.name}").each { |o| o.destroy }
|
1272
|
+
end
|
1273
|
+
before_destroy method_name
|
1180
1274
|
when :delete_all
|
1181
1275
|
module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }"
|
1182
1276
|
when :nullify
|
@@ -1191,17 +1285,55 @@ module ActiveRecord
|
|
1191
1285
|
if reflection.options.include?(:dependent)
|
1192
1286
|
case reflection.options[:dependent]
|
1193
1287
|
when :destroy
|
1194
|
-
|
1288
|
+
method_name = "has_one_dependent_destroy_for_#{reflection.name}".to_sym
|
1289
|
+
define_method(method_name) do
|
1290
|
+
association = send("#{reflection.name}")
|
1291
|
+
association.destroy unless association.nil?
|
1292
|
+
end
|
1293
|
+
before_destroy method_name
|
1195
1294
|
when :delete
|
1196
|
-
|
1295
|
+
method_name = "has_one_dependent_delete_for_#{reflection.name}".to_sym
|
1296
|
+
define_method(method_name) do
|
1297
|
+
association = send("#{reflection.name}")
|
1298
|
+
association.class.delete(association.id) unless association.nil?
|
1299
|
+
end
|
1300
|
+
before_destroy method_name
|
1197
1301
|
when :nullify
|
1198
|
-
|
1302
|
+
method_name = "has_one_dependent_nullify_for_#{reflection.name}".to_sym
|
1303
|
+
define_method(method_name) do
|
1304
|
+
association = send("#{reflection.name}")
|
1305
|
+
association.update_attribute("#{reflection.primary_key_name}", nil) unless association.nil?
|
1306
|
+
end
|
1307
|
+
before_destroy method_name
|
1199
1308
|
else
|
1200
1309
|
raise ArgumentError, "The :dependent option expects either :destroy, :delete or :nullify (#{reflection.options[:dependent].inspect})"
|
1201
1310
|
end
|
1202
1311
|
end
|
1203
1312
|
end
|
1204
1313
|
|
1314
|
+
def configure_dependency_for_belongs_to(reflection)
|
1315
|
+
if reflection.options.include?(:dependent)
|
1316
|
+
case reflection.options[:dependent]
|
1317
|
+
when :destroy
|
1318
|
+
method_name = "belongs_to_dependent_destroy_for_#{reflection.name}".to_sym
|
1319
|
+
define_method(method_name) do
|
1320
|
+
association = send("#{reflection.name}")
|
1321
|
+
association.destroy unless association.nil?
|
1322
|
+
end
|
1323
|
+
before_destroy method_name
|
1324
|
+
when :delete
|
1325
|
+
method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym
|
1326
|
+
define_method(method_name) do
|
1327
|
+
association = send("#{reflection.name}")
|
1328
|
+
association.class.delete(association.id) unless association.nil?
|
1329
|
+
end
|
1330
|
+
before_destroy method_name
|
1331
|
+
else
|
1332
|
+
raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
|
1333
|
+
end
|
1334
|
+
end
|
1335
|
+
end
|
1336
|
+
|
1205
1337
|
def create_has_many_reflection(association_id, options, &extension)
|
1206
1338
|
options.assert_valid_keys(
|
1207
1339
|
:class_name, :table_name, :foreign_key,
|
@@ -1211,7 +1343,7 @@ module ActiveRecord
|
|
1211
1343
|
:uniq,
|
1212
1344
|
:finder_sql, :counter_sql,
|
1213
1345
|
:before_add, :after_add, :before_remove, :after_remove,
|
1214
|
-
:extend
|
1346
|
+
:extend, :readonly
|
1215
1347
|
)
|
1216
1348
|
|
1217
1349
|
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
@@ -1221,16 +1353,23 @@ module ActiveRecord
|
|
1221
1353
|
|
1222
1354
|
def create_has_one_reflection(association_id, options)
|
1223
1355
|
options.assert_valid_keys(
|
1224
|
-
:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as
|
1356
|
+
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly
|
1225
1357
|
)
|
1226
1358
|
|
1227
1359
|
create_reflection(:has_one, association_id, options, self)
|
1228
1360
|
end
|
1361
|
+
|
1362
|
+
def create_has_one_through_reflection(association_id, options)
|
1363
|
+
options.assert_valid_keys(
|
1364
|
+
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type
|
1365
|
+
)
|
1366
|
+
create_reflection(:has_one, association_id, options, self)
|
1367
|
+
end
|
1229
1368
|
|
1230
1369
|
def create_belongs_to_reflection(association_id, options)
|
1231
1370
|
options.assert_valid_keys(
|
1232
|
-
:class_name, :foreign_key, :foreign_type, :remote, :
|
1233
|
-
:counter_cache, :extend, :polymorphic
|
1371
|
+
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
|
1372
|
+
:counter_cache, :extend, :polymorphic, :readonly
|
1234
1373
|
)
|
1235
1374
|
|
1236
1375
|
reflection = create_reflection(:belongs_to, association_id, options, self)
|
@@ -1249,7 +1388,7 @@ module ActiveRecord
|
|
1249
1388
|
:uniq,
|
1250
1389
|
:finder_sql, :delete_sql, :insert_sql,
|
1251
1390
|
:before_add, :after_add, :before_remove, :after_remove,
|
1252
|
-
:extend
|
1391
|
+
:extend, :readonly
|
1253
1392
|
)
|
1254
1393
|
|
1255
1394
|
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
@@ -1317,7 +1456,16 @@ module ActiveRecord
|
|
1317
1456
|
|
1318
1457
|
def construct_finder_sql_for_association_limiting(options, join_dependency)
|
1319
1458
|
scope = scope(:find)
|
1320
|
-
|
1459
|
+
|
1460
|
+
# Only join tables referenced in order or conditions since this is particularly slow on the pre-query.
|
1461
|
+
tables_from_conditions = conditions_tables(options)
|
1462
|
+
tables_from_order = order_tables(options)
|
1463
|
+
all_tables = tables_from_conditions + tables_from_order
|
1464
|
+
distinct_join_associations = all_tables.uniq.map{|table|
|
1465
|
+
join_dependency.joins_for_table_name(table)
|
1466
|
+
}.flatten.compact.uniq
|
1467
|
+
|
1468
|
+
is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order)
|
1321
1469
|
sql = "SELECT "
|
1322
1470
|
if is_distinct
|
1323
1471
|
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order])
|
@@ -1327,7 +1475,7 @@ module ActiveRecord
|
|
1327
1475
|
sql << " FROM #{connection.quote_table_name table_name} "
|
1328
1476
|
|
1329
1477
|
if is_distinct
|
1330
|
-
sql <<
|
1478
|
+
sql << distinct_join_associations.collect(&:association_join).join
|
1331
1479
|
add_joins!(sql, options, scope)
|
1332
1480
|
end
|
1333
1481
|
|
@@ -1345,8 +1493,7 @@ module ActiveRecord
|
|
1345
1493
|
return sanitize_sql(sql)
|
1346
1494
|
end
|
1347
1495
|
|
1348
|
-
|
1349
|
-
def include_eager_conditions?(options)
|
1496
|
+
def conditions_tables(options)
|
1350
1497
|
# look in both sets of conditions
|
1351
1498
|
conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
|
1352
1499
|
case cond
|
@@ -1355,19 +1502,37 @@ module ActiveRecord
|
|
1355
1502
|
else all << cond
|
1356
1503
|
end
|
1357
1504
|
end
|
1358
|
-
|
1359
|
-
conditions.join(' ').scan(/([\.\w]+)\.\w+/).flatten.any? do |condition_table_name|
|
1360
|
-
condition_table_name != table_name
|
1361
|
-
end
|
1505
|
+
conditions.join(' ').scan(/([\.\w]+).?\./).flatten
|
1362
1506
|
end
|
1363
1507
|
|
1364
|
-
|
1365
|
-
def include_eager_order?(options)
|
1508
|
+
def order_tables(options)
|
1366
1509
|
order = options[:order]
|
1367
|
-
return
|
1368
|
-
order.scan(/([\.\w]+)
|
1369
|
-
|
1370
|
-
|
1510
|
+
return [] unless order && order.is_a?(String)
|
1511
|
+
order.scan(/([\.\w]+).?\./).flatten
|
1512
|
+
end
|
1513
|
+
|
1514
|
+
def selects_tables(options)
|
1515
|
+
select = options[:select]
|
1516
|
+
return [] unless select && select.is_a?(String)
|
1517
|
+
select.scan(/"?([\.\w]+)"?.?\./).flatten
|
1518
|
+
end
|
1519
|
+
|
1520
|
+
# Checks if the conditions reference a table other than the current model table
|
1521
|
+
def include_eager_conditions?(options, tables = nil)
|
1522
|
+
((tables || conditions_tables(options)) - [table_name]).any?
|
1523
|
+
end
|
1524
|
+
|
1525
|
+
# Checks if the query order references a table other than the current model's table.
|
1526
|
+
def include_eager_order?(options, tables = nil)
|
1527
|
+
((tables || order_tables(options)) - [table_name]).any?
|
1528
|
+
end
|
1529
|
+
|
1530
|
+
def include_eager_select?(options)
|
1531
|
+
(selects_tables(options) - [table_name]).any?
|
1532
|
+
end
|
1533
|
+
|
1534
|
+
def references_eager_loaded_tables?(options)
|
1535
|
+
include_eager_order?(options) || include_eager_conditions?(options) || include_eager_select?(options)
|
1371
1536
|
end
|
1372
1537
|
|
1373
1538
|
def using_limitable_reflections?(reflections)
|
@@ -1461,8 +1626,10 @@ module ActiveRecord
|
|
1461
1626
|
is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
1462
1627
|
|
1463
1628
|
parent_records = records.map do |record|
|
1464
|
-
|
1465
|
-
|
1629
|
+
descendant = record.send(reflection.name)
|
1630
|
+
next unless descendant
|
1631
|
+
descendant.target.uniq! if is_collection
|
1632
|
+
descendant
|
1466
1633
|
end.flatten.compact
|
1467
1634
|
|
1468
1635
|
remove_duplicate_results!(reflection.class_name.constantize, parent_records, associations[name]) unless parent_records.empty?
|
@@ -1470,6 +1637,23 @@ module ActiveRecord
|
|
1470
1637
|
end
|
1471
1638
|
end
|
1472
1639
|
|
1640
|
+
def join_for_table_name(table_name)
|
1641
|
+
@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
|
1642
|
+
end
|
1643
|
+
|
1644
|
+
def joins_for_table_name(table_name)
|
1645
|
+
join = join_for_table_name(table_name)
|
1646
|
+
result = nil
|
1647
|
+
if join && join.is_a?(JoinAssociation)
|
1648
|
+
result = [join]
|
1649
|
+
if join.parent && join.parent.is_a?(JoinAssociation)
|
1650
|
+
result = joins_for_table_name(join.parent.aliased_table_name) +
|
1651
|
+
result
|
1652
|
+
end
|
1653
|
+
end
|
1654
|
+
result
|
1655
|
+
end
|
1656
|
+
|
1473
1657
|
protected
|
1474
1658
|
def build(associations, parent = nil)
|
1475
1659
|
parent ||= @joins.last
|
@@ -1600,36 +1784,19 @@ module ActiveRecord
|
|
1600
1784
|
end
|
1601
1785
|
|
1602
1786
|
super(reflection.klass)
|
1787
|
+
@join_dependency = join_dependency
|
1603
1788
|
@parent = parent
|
1604
1789
|
@reflection = reflection
|
1605
1790
|
@aliased_prefix = "t#{ join_dependency.joins.size }"
|
1606
|
-
@aliased_table_name = table_name #.tr('.', '_') # start with the table name, sub out any .'s
|
1607
1791
|
@parent_table_name = parent.active_record.table_name
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
1792
|
+
@aliased_table_name = aliased_table_name_for(table_name)
|
1793
|
+
|
1794
|
+
if reflection.macro == :has_and_belongs_to_many
|
1795
|
+
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
|
1611
1796
|
end
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
@aliased_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}"
|
1616
|
-
table_index = join_dependency.table_aliases[aliased_table_name]
|
1617
|
-
join_dependency.table_aliases[aliased_table_name] += 1
|
1618
|
-
@aliased_table_name = @aliased_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
|
1619
|
-
else
|
1620
|
-
join_dependency.table_aliases[aliased_table_name] += 1
|
1621
|
-
end
|
1622
|
-
|
1623
|
-
if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through])
|
1624
|
-
@aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : reflection.through_reflection.klass.table_name
|
1625
|
-
unless join_dependency.table_aliases[aliased_join_table_name].zero?
|
1626
|
-
@aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join"
|
1627
|
-
table_index = join_dependency.table_aliases[aliased_join_table_name]
|
1628
|
-
join_dependency.table_aliases[aliased_join_table_name] += 1
|
1629
|
-
@aliased_join_table_name = @aliased_join_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
|
1630
|
-
else
|
1631
|
-
join_dependency.table_aliases[aliased_join_table_name] += 1
|
1632
|
-
end
|
1797
|
+
|
1798
|
+
if reflection.macro == :has_many && reflection.options[:through]
|
1799
|
+
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
|
1633
1800
|
end
|
1634
1801
|
end
|
1635
1802
|
|
@@ -1766,6 +1933,25 @@ module ActiveRecord
|
|
1766
1933
|
end
|
1767
1934
|
|
1768
1935
|
protected
|
1936
|
+
|
1937
|
+
def aliased_table_name_for(name, suffix = nil)
|
1938
|
+
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{name.downcase}\son}
|
1939
|
+
@join_dependency.table_aliases[name] += 1
|
1940
|
+
end
|
1941
|
+
|
1942
|
+
unless @join_dependency.table_aliases[name].zero?
|
1943
|
+
# if the table name has been used, then use an alias
|
1944
|
+
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
1945
|
+
table_index = @join_dependency.table_aliases[name]
|
1946
|
+
@join_dependency.table_aliases[name] += 1
|
1947
|
+
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
|
1948
|
+
else
|
1949
|
+
@join_dependency.table_aliases[name] += 1
|
1950
|
+
end
|
1951
|
+
|
1952
|
+
name
|
1953
|
+
end
|
1954
|
+
|
1769
1955
|
def pluralize(table_name)
|
1770
1956
|
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
1771
1957
|
end
|