sskirby-activerecord 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +6749 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +222 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +177 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record.rb +147 -0
- data/lib/active_record/aggregations.rb +255 -0
- data/lib/active_record/associations.rb +1604 -0
- data/lib/active_record/associations/alias_tracker.rb +79 -0
- data/lib/active_record/associations/association.rb +239 -0
- data/lib/active_record/associations/association_scope.rb +119 -0
- data/lib/active_record/associations/belongs_to_association.rb +79 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
- data/lib/active_record/associations/builder/association.rb +55 -0
- data/lib/active_record/associations/builder/belongs_to.rb +85 -0
- data/lib/active_record/associations/builder/collection_association.rb +75 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
- data/lib/active_record/associations/builder/has_many.rb +71 -0
- data/lib/active_record/associations/builder/has_one.rb +62 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +574 -0
- data/lib/active_record/associations/collection_proxy.rb +132 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
- data/lib/active_record/associations/has_many_association.rb +108 -0
- data/lib/active_record/associations/has_many_through_association.rb +180 -0
- data/lib/active_record/associations/has_one_association.rb +73 -0
- data/lib/active_record/associations/has_one_through_association.rb +36 -0
- data/lib/active_record/associations/join_dependency.rb +214 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_helper.rb +55 -0
- data/lib/active_record/associations/preloader.rb +177 -0
- data/lib/active_record/associations/preloader/association.rb +127 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +67 -0
- data/lib/active_record/associations/singular_association.rb +64 -0
- data/lib/active_record/associations/through_association.rb +83 -0
- data/lib/active_record/attribute_assignment.rb +221 -0
- data/lib/active_record/attribute_methods.rb +272 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
- data/lib/active_record/attribute_methods/dirty.rb +101 -0
- data/lib/active_record/attribute_methods/primary_key.rb +114 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +135 -0
- data/lib/active_record/attribute_methods/serialization.rb +93 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
- data/lib/active_record/attribute_methods/write.rb +69 -0
- data/lib/active_record/autosave_association.rb +422 -0
- data/lib/active_record/base.rb +716 -0
- data/lib/active_record/callbacks.rb +275 -0
- data/lib/active_record/coders/yaml_column.rb +41 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
- data/lib/active_record/connection_adapters/column.rb +270 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
- data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
- data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
- data/lib/active_record/counter_cache.rb +119 -0
- data/lib/active_record/dynamic_finder_match.rb +56 -0
- data/lib/active_record/dynamic_matchers.rb +79 -0
- data/lib/active_record/dynamic_scope_match.rb +23 -0
- data/lib/active_record/errors.rb +195 -0
- data/lib/active_record/explain.rb +85 -0
- data/lib/active_record/explain_subscriber.rb +21 -0
- data/lib/active_record/fixtures.rb +906 -0
- data/lib/active_record/fixtures/file.rb +65 -0
- data/lib/active_record/identity_map.rb +156 -0
- data/lib/active_record/inheritance.rb +167 -0
- data/lib/active_record/integration.rb +49 -0
- data/lib/active_record/locale/en.yml +40 -0
- data/lib/active_record/locking/optimistic.rb +183 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/log_subscriber.rb +68 -0
- data/lib/active_record/migration.rb +765 -0
- data/lib/active_record/migration/command_recorder.rb +105 -0
- data/lib/active_record/model_schema.rb +366 -0
- data/lib/active_record/nested_attributes.rb +469 -0
- data/lib/active_record/observer.rb +121 -0
- data/lib/active_record/persistence.rb +372 -0
- data/lib/active_record/query_cache.rb +74 -0
- data/lib/active_record/querying.rb +58 -0
- data/lib/active_record/railtie.rb +119 -0
- data/lib/active_record/railties/console_sandbox.rb +6 -0
- data/lib/active_record/railties/controller_runtime.rb +49 -0
- data/lib/active_record/railties/databases.rake +620 -0
- data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
- data/lib/active_record/readonly_attributes.rb +26 -0
- data/lib/active_record/reflection.rb +534 -0
- data/lib/active_record/relation.rb +534 -0
- data/lib/active_record/relation/batches.rb +90 -0
- data/lib/active_record/relation/calculations.rb +354 -0
- data/lib/active_record/relation/delegation.rb +49 -0
- data/lib/active_record/relation/finder_methods.rb +398 -0
- data/lib/active_record/relation/predicate_builder.rb +58 -0
- data/lib/active_record/relation/query_methods.rb +417 -0
- data/lib/active_record/relation/spawn_methods.rb +148 -0
- data/lib/active_record/result.rb +34 -0
- data/lib/active_record/sanitization.rb +194 -0
- data/lib/active_record/schema.rb +58 -0
- data/lib/active_record/schema_dumper.rb +204 -0
- data/lib/active_record/scoping.rb +152 -0
- data/lib/active_record/scoping/default.rb +142 -0
- data/lib/active_record/scoping/named.rb +202 -0
- data/lib/active_record/serialization.rb +18 -0
- data/lib/active_record/serializers/xml_serializer.rb +202 -0
- data/lib/active_record/session_store.rb +358 -0
- data/lib/active_record/store.rb +50 -0
- data/lib/active_record/test_case.rb +73 -0
- data/lib/active_record/timestamp.rb +113 -0
- data/lib/active_record/transactions.rb +360 -0
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations.rb +83 -0
- data/lib/active_record/validations/associated.rb +43 -0
- data/lib/active_record/validations/uniqueness.rb +180 -0
- data/lib/active_record/version.rb +10 -0
- data/lib/rails/generators/active_record.rb +25 -0
- data/lib/rails/generators/active_record/migration.rb +15 -0
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
- metadata +242 -0
|
@@ -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.active_record == active_record
|
|
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
|
+
active_record.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 an abstract class, 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 :active_record
|
|
15
|
+
|
|
16
|
+
delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
|
|
17
|
+
|
|
18
|
+
def initialize(active_record)
|
|
19
|
+
@active_record = active_record
|
|
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])).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)] ||= active_record.send(:instantiate, extract_record(row))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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).options[: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
|
+
|
|
44
|
+
def sanitize(conditions, table)
|
|
45
|
+
conditions = conditions.map do |condition|
|
|
46
|
+
condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name)
|
|
47
|
+
condition = Arel.sql(condition) unless condition.is_a?(Arel::Node)
|
|
48
|
+
condition
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
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.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
|
+
autoload :Association, 'active_record/associations/preloader/association'
|
|
34
|
+
autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
|
|
35
|
+
autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
|
|
36
|
+
autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
|
|
37
|
+
|
|
38
|
+
autoload :HasMany, 'active_record/associations/preloader/has_many'
|
|
39
|
+
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
|
|
40
|
+
autoload :HasOne, 'active_record/associations/preloader/has_one'
|
|
41
|
+
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
|
|
42
|
+
autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
|
|
43
|
+
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
|
|
44
|
+
|
|
45
|
+
attr_reader :records, :associations, :options, :model
|
|
46
|
+
|
|
47
|
+
# Eager loads the named associations for the given Active Record record(s).
|
|
48
|
+
#
|
|
49
|
+
# In this description, 'association name' shall refer to the name passed
|
|
50
|
+
# to an association creation method. For example, a model that specifies
|
|
51
|
+
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
|
|
52
|
+
# names +:author+ and +:buyers+.
|
|
53
|
+
#
|
|
54
|
+
# == Parameters
|
|
55
|
+
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
|
|
56
|
+
# i.e. +records+ itself may also contain arrays of records. In any case,
|
|
57
|
+
# +preload_associations+ will preload the all associations records by
|
|
58
|
+
# flattening +records+.
|
|
59
|
+
#
|
|
60
|
+
# +associations+ specifies one or more associations that you want to
|
|
61
|
+
# preload. It may be:
|
|
62
|
+
# - a Symbol or a String which specifies a single association name. For
|
|
63
|
+
# example, specifying +:books+ allows this method to preload all books
|
|
64
|
+
# for an Author.
|
|
65
|
+
# - an Array which specifies multiple association names. This array
|
|
66
|
+
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
|
|
67
|
+
# allows this method to preload an author's avatar as well as all of his
|
|
68
|
+
# books.
|
|
69
|
+
# - a Hash which specifies multiple association names, as well as
|
|
70
|
+
# association names for the to-be-preloaded association objects. For
|
|
71
|
+
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
|
|
72
|
+
# book's author, as well as that author's avatar.
|
|
73
|
+
#
|
|
74
|
+
# +:associations+ has the same format as the +:include+ option for
|
|
75
|
+
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
|
|
76
|
+
#
|
|
77
|
+
# :books
|
|
78
|
+
# [ :books, :author ]
|
|
79
|
+
# { :author => :avatar }
|
|
80
|
+
# [ :books, { :author => :avatar } ]
|
|
81
|
+
#
|
|
82
|
+
# +options+ contains options that will be passed to ActiveRecord::Base#find
|
|
83
|
+
# (which is called under the hood for preloading records). But it is passed
|
|
84
|
+
# only one level deep in the +associations+ argument, i.e. it's not passed
|
|
85
|
+
# to the child associations when +associations+ is a Hash.
|
|
86
|
+
def initialize(records, associations, options = {})
|
|
87
|
+
@records = Array.wrap(records).compact.uniq
|
|
88
|
+
@associations = Array.wrap(associations)
|
|
89
|
+
@options = options
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run
|
|
93
|
+
unless records.empty?
|
|
94
|
+
associations.each { |association| preload(association) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def preload(association)
|
|
101
|
+
case association
|
|
102
|
+
when Hash
|
|
103
|
+
preload_hash(association)
|
|
104
|
+
when String, Symbol
|
|
105
|
+
preload_one(association.to_sym)
|
|
106
|
+
else
|
|
107
|
+
raise ArgumentError, "#{association.inspect} was not recognised for preload"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def preload_hash(association)
|
|
112
|
+
association.each do |parent, child|
|
|
113
|
+
Preloader.new(records, parent, options).run
|
|
114
|
+
Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Not all records have the same class, so group then preload group on the reflection
|
|
119
|
+
# itself so that if various subclass share the same association then we do not split
|
|
120
|
+
# them unnecessarily
|
|
121
|
+
#
|
|
122
|
+
# Additionally, polymorphic belongs_to associations can have multiple associated
|
|
123
|
+
# classes, depending on the polymorphic_type field. So we group by the classes as
|
|
124
|
+
# well.
|
|
125
|
+
def preload_one(association)
|
|
126
|
+
grouped_records(association).each do |reflection, klasses|
|
|
127
|
+
klasses.each do |klass, records|
|
|
128
|
+
preloader_for(reflection).new(klass, records, reflection, options).run
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def grouped_records(association)
|
|
134
|
+
Hash[
|
|
135
|
+
records_by_reflection(association).map do |reflection, records|
|
|
136
|
+
[reflection, records.group_by { |record| association_klass(reflection, record) }]
|
|
137
|
+
end
|
|
138
|
+
]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def records_by_reflection(association)
|
|
142
|
+
records.group_by do |record|
|
|
143
|
+
reflection = record.class.reflections[association]
|
|
144
|
+
|
|
145
|
+
unless reflection
|
|
146
|
+
raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
|
|
147
|
+
"perhaps you misspelled it?"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
reflection
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def association_klass(reflection, record)
|
|
155
|
+
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
|
|
156
|
+
klass = record.send(reflection.foreign_type)
|
|
157
|
+
klass && klass.constantize
|
|
158
|
+
else
|
|
159
|
+
reflection.klass
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def preloader_for(reflection)
|
|
164
|
+
case reflection.macro
|
|
165
|
+
when :has_many
|
|
166
|
+
reflection.options[:through] ? HasManyThrough : HasMany
|
|
167
|
+
when :has_one
|
|
168
|
+
reflection.options[:through] ? HasOneThrough : HasOne
|
|
169
|
+
when :has_and_belongs_to_many
|
|
170
|
+
HasAndBelongsToMany
|
|
171
|
+
when :belongs_to
|
|
172
|
+
BelongsTo
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module Associations
|
|
3
|
+
class Preloader
|
|
4
|
+
class Association #:nodoc:
|
|
5
|
+
attr_reader :owners, :reflection, :preload_options, :model, :klass
|
|
6
|
+
|
|
7
|
+
def initialize(klass, owners, reflection, preload_options)
|
|
8
|
+
@klass = klass
|
|
9
|
+
@owners = owners
|
|
10
|
+
@reflection = reflection
|
|
11
|
+
@preload_options = preload_options || {}
|
|
12
|
+
@model = owners.first && owners.first.class
|
|
13
|
+
@scoped = 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 scoped
|
|
28
|
+
@scoped ||= build_scope
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def records_for(ids)
|
|
32
|
+
scoped.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(model.connection.in_clause_length || owner_keys.size)
|
|
80
|
+
records = sliced.map { |slice| records_for(slice) }.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 build_scope
|
|
96
|
+
scope = klass.scoped
|
|
97
|
+
|
|
98
|
+
scope = scope.where(process_conditions(options[:conditions]))
|
|
99
|
+
scope = scope.where(process_conditions(preload_options[:conditions]))
|
|
100
|
+
|
|
101
|
+
scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
|
|
102
|
+
scope = scope.includes(preload_options[:include] || options[:include])
|
|
103
|
+
|
|
104
|
+
if options[:as]
|
|
105
|
+
scope = scope.where(
|
|
106
|
+
klass.table_name => {
|
|
107
|
+
reflection.type => model.base_class.sti_name
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
scope
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def process_conditions(conditions)
|
|
116
|
+
if conditions.respond_to?(:to_proc)
|
|
117
|
+
conditions = klass.send(:instance_eval, &conditions)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if conditions
|
|
121
|
+
klass.send(:sanitize_sql, conditions)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|