activerecord 1.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2102 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +213 -0
- data/examples/performance.rb +172 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record/aggregations.rb +180 -84
- data/lib/active_record/associations/alias_tracker.rb +76 -0
- data/lib/active_record/associations/association.rb +248 -0
- data/lib/active_record/associations/association_scope.rb +135 -0
- data/lib/active_record/associations/belongs_to_association.rb +92 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +35 -0
- data/lib/active_record/associations/builder/association.rb +108 -0
- data/lib/active_record/associations/builder/belongs_to.rb +98 -0
- data/lib/active_record/associations/builder/collection_association.rb +89 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
- data/lib/active_record/associations/builder/has_many.rb +15 -0
- data/lib/active_record/associations/builder/has_one.rb +25 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +608 -0
- data/lib/active_record/associations/collection_proxy.rb +986 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +58 -39
- data/lib/active_record/associations/has_many_association.rb +116 -85
- data/lib/active_record/associations/has_many_through_association.rb +197 -0
- data/lib/active_record/associations/has_one_association.rb +102 -0
- data/lib/active_record/associations/has_one_through_association.rb +36 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_dependency.rb +235 -0
- data/lib/active_record/associations/join_helper.rb +45 -0
- data/lib/active_record/associations/preloader/association.rb +121 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +63 -0
- data/lib/active_record/associations/preloader.rb +178 -0
- data/lib/active_record/associations/singular_association.rb +64 -0
- data/lib/active_record/associations/through_association.rb +87 -0
- data/lib/active_record/associations.rb +1437 -431
- data/lib/active_record/attribute_assignment.rb +201 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +70 -0
- data/lib/active_record/attribute_methods/dirty.rb +118 -0
- data/lib/active_record/attribute_methods/primary_key.rb +122 -0
- data/lib/active_record/attribute_methods/query.rb +40 -0
- data/lib/active_record/attribute_methods/read.rb +107 -0
- data/lib/active_record/attribute_methods/serialization.rb +162 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +59 -0
- data/lib/active_record/attribute_methods/write.rb +63 -0
- data/lib/active_record/attribute_methods.rb +393 -0
- data/lib/active_record/autosave_association.rb +426 -0
- data/lib/active_record/base.rb +268 -930
- data/lib/active_record/callbacks.rb +203 -230
- data/lib/active_record/coders/yaml_column.rb +38 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +638 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +67 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +390 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +95 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +129 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +501 -0
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +873 -0
- data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +389 -275
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
- data/lib/active_record/connection_adapters/column.rb +318 -0
- data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +517 -90
- data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
- data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
- data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +911 -138
- data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +624 -0
- data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
- data/lib/active_record/connection_handling.rb +98 -0
- data/lib/active_record/core.rb +463 -0
- data/lib/active_record/counter_cache.rb +122 -0
- data/lib/active_record/dynamic_matchers.rb +131 -0
- data/lib/active_record/errors.rb +213 -0
- data/lib/active_record/explain.rb +38 -0
- data/lib/active_record/explain_registry.rb +30 -0
- data/lib/active_record/explain_subscriber.rb +29 -0
- data/lib/active_record/fixture_set/file.rb +55 -0
- data/lib/active_record/fixtures.rb +892 -138
- data/lib/active_record/inheritance.rb +200 -0
- data/lib/active_record/integration.rb +60 -0
- data/lib/active_record/locale/en.yml +47 -0
- data/lib/active_record/locking/optimistic.rb +181 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/log_subscriber.rb +82 -0
- data/lib/active_record/migration/command_recorder.rb +164 -0
- data/lib/active_record/migration/join_table.rb +15 -0
- data/lib/active_record/migration.rb +1015 -0
- data/lib/active_record/model_schema.rb +345 -0
- data/lib/active_record/nested_attributes.rb +546 -0
- data/lib/active_record/null_relation.rb +65 -0
- data/lib/active_record/persistence.rb +509 -0
- data/lib/active_record/query_cache.rb +56 -0
- data/lib/active_record/querying.rb +62 -0
- data/lib/active_record/railtie.rb +205 -0
- data/lib/active_record/railties/console_sandbox.rb +5 -0
- data/lib/active_record/railties/controller_runtime.rb +50 -0
- data/lib/active_record/railties/databases.rake +402 -0
- data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
- data/lib/active_record/readonly_attributes.rb +30 -0
- data/lib/active_record/reflection.rb +544 -87
- data/lib/active_record/relation/batches.rb +93 -0
- data/lib/active_record/relation/calculations.rb +399 -0
- data/lib/active_record/relation/delegation.rb +125 -0
- data/lib/active_record/relation/finder_methods.rb +349 -0
- data/lib/active_record/relation/merger.rb +161 -0
- data/lib/active_record/relation/predicate_builder.rb +106 -0
- data/lib/active_record/relation/query_methods.rb +1044 -0
- data/lib/active_record/relation/spawn_methods.rb +73 -0
- data/lib/active_record/relation.rb +655 -0
- data/lib/active_record/result.rb +67 -0
- data/lib/active_record/runtime_registry.rb +17 -0
- data/lib/active_record/sanitization.rb +168 -0
- data/lib/active_record/schema.rb +65 -0
- data/lib/active_record/schema_dumper.rb +204 -0
- data/lib/active_record/schema_migration.rb +39 -0
- data/lib/active_record/scoping/default.rb +146 -0
- data/lib/active_record/scoping/named.rb +175 -0
- data/lib/active_record/scoping.rb +82 -0
- data/lib/active_record/serialization.rb +22 -0
- data/lib/active_record/serializers/xml_serializer.rb +197 -0
- data/lib/active_record/statement_cache.rb +26 -0
- data/lib/active_record/store.rb +156 -0
- data/lib/active_record/tasks/database_tasks.rb +203 -0
- data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
- data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
- data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
- data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
- data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
- data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
- data/lib/active_record/test_case.rb +96 -0
- data/lib/active_record/timestamp.rb +119 -0
- data/lib/active_record/transactions.rb +366 -69
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations/associated.rb +49 -0
- data/lib/active_record/validations/presence.rb +65 -0
- data/lib/active_record/validations/uniqueness.rb +225 -0
- data/lib/active_record/validations.rb +64 -185
- data/lib/active_record/version.rb +11 -0
- data/lib/active_record.rb +149 -24
- data/lib/rails/generators/active_record/migration/migration_generator.rb +62 -0
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +39 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +10 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
- data/lib/rails/generators/active_record.rb +23 -0
- metadata +261 -161
- data/CHANGELOG +0 -581
- data/README +0 -361
- data/RUNNING_UNIT_TESTS +0 -36
- data/dev-utils/eval_debugger.rb +0 -9
- data/examples/associations.png +0 -0
- data/examples/associations.rb +0 -87
- data/examples/shared_setup.rb +0 -15
- data/examples/validation.rb +0 -88
- data/install.rb +0 -60
- data/lib/active_record/associations/association_collection.rb +0 -70
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -107
- data/lib/active_record/deprecated_associations.rb +0 -70
- data/lib/active_record/observer.rb +0 -71
- data/lib/active_record/support/class_attribute_accessors.rb +0 -43
- data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
- data/lib/active_record/support/clean_logger.rb +0 -10
- data/lib/active_record/support/inflector.rb +0 -70
- data/lib/active_record/vendor/mysql.rb +0 -1117
- data/lib/active_record/vendor/simple.rb +0 -702
- data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
- data/lib/active_record/wrappings.rb +0 -59
- data/rakefile +0 -122
- data/test/abstract_unit.rb +0 -16
- data/test/aggregations_test.rb +0 -34
- data/test/all.sh +0 -8
- data/test/associations_test.rb +0 -477
- data/test/base_test.rb +0 -513
- data/test/class_inheritable_attributes_test.rb +0 -33
- data/test/connections/native_mysql/connection.rb +0 -24
- data/test/connections/native_postgresql/connection.rb +0 -24
- data/test/connections/native_sqlite/connection.rb +0 -24
- data/test/deprecated_associations_test.rb +0 -336
- data/test/finder_test.rb +0 -67
- data/test/fixtures/accounts/signals37 +0 -3
- data/test/fixtures/accounts/unknown +0 -2
- data/test/fixtures/auto_id.rb +0 -4
- data/test/fixtures/column_name.rb +0 -3
- data/test/fixtures/companies/first_client +0 -6
- data/test/fixtures/companies/first_firm +0 -4
- data/test/fixtures/companies/second_client +0 -6
- data/test/fixtures/company.rb +0 -37
- data/test/fixtures/company_in_module.rb +0 -33
- data/test/fixtures/course.rb +0 -3
- data/test/fixtures/courses/java +0 -2
- data/test/fixtures/courses/ruby +0 -2
- data/test/fixtures/customer.rb +0 -30
- data/test/fixtures/customers/david +0 -6
- data/test/fixtures/db_definitions/mysql.sql +0 -96
- data/test/fixtures/db_definitions/mysql2.sql +0 -4
- data/test/fixtures/db_definitions/postgresql.sql +0 -113
- data/test/fixtures/db_definitions/postgresql2.sql +0 -4
- data/test/fixtures/db_definitions/sqlite.sql +0 -85
- data/test/fixtures/db_definitions/sqlite2.sql +0 -4
- data/test/fixtures/default.rb +0 -2
- data/test/fixtures/developer.rb +0 -8
- data/test/fixtures/developers/david +0 -2
- data/test/fixtures/developers/jamis +0 -2
- data/test/fixtures/developers_projects/david_action_controller +0 -2
- data/test/fixtures/developers_projects/david_active_record +0 -2
- data/test/fixtures/developers_projects/jamis_active_record +0 -2
- data/test/fixtures/entrant.rb +0 -3
- data/test/fixtures/entrants/first +0 -3
- data/test/fixtures/entrants/second +0 -3
- data/test/fixtures/entrants/third +0 -3
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/fixture_database_2.sqlite +0 -0
- data/test/fixtures/movie.rb +0 -5
- data/test/fixtures/movies/first +0 -2
- data/test/fixtures/movies/second +0 -2
- data/test/fixtures/project.rb +0 -3
- data/test/fixtures/projects/action_controller +0 -2
- data/test/fixtures/projects/active_record +0 -2
- data/test/fixtures/reply.rb +0 -21
- data/test/fixtures/subscriber.rb +0 -5
- data/test/fixtures/subscribers/first +0 -2
- data/test/fixtures/subscribers/second +0 -2
- data/test/fixtures/topic.rb +0 -20
- data/test/fixtures/topics/first +0 -9
- data/test/fixtures/topics/second +0 -8
- data/test/fixtures_test.rb +0 -20
- data/test/inflector_test.rb +0 -104
- data/test/inheritance_test.rb +0 -125
- data/test/lifecycle_test.rb +0 -110
- data/test/modules_test.rb +0 -21
- data/test/multiple_db_test.rb +0 -46
- data/test/pk_test.rb +0 -57
- data/test/reflection_test.rb +0 -78
- data/test/thread_safety_test.rb +0 -33
- data/test/transactions_test.rb +0 -83
- data/test/unconnected_test.rb +0 -24
- data/test/validations_test.rb +0 -126
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class CollectionAssociation < Association #:nodoc:
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def build_scope
|
9
|
+
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
|
10
|
+
end
|
11
|
+
|
12
|
+
def preload
|
13
|
+
associated_records_by_owner.each do |owner, records|
|
14
|
+
association = owner.association(reflection.name)
|
15
|
+
association.loaded!
|
16
|
+
association.target.concat(records)
|
17
|
+
records.each { |record| association.set_inverse_instance(record) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class HasAndBelongsToMany < CollectionAssociation #:nodoc:
|
5
|
+
attr_reader :join_table
|
6
|
+
|
7
|
+
def initialize(klass, records, reflection, preload_options)
|
8
|
+
super
|
9
|
+
@join_table = Arel::Table.new(reflection.join_table).alias('t0')
|
10
|
+
end
|
11
|
+
|
12
|
+
# Unlike the other associations, we want to get a raw array of rows so that we can
|
13
|
+
# access the aliased column on the join table
|
14
|
+
def records_for(ids)
|
15
|
+
scope = super
|
16
|
+
klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
|
17
|
+
end
|
18
|
+
|
19
|
+
def owner_key_name
|
20
|
+
reflection.active_record_primary_key
|
21
|
+
end
|
22
|
+
|
23
|
+
def association_key_name
|
24
|
+
'ar_association_key_name'
|
25
|
+
end
|
26
|
+
|
27
|
+
def association_key
|
28
|
+
join_table[reflection.foreign_key]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Once we have used the join table column (in super), we manually instantiate the
|
34
|
+
# actual records, ensuring that we don't create more than one instances of the same
|
35
|
+
# record
|
36
|
+
def associated_records_by_owner
|
37
|
+
records = {}
|
38
|
+
super.each_value do |rows|
|
39
|
+
rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_scope
|
44
|
+
super.joins(join).select(join_select)
|
45
|
+
end
|
46
|
+
|
47
|
+
def join_select
|
48
|
+
association_key.as(Arel.sql(association_key_name))
|
49
|
+
end
|
50
|
+
|
51
|
+
def join
|
52
|
+
condition = table[reflection.association_primary_key].eq(
|
53
|
+
join_table[reflection.association_foreign_key])
|
54
|
+
|
55
|
+
table.create_join(join_table, table.create_on(condition))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class HasMany < CollectionAssociation #:nodoc:
|
5
|
+
|
6
|
+
def association_key_name
|
7
|
+
reflection.foreign_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def owner_key_name
|
11
|
+
reflection.active_record_primary_key
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class HasManyThrough < CollectionAssociation #:nodoc:
|
5
|
+
include ThroughAssociation
|
6
|
+
|
7
|
+
def associated_records_by_owner
|
8
|
+
records_by_owner = super
|
9
|
+
|
10
|
+
if reflection_scope.distinct_value
|
11
|
+
records_by_owner.each_value { |records| records.uniq! }
|
12
|
+
end
|
13
|
+
|
14
|
+
records_by_owner
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class HasOne < SingularAssociation #:nodoc:
|
5
|
+
|
6
|
+
def association_key_name
|
7
|
+
reflection.foreign_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def owner_key_name
|
11
|
+
reflection.active_record_primary_key
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_scope
|
17
|
+
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class SingularAssociation < Association #:nodoc:
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def preload
|
9
|
+
associated_records_by_owner.each do |owner, associated_records|
|
10
|
+
record = associated_records.first
|
11
|
+
|
12
|
+
association = owner.association(reflection.name)
|
13
|
+
association.target = record
|
14
|
+
association.set_inverse_instance(record)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
module ThroughAssociation #:nodoc:
|
5
|
+
|
6
|
+
def through_reflection
|
7
|
+
reflection.through_reflection
|
8
|
+
end
|
9
|
+
|
10
|
+
def source_reflection
|
11
|
+
reflection.source_reflection
|
12
|
+
end
|
13
|
+
|
14
|
+
def associated_records_by_owner
|
15
|
+
through_records = through_records_by_owner
|
16
|
+
|
17
|
+
Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
|
18
|
+
|
19
|
+
through_records.each do |owner, records|
|
20
|
+
records.map! { |r| r.send(source_reflection.name) }.flatten!
|
21
|
+
records.compact!
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def through_records_by_owner
|
28
|
+
Preloader.new(owners, through_reflection.name, through_scope).run
|
29
|
+
|
30
|
+
Hash[owners.map do |owner|
|
31
|
+
through_records = Array.wrap(owner.send(through_reflection.name))
|
32
|
+
|
33
|
+
# Dont cache the association - we would only be caching a subset
|
34
|
+
if (through_scope != through_reflection.klass.unscoped) ||
|
35
|
+
(reflection.options[:source_type] && through_reflection.collection?)
|
36
|
+
owner.association(through_reflection.name).reset
|
37
|
+
end
|
38
|
+
|
39
|
+
[owner, through_records]
|
40
|
+
end]
|
41
|
+
end
|
42
|
+
|
43
|
+
def through_scope
|
44
|
+
through_scope = through_reflection.klass.unscoped
|
45
|
+
|
46
|
+
if options[:source_type]
|
47
|
+
through_scope.where! reflection.foreign_type => options[:source_type]
|
48
|
+
else
|
49
|
+
unless reflection_scope.where_values.empty?
|
50
|
+
through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
|
51
|
+
through_scope.where_values = reflection_scope.values[:where]
|
52
|
+
end
|
53
|
+
|
54
|
+
through_scope.references! reflection_scope.values[:references]
|
55
|
+
through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading?
|
56
|
+
end
|
57
|
+
|
58
|
+
through_scope
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
# Implements the details of eager loading of Active Record associations.
|
4
|
+
#
|
5
|
+
# Note that 'eager loading' and 'preloading' are actually the same thing.
|
6
|
+
# However, there are two different eager loading strategies.
|
7
|
+
#
|
8
|
+
# The first one is by using table joins. This was only strategy available
|
9
|
+
# prior to Rails 2.1. Suppose that you have an Author model with columns
|
10
|
+
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
|
11
|
+
# this strategy, Active Record would try to retrieve all data for an author
|
12
|
+
# and all of its books via a single query:
|
13
|
+
#
|
14
|
+
# SELECT * FROM authors
|
15
|
+
# LEFT OUTER JOIN books ON authors.id = books.author_id
|
16
|
+
# WHERE authors.name = 'Ken Akamatsu'
|
17
|
+
#
|
18
|
+
# However, this could result in many rows that contain redundant data. After
|
19
|
+
# having received the first row, we already have enough data to instantiate
|
20
|
+
# the Author object. In all subsequent rows, only the data for the joined
|
21
|
+
# 'books' table is useful; the joined 'authors' data is just redundant, and
|
22
|
+
# processing this redundant data takes memory and CPU time. The problem
|
23
|
+
# quickly becomes worse and worse as the level of eager loading increases
|
24
|
+
# (i.e. if Active Record is to eager load the associations' associations as
|
25
|
+
# well).
|
26
|
+
#
|
27
|
+
# The second strategy is to use multiple database queries, one for each
|
28
|
+
# level of association. Since Rails 2.1, this is the default strategy. In
|
29
|
+
# situations where a table join is necessary (e.g. when the +:conditions+
|
30
|
+
# option references an association's column), it will fallback to the table
|
31
|
+
# join strategy.
|
32
|
+
class Preloader #:nodoc:
|
33
|
+
extend ActiveSupport::Autoload
|
34
|
+
|
35
|
+
eager_autoload do
|
36
|
+
autoload :Association, 'active_record/associations/preloader/association'
|
37
|
+
autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
|
38
|
+
autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
|
39
|
+
autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
|
40
|
+
|
41
|
+
autoload :HasMany, 'active_record/associations/preloader/has_many'
|
42
|
+
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
|
43
|
+
autoload :HasOne, 'active_record/associations/preloader/has_one'
|
44
|
+
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
|
45
|
+
autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
|
46
|
+
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :records, :associations, :preload_scope, :model
|
50
|
+
|
51
|
+
# Eager loads the named associations for the given Active Record record(s).
|
52
|
+
#
|
53
|
+
# In this description, 'association name' shall refer to the name passed
|
54
|
+
# to an association creation method. For example, a model that specifies
|
55
|
+
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
|
56
|
+
# names +:author+ and +:buyers+.
|
57
|
+
#
|
58
|
+
# == Parameters
|
59
|
+
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
|
60
|
+
# i.e. +records+ itself may also contain arrays of records. In any case,
|
61
|
+
# +preload_associations+ will preload the all associations records by
|
62
|
+
# flattening +records+.
|
63
|
+
#
|
64
|
+
# +associations+ specifies one or more associations that you want to
|
65
|
+
# preload. It may be:
|
66
|
+
# - a Symbol or a String which specifies a single association name. For
|
67
|
+
# example, specifying +:books+ allows this method to preload all books
|
68
|
+
# for an Author.
|
69
|
+
# - an Array which specifies multiple association names. This array
|
70
|
+
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
|
71
|
+
# allows this method to preload an author's avatar as well as all of his
|
72
|
+
# books.
|
73
|
+
# - a Hash which specifies multiple association names, as well as
|
74
|
+
# association names for the to-be-preloaded association objects. For
|
75
|
+
# example, specifying <tt>{ author: :avatar }</tt> will preload a
|
76
|
+
# book's author, as well as that author's avatar.
|
77
|
+
#
|
78
|
+
# +:associations+ has the same format as the +:include+ option for
|
79
|
+
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
|
80
|
+
#
|
81
|
+
# :books
|
82
|
+
# [ :books, :author ]
|
83
|
+
# { author: :avatar }
|
84
|
+
# [ :books, { author: :avatar } ]
|
85
|
+
def initialize(records, associations, preload_scope = nil)
|
86
|
+
@records = Array.wrap(records).compact.uniq
|
87
|
+
@associations = Array.wrap(associations)
|
88
|
+
@preload_scope = preload_scope || Relation.new(nil, nil)
|
89
|
+
end
|
90
|
+
|
91
|
+
def run
|
92
|
+
unless records.empty?
|
93
|
+
associations.each { |association| preload(association) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def preload(association)
|
100
|
+
case association
|
101
|
+
when Hash
|
102
|
+
preload_hash(association)
|
103
|
+
when Symbol
|
104
|
+
preload_one(association)
|
105
|
+
when String
|
106
|
+
preload_one(association.to_sym)
|
107
|
+
else
|
108
|
+
raise ArgumentError, "#{association.inspect} was not recognised for preload"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def preload_hash(association)
|
113
|
+
association.each do |parent, child|
|
114
|
+
Preloader.new(records, parent, preload_scope).run
|
115
|
+
Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Not all records have the same class, so group then preload group on the reflection
|
120
|
+
# itself so that if various subclass share the same association then we do not split
|
121
|
+
# them unnecessarily
|
122
|
+
#
|
123
|
+
# Additionally, polymorphic belongs_to associations can have multiple associated
|
124
|
+
# classes, depending on the polymorphic_type field. So we group by the classes as
|
125
|
+
# well.
|
126
|
+
def preload_one(association)
|
127
|
+
grouped_records(association).each do |reflection, klasses|
|
128
|
+
klasses.each do |klass, records|
|
129
|
+
preloader_for(reflection).new(klass, records, reflection, preload_scope).run
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def grouped_records(association)
|
135
|
+
Hash[
|
136
|
+
records_by_reflection(association).map do |reflection, records|
|
137
|
+
[reflection, records.group_by { |record| association_klass(reflection, record) }]
|
138
|
+
end
|
139
|
+
]
|
140
|
+
end
|
141
|
+
|
142
|
+
def records_by_reflection(association)
|
143
|
+
records.group_by do |record|
|
144
|
+
reflection = record.class.reflections[association]
|
145
|
+
|
146
|
+
unless reflection
|
147
|
+
raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
|
148
|
+
"perhaps you misspelled it?"
|
149
|
+
end
|
150
|
+
|
151
|
+
reflection
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def association_klass(reflection, record)
|
156
|
+
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
|
157
|
+
klass = record.send(reflection.foreign_type)
|
158
|
+
klass && klass.constantize
|
159
|
+
else
|
160
|
+
reflection.klass
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def preloader_for(reflection)
|
165
|
+
case reflection.macro
|
166
|
+
when :has_many
|
167
|
+
reflection.options[:through] ? HasManyThrough : HasMany
|
168
|
+
when :has_one
|
169
|
+
reflection.options[:through] ? HasOneThrough : HasOne
|
170
|
+
when :has_and_belongs_to_many
|
171
|
+
HasAndBelongsToMany
|
172
|
+
when :belongs_to
|
173
|
+
BelongsTo
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class SingularAssociation < Association #:nodoc:
|
4
|
+
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
|
5
|
+
def reader(force_reload = false)
|
6
|
+
if force_reload
|
7
|
+
klass.uncached { reload }
|
8
|
+
elsif !loaded? || stale_target?
|
9
|
+
reload
|
10
|
+
end
|
11
|
+
|
12
|
+
target
|
13
|
+
end
|
14
|
+
|
15
|
+
# Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
|
16
|
+
def writer(record)
|
17
|
+
replace(record)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create(attributes = {}, &block)
|
21
|
+
create_record(attributes, &block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create!(attributes = {}, &block)
|
25
|
+
create_record(attributes, true, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def build(attributes = {})
|
29
|
+
record = build_record(attributes)
|
30
|
+
yield(record) if block_given?
|
31
|
+
set_new_record(record)
|
32
|
+
record
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def create_scope
|
38
|
+
scope.scope_for_create.stringify_keys.except(klass.primary_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_target
|
42
|
+
scope.first.tap { |record| set_inverse_instance(record) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Implemented by subclasses
|
46
|
+
def replace(record)
|
47
|
+
raise NotImplementedError, "Subclasses must implement a replace(record) method"
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_new_record(record)
|
51
|
+
replace(record)
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_record(attributes, raise_error = false)
|
55
|
+
record = build_record(attributes)
|
56
|
+
yield(record) if block_given?
|
57
|
+
saved = record.save
|
58
|
+
set_new_record(record)
|
59
|
+
raise RecordInvalid.new(record) if !saved && raise_error
|
60
|
+
record
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
# = Active Record Through Association
|
3
|
+
module Associations
|
4
|
+
module ThroughAssociation #:nodoc:
|
5
|
+
|
6
|
+
delegate :source_reflection, :through_reflection, :chain, :to => :reflection
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
# We merge in these scopes for two reasons:
|
11
|
+
#
|
12
|
+
# 1. To get the default_scope conditions for any of the other reflections in the chain
|
13
|
+
# 2. To get the type conditions for any STI models in the chain
|
14
|
+
def target_scope
|
15
|
+
scope = super
|
16
|
+
chain[1..-1].each do |reflection|
|
17
|
+
scope.merge!(
|
18
|
+
reflection.klass.all.with_default_scope.
|
19
|
+
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
scope
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Construct attributes for :through pointing to owner and associate. This is used by the
|
28
|
+
# methods which create and delete records on the association.
|
29
|
+
#
|
30
|
+
# We only support indirectly modifying through associations which has a belongs_to source.
|
31
|
+
# This is the "has_many :tags, through: :taggings" situation, where the join model
|
32
|
+
# typically has a belongs_to on both side. In other words, associations which could also
|
33
|
+
# be represented as has_and_belongs_to_many associations.
|
34
|
+
#
|
35
|
+
# We do not support creating/deleting records on the association where the source has
|
36
|
+
# some other type, because this opens up a whole can of worms, and in basically any
|
37
|
+
# situation it is more natural for the user to just create or modify their join records
|
38
|
+
# directly as required.
|
39
|
+
def construct_join_attributes(*records)
|
40
|
+
ensure_mutable
|
41
|
+
|
42
|
+
join_attributes = {
|
43
|
+
source_reflection.foreign_key =>
|
44
|
+
records.map { |record|
|
45
|
+
record.send(source_reflection.association_primary_key(reflection.klass))
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
if options[:source_type]
|
50
|
+
join_attributes[source_reflection.foreign_type] =
|
51
|
+
records.map { |record| record.class.base_class.name }
|
52
|
+
end
|
53
|
+
|
54
|
+
if records.count == 1
|
55
|
+
Hash[join_attributes.map { |k, v| [k, v.first] }]
|
56
|
+
else
|
57
|
+
join_attributes
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Note: this does not capture all cases, for example it would be crazy to try to
|
62
|
+
# properly support stale-checking for nested associations.
|
63
|
+
def stale_state
|
64
|
+
if through_reflection.macro == :belongs_to
|
65
|
+
owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def foreign_key_present?
|
70
|
+
through_reflection.macro == :belongs_to &&
|
71
|
+
!owner[through_reflection.foreign_key].nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
def ensure_mutable
|
75
|
+
if source_reflection.macro != :belongs_to
|
76
|
+
raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def ensure_not_nested
|
81
|
+
if reflection.nested?
|
82
|
+
raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|