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,174 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class JoinDependency # :nodoc:
|
4
|
+
class JoinAssociation < JoinPart # :nodoc:
|
5
|
+
include JoinHelper
|
6
|
+
|
7
|
+
# The reflection of the association represented
|
8
|
+
attr_reader :reflection
|
9
|
+
|
10
|
+
# The JoinDependency object which this JoinAssociation exists within. This is mainly
|
11
|
+
# relevant for generating aliases which do not conflict with other joins which are
|
12
|
+
# part of the query.
|
13
|
+
attr_reader :join_dependency
|
14
|
+
|
15
|
+
# A JoinBase instance representing the active record we are joining onto.
|
16
|
+
# (So in Author.has_many :posts, the Author would be that base record.)
|
17
|
+
attr_reader :parent
|
18
|
+
|
19
|
+
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
|
20
|
+
attr_accessor :join_type
|
21
|
+
|
22
|
+
# These implement abstract methods from the superclass
|
23
|
+
attr_reader :aliased_prefix
|
24
|
+
|
25
|
+
attr_reader :tables
|
26
|
+
|
27
|
+
delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
|
28
|
+
delegate :table, :table_name, :to => :parent, :prefix => :parent
|
29
|
+
delegate :alias_tracker, :to => :join_dependency
|
30
|
+
|
31
|
+
alias :alias_suffix :parent_table_name
|
32
|
+
|
33
|
+
def initialize(reflection, join_dependency, parent = nil)
|
34
|
+
reflection.check_validity!
|
35
|
+
|
36
|
+
if reflection.options[:polymorphic]
|
37
|
+
raise EagerLoadPolymorphicError.new(reflection)
|
38
|
+
end
|
39
|
+
|
40
|
+
super(reflection.klass)
|
41
|
+
|
42
|
+
@reflection = reflection
|
43
|
+
@join_dependency = join_dependency
|
44
|
+
@parent = parent
|
45
|
+
@join_type = Arel::InnerJoin
|
46
|
+
@aliased_prefix = "t#{ join_dependency.join_parts.size }"
|
47
|
+
@tables = construct_tables.reverse
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
other.class == self.class &&
|
52
|
+
other.reflection == reflection &&
|
53
|
+
other.parent == parent
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_parent_in(other_join_dependency)
|
57
|
+
other_join_dependency.join_parts.detect do |join_part|
|
58
|
+
case parent
|
59
|
+
when JoinBase
|
60
|
+
parent.base_klass == join_part.base_klass
|
61
|
+
else
|
62
|
+
parent == join_part
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def join_to(manager)
|
68
|
+
tables = @tables.dup
|
69
|
+
foreign_table = parent_table
|
70
|
+
foreign_klass = parent.base_klass
|
71
|
+
|
72
|
+
# The chain starts with the target table, but we want to end with it here (makes
|
73
|
+
# more sense in this context), so we reverse
|
74
|
+
chain.reverse.each_with_index do |reflection, i|
|
75
|
+
table = tables.shift
|
76
|
+
|
77
|
+
case reflection.source_macro
|
78
|
+
when :belongs_to
|
79
|
+
key = reflection.association_primary_key
|
80
|
+
foreign_key = reflection.foreign_key
|
81
|
+
when :has_and_belongs_to_many
|
82
|
+
# Join the join table first...
|
83
|
+
manager.from(join(
|
84
|
+
table,
|
85
|
+
table[reflection.foreign_key].
|
86
|
+
eq(foreign_table[reflection.active_record_primary_key])
|
87
|
+
))
|
88
|
+
|
89
|
+
foreign_table, table = table, tables.shift
|
90
|
+
|
91
|
+
key = reflection.association_primary_key
|
92
|
+
foreign_key = reflection.association_foreign_key
|
93
|
+
else
|
94
|
+
key = reflection.foreign_key
|
95
|
+
foreign_key = reflection.active_record_primary_key
|
96
|
+
end
|
97
|
+
|
98
|
+
constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
|
99
|
+
|
100
|
+
scope_chain_items = scope_chain[i]
|
101
|
+
|
102
|
+
if reflection.type
|
103
|
+
scope_chain_items += [
|
104
|
+
ActiveRecord::Relation.new(reflection.klass, table)
|
105
|
+
.where(reflection.type => foreign_klass.base_class.name)
|
106
|
+
]
|
107
|
+
end
|
108
|
+
|
109
|
+
scope_chain_items.each do |item|
|
110
|
+
unless item.is_a?(Relation)
|
111
|
+
item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
|
112
|
+
end
|
113
|
+
|
114
|
+
constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
|
115
|
+
end
|
116
|
+
|
117
|
+
manager.from(join(table, constraint))
|
118
|
+
|
119
|
+
# The current table in this iteration becomes the foreign table in the next
|
120
|
+
foreign_table, foreign_klass = table, reflection.klass
|
121
|
+
end
|
122
|
+
|
123
|
+
manager
|
124
|
+
end
|
125
|
+
|
126
|
+
# Builds equality condition.
|
127
|
+
#
|
128
|
+
# Example:
|
129
|
+
#
|
130
|
+
# class Physician < ActiveRecord::Base
|
131
|
+
# has_many :appointments
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# If I execute `Physician.joins(:appointments).to_a` then
|
135
|
+
# reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
|
136
|
+
# table #=> #<Arel::Table @name="appointments" ...>
|
137
|
+
# key #=> physician_id
|
138
|
+
# foreign_table #=> #<Arel::Table @name="physicians" ...>
|
139
|
+
# foreign_key #=> id
|
140
|
+
#
|
141
|
+
def build_constraint(reflection, table, key, foreign_table, foreign_key)
|
142
|
+
constraint = table[key].eq(foreign_table[foreign_key])
|
143
|
+
|
144
|
+
if reflection.klass.finder_needs_type_condition?
|
145
|
+
constraint = table.create_and([
|
146
|
+
constraint,
|
147
|
+
reflection.klass.send(:type_condition, table)
|
148
|
+
])
|
149
|
+
end
|
150
|
+
|
151
|
+
constraint
|
152
|
+
end
|
153
|
+
|
154
|
+
def join_relation(joining_relation)
|
155
|
+
self.join_type = Arel::OuterJoin
|
156
|
+
joining_relation.joins(self)
|
157
|
+
end
|
158
|
+
|
159
|
+
def table
|
160
|
+
tables.last
|
161
|
+
end
|
162
|
+
|
163
|
+
def aliased_table_name
|
164
|
+
table.table_alias || table.name
|
165
|
+
end
|
166
|
+
|
167
|
+
def scope_chain
|
168
|
+
@scope_chain ||= reflection.scope_chain.reverse
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class JoinDependency # :nodoc:
|
4
|
+
class JoinBase < JoinPart # :nodoc:
|
5
|
+
def ==(other)
|
6
|
+
other.class == self.class &&
|
7
|
+
other.base_klass == base_klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def aliased_prefix
|
11
|
+
"t0"
|
12
|
+
end
|
13
|
+
|
14
|
+
def table
|
15
|
+
Arel::Table.new(table_name, arel_engine)
|
16
|
+
end
|
17
|
+
|
18
|
+
def aliased_table_name
|
19
|
+
base_klass.table_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class JoinDependency # :nodoc:
|
4
|
+
# A JoinPart represents a part of a JoinDependency. It is inherited
|
5
|
+
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
|
6
|
+
# everything else is being joined onto. A JoinAssociation represents an association which
|
7
|
+
# is joining to the base. A JoinAssociation may result in more than one actual join
|
8
|
+
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
|
9
|
+
# two; one for the join table and one for the target table).
|
10
|
+
class JoinPart # :nodoc:
|
11
|
+
# The Active Record class which this join part is associated 'about'; for a JoinBase
|
12
|
+
# this is the actual base model, for a JoinAssociation this is the target model of the
|
13
|
+
# association.
|
14
|
+
attr_reader :base_klass
|
15
|
+
|
16
|
+
delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass
|
17
|
+
|
18
|
+
def initialize(base_klass)
|
19
|
+
@base_klass = base_klass
|
20
|
+
@cached_record = {}
|
21
|
+
@column_names_with_alias = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def aliased_table
|
25
|
+
Arel::Nodes::TableAlias.new table, aliased_table_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
# An Arel::Table for the active_record
|
33
|
+
def table
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
# The prefix to be used when aliasing columns in the active_record's table
|
38
|
+
def aliased_prefix
|
39
|
+
raise NotImplementedError
|
40
|
+
end
|
41
|
+
|
42
|
+
# The alias for the active_record's table
|
43
|
+
def aliased_table_name
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
# The alias for the primary key of the active_record's table
|
48
|
+
def aliased_primary_key
|
49
|
+
"#{aliased_prefix}_r0"
|
50
|
+
end
|
51
|
+
|
52
|
+
# An array of [column_name, alias] pairs for the table
|
53
|
+
def column_names_with_alias
|
54
|
+
unless @column_names_with_alias
|
55
|
+
@column_names_with_alias = []
|
56
|
+
|
57
|
+
([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
|
58
|
+
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
@column_names_with_alias
|
62
|
+
end
|
63
|
+
|
64
|
+
def extract_record(row)
|
65
|
+
Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
|
66
|
+
end
|
67
|
+
|
68
|
+
def record_id(row)
|
69
|
+
row[aliased_primary_key]
|
70
|
+
end
|
71
|
+
|
72
|
+
def instantiate(row)
|
73
|
+
@cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class JoinDependency # :nodoc:
|
4
|
+
autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
|
5
|
+
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
|
6
|
+
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
|
7
|
+
|
8
|
+
attr_reader :join_parts, :reflections, :alias_tracker, :base_klass
|
9
|
+
|
10
|
+
# base is the base class on which operation is taking place.
|
11
|
+
# associations is the list of associations which are joined using hash, symbol or array.
|
12
|
+
# joins is the list of all string join commnads and arel nodes.
|
13
|
+
#
|
14
|
+
# Example :
|
15
|
+
#
|
16
|
+
# class Physician < ActiveRecord::Base
|
17
|
+
# has_many :appointments
|
18
|
+
# has_many :patients, through: :appointments
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# If I execute `@physician.patients.to_a` then
|
22
|
+
# base #=> Physician
|
23
|
+
# associations #=> []
|
24
|
+
# joins #=> [#<Arel::Nodes::InnerJoin: ...]
|
25
|
+
#
|
26
|
+
# However if I execute `Physician.joins(:appointments).to_a` then
|
27
|
+
# base #=> Physician
|
28
|
+
# associations #=> [:appointments]
|
29
|
+
# joins #=> []
|
30
|
+
#
|
31
|
+
def initialize(base, associations, joins)
|
32
|
+
@base_klass = base
|
33
|
+
@table_joins = joins
|
34
|
+
@join_parts = [JoinBase.new(base)]
|
35
|
+
@associations = {}
|
36
|
+
@reflections = []
|
37
|
+
@alias_tracker = AliasTracker.new(base.connection, joins)
|
38
|
+
@alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
|
39
|
+
build(associations)
|
40
|
+
end
|
41
|
+
|
42
|
+
def graft(*associations)
|
43
|
+
associations.each do |association|
|
44
|
+
join_associations.detect {|a| association == a} ||
|
45
|
+
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
|
46
|
+
end
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def join_associations
|
51
|
+
join_parts.last(join_parts.length - 1)
|
52
|
+
end
|
53
|
+
|
54
|
+
def join_base
|
55
|
+
join_parts.first
|
56
|
+
end
|
57
|
+
|
58
|
+
def columns
|
59
|
+
join_parts.collect { |join_part|
|
60
|
+
table = join_part.aliased_table
|
61
|
+
join_part.column_names_with_alias.collect{ |column_name, aliased_name|
|
62
|
+
table[column_name].as Arel.sql(aliased_name)
|
63
|
+
}
|
64
|
+
}.flatten
|
65
|
+
end
|
66
|
+
|
67
|
+
def instantiate(rows)
|
68
|
+
primary_key = join_base.aliased_primary_key
|
69
|
+
parents = {}
|
70
|
+
|
71
|
+
records = rows.map { |model|
|
72
|
+
primary_id = model[primary_key]
|
73
|
+
parent = parents[primary_id] ||= join_base.instantiate(model)
|
74
|
+
construct(parent, @associations, join_associations, model)
|
75
|
+
parent
|
76
|
+
}.uniq
|
77
|
+
|
78
|
+
remove_duplicate_results!(base_klass, records, @associations)
|
79
|
+
records
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def remove_duplicate_results!(base, records, associations)
|
85
|
+
case associations
|
86
|
+
when Symbol, String
|
87
|
+
reflection = base.reflections[associations]
|
88
|
+
remove_uniq_by_reflection(reflection, records)
|
89
|
+
when Array
|
90
|
+
associations.each do |association|
|
91
|
+
remove_duplicate_results!(base, records, association)
|
92
|
+
end
|
93
|
+
when Hash
|
94
|
+
associations.each_key do |name|
|
95
|
+
reflection = base.reflections[name]
|
96
|
+
remove_uniq_by_reflection(reflection, records)
|
97
|
+
|
98
|
+
parent_records = []
|
99
|
+
records.each do |record|
|
100
|
+
if descendant = record.send(reflection.name)
|
101
|
+
if reflection.collection?
|
102
|
+
parent_records.concat descendant.target.uniq
|
103
|
+
else
|
104
|
+
parent_records << descendant
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def cache_joined_association(association)
|
115
|
+
associations = []
|
116
|
+
parent = association.parent
|
117
|
+
while parent != join_base
|
118
|
+
associations.unshift(parent.reflection.name)
|
119
|
+
parent = parent.parent
|
120
|
+
end
|
121
|
+
ref = @associations
|
122
|
+
associations.each do |key|
|
123
|
+
ref = ref[key]
|
124
|
+
end
|
125
|
+
ref[association.reflection.name] ||= {}
|
126
|
+
end
|
127
|
+
|
128
|
+
def build(associations, parent = nil, join_type = Arel::InnerJoin)
|
129
|
+
parent ||= join_parts.last
|
130
|
+
case associations
|
131
|
+
when Symbol, String
|
132
|
+
reflection = parent.reflections[associations.intern] or
|
133
|
+
raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?"
|
134
|
+
unless join_association = find_join_association(reflection, parent)
|
135
|
+
@reflections << reflection
|
136
|
+
join_association = build_join_association(reflection, parent)
|
137
|
+
join_association.join_type = join_type
|
138
|
+
@join_parts << join_association
|
139
|
+
cache_joined_association(join_association)
|
140
|
+
end
|
141
|
+
join_association
|
142
|
+
when Array
|
143
|
+
associations.each do |association|
|
144
|
+
build(association, parent, join_type)
|
145
|
+
end
|
146
|
+
when Hash
|
147
|
+
associations.keys.sort_by { |a| a.to_s }.each do |name|
|
148
|
+
join_association = build(name, parent, join_type)
|
149
|
+
build(associations[name], join_association, join_type)
|
150
|
+
end
|
151
|
+
else
|
152
|
+
raise ConfigurationError, associations.inspect
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_join_association(name_or_reflection, parent)
|
157
|
+
if String === name_or_reflection
|
158
|
+
name_or_reflection = name_or_reflection.to_sym
|
159
|
+
end
|
160
|
+
|
161
|
+
join_associations.detect { |j|
|
162
|
+
j.reflection == name_or_reflection && j.parent == parent
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def remove_uniq_by_reflection(reflection, records)
|
167
|
+
if reflection && reflection.collection?
|
168
|
+
records.each { |record| record.send(reflection.name).target.uniq! }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def build_join_association(reflection, parent)
|
173
|
+
JoinAssociation.new(reflection, self, parent)
|
174
|
+
end
|
175
|
+
|
176
|
+
def construct(parent, associations, join_parts, row)
|
177
|
+
case associations
|
178
|
+
when Symbol, String
|
179
|
+
name = associations.to_s
|
180
|
+
|
181
|
+
join_part = join_parts.detect { |j|
|
182
|
+
j.reflection.name.to_s == name &&
|
183
|
+
j.parent_table_name == parent.class.table_name }
|
184
|
+
|
185
|
+
raise(ConfigurationError, "No such association") unless join_part
|
186
|
+
|
187
|
+
join_parts.delete(join_part)
|
188
|
+
construct_association(parent, join_part, row)
|
189
|
+
when Array
|
190
|
+
associations.each do |association|
|
191
|
+
construct(parent, association, join_parts, row)
|
192
|
+
end
|
193
|
+
when Hash
|
194
|
+
associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
|
195
|
+
association = construct(parent, association_name, join_parts, row)
|
196
|
+
construct(association, assoc, join_parts, row) if association
|
197
|
+
end
|
198
|
+
else
|
199
|
+
raise ConfigurationError, associations.inspect
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def construct_association(record, join_part, row)
|
204
|
+
return if record.id.to_s != join_part.parent.record_id(row).to_s
|
205
|
+
|
206
|
+
macro = join_part.reflection.macro
|
207
|
+
if macro == :has_one
|
208
|
+
return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
|
209
|
+
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
|
210
|
+
set_target_and_inverse(join_part, association, record)
|
211
|
+
else
|
212
|
+
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
|
213
|
+
case macro
|
214
|
+
when :has_many, :has_and_belongs_to_many
|
215
|
+
other = record.association(join_part.reflection.name)
|
216
|
+
other.loaded!
|
217
|
+
other.target.push(association) if association
|
218
|
+
other.set_inverse_instance(association)
|
219
|
+
when :belongs_to
|
220
|
+
set_target_and_inverse(join_part, association, record)
|
221
|
+
else
|
222
|
+
raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
association
|
226
|
+
end
|
227
|
+
|
228
|
+
def set_target_and_inverse(join_part, association, record)
|
229
|
+
other = record.association(join_part.reflection.name)
|
230
|
+
other.target = association
|
231
|
+
other.set_inverse_instance(association)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
# Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
|
4
|
+
module JoinHelper #:nodoc:
|
5
|
+
|
6
|
+
def join_type
|
7
|
+
Arel::InnerJoin
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def construct_tables
|
13
|
+
tables = []
|
14
|
+
chain.each do |reflection|
|
15
|
+
tables << alias_tracker.aliased_table_for(
|
16
|
+
table_name_for(reflection),
|
17
|
+
table_alias_for(reflection, reflection != self.reflection)
|
18
|
+
)
|
19
|
+
|
20
|
+
if reflection.source_macro == :has_and_belongs_to_many
|
21
|
+
tables << alias_tracker.aliased_table_for(
|
22
|
+
(reflection.source_reflection || reflection).join_table,
|
23
|
+
table_alias_for(reflection, true)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
tables
|
28
|
+
end
|
29
|
+
|
30
|
+
def table_name_for(reflection)
|
31
|
+
reflection.table_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def table_alias_for(reflection, join = false)
|
35
|
+
name = "#{reflection.plural_name}_#{alias_suffix}"
|
36
|
+
name << "_join" if join
|
37
|
+
name
|
38
|
+
end
|
39
|
+
|
40
|
+
def join(table, constraint)
|
41
|
+
table.create_join(table, table.create_on(constraint), join_type)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class Association #:nodoc:
|
5
|
+
attr_reader :owners, :reflection, :preload_scope, :model, :klass
|
6
|
+
|
7
|
+
def initialize(klass, owners, reflection, preload_scope)
|
8
|
+
@klass = klass
|
9
|
+
@owners = owners
|
10
|
+
@reflection = reflection
|
11
|
+
@preload_scope = preload_scope
|
12
|
+
@model = owners.first && owners.first.class
|
13
|
+
@scope = nil
|
14
|
+
@owners_by_key = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
unless owners.first.association(reflection.name).loaded?
|
19
|
+
preload
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def preload
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def scope
|
28
|
+
@scope ||= build_scope
|
29
|
+
end
|
30
|
+
|
31
|
+
def records_for(ids)
|
32
|
+
scope.where(association_key.in(ids))
|
33
|
+
end
|
34
|
+
|
35
|
+
def table
|
36
|
+
klass.arel_table
|
37
|
+
end
|
38
|
+
|
39
|
+
# The name of the key on the associated records
|
40
|
+
def association_key_name
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
# This is overridden by HABTM as the condition should be on the foreign_key column in
|
45
|
+
# the join table
|
46
|
+
def association_key
|
47
|
+
table[association_key_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
# The name of the key on the model which declares the association
|
51
|
+
def owner_key_name
|
52
|
+
raise NotImplementedError
|
53
|
+
end
|
54
|
+
|
55
|
+
# We're converting to a string here because postgres will return the aliased association
|
56
|
+
# key in a habtm as a string (for whatever reason)
|
57
|
+
def owners_by_key
|
58
|
+
@owners_by_key ||= owners.group_by do |owner|
|
59
|
+
key = owner[owner_key_name]
|
60
|
+
key && key.to_s
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def options
|
65
|
+
reflection.options
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def associated_records_by_owner
|
71
|
+
owners_map = owners_by_key
|
72
|
+
owner_keys = owners_map.keys.compact
|
73
|
+
|
74
|
+
if klass.nil? || owner_keys.empty?
|
75
|
+
records = []
|
76
|
+
else
|
77
|
+
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
|
78
|
+
# Make several smaller queries if necessary or make one query if the adapter supports it
|
79
|
+
sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
|
80
|
+
records = sliced.map { |slice| records_for(slice).to_a }.flatten
|
81
|
+
end
|
82
|
+
|
83
|
+
# Each record may have multiple owners, and vice-versa
|
84
|
+
records_by_owner = Hash[owners.map { |owner| [owner, []] }]
|
85
|
+
records.each do |record|
|
86
|
+
owner_key = record[association_key_name].to_s
|
87
|
+
|
88
|
+
owners_map[owner_key].each do |owner|
|
89
|
+
records_by_owner[owner] << record
|
90
|
+
end
|
91
|
+
end
|
92
|
+
records_by_owner
|
93
|
+
end
|
94
|
+
|
95
|
+
def reflection_scope
|
96
|
+
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_scope
|
100
|
+
scope = klass.unscoped
|
101
|
+
scope.default_scoped = true
|
102
|
+
|
103
|
+
values = reflection_scope.values
|
104
|
+
preload_values = preload_scope.values
|
105
|
+
|
106
|
+
scope.where_values = Array(values[:where]) + Array(preload_values[:where])
|
107
|
+
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
|
108
|
+
|
109
|
+
scope.select! preload_values[:select] || values[:select] || table[Arel.star]
|
110
|
+
scope.includes! preload_values[:includes] || values[:includes]
|
111
|
+
|
112
|
+
if options[:as]
|
113
|
+
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
|
114
|
+
end
|
115
|
+
|
116
|
+
scope
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Associations
|
3
|
+
class Preloader
|
4
|
+
class BelongsTo < SingularAssociation #:nodoc:
|
5
|
+
|
6
|
+
def association_key_name
|
7
|
+
reflection.options[:primary_key] || klass && klass.primary_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def owner_key_name
|
11
|
+
reflection.foreign_key
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|