activerecord 3.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 +35 -44
- data/examples/performance.rb +110 -100
- data/lib/active_record/aggregations.rb +59 -75
- 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 +60 -59
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +16 -59
- 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 +40 -112
- data/lib/active_record/associations/has_many_association.rb +83 -76
- data/lib/active_record/associations/has_many_through_association.rb +147 -66
- data/lib/active_record/associations/has_one_association.rb +67 -108
- data/lib/active_record/associations/has_one_through_association.rb +21 -25
- 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 +512 -1224
- data/lib/active_record/attribute_assignment.rb +201 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +49 -12
- data/lib/active_record/attribute_methods/dirty.rb +51 -28
- data/lib/active_record/attribute_methods/primary_key.rb +94 -22
- data/lib/active_record/attribute_methods/query.rb +5 -4
- data/lib/active_record/attribute_methods/read.rb +63 -72
- data/lib/active_record/attribute_methods/serialization.rb +162 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -41
- data/lib/active_record/attribute_methods/write.rb +39 -13
- data/lib/active_record/attribute_methods.rb +362 -29
- data/lib/active_record/autosave_association.rb +132 -75
- data/lib/active_record/base.rb +83 -1627
- data/lib/active_record/callbacks.rb +69 -47
- data/lib/active_record/coders/yaml_column.rb +38 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +411 -138
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +21 -11
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +234 -173
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -22
- data/lib/active_record/connection_adapters/abstract/quoting.rb +82 -25
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +176 -414
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +562 -232
- data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +281 -53
- 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 +365 -450
- 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 +672 -752
- data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +588 -17
- 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 +108 -101
- data/lib/active_record/dynamic_matchers.rb +131 -0
- data/lib/active_record/errors.rb +54 -13
- 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 +703 -785
- data/lib/active_record/inheritance.rb +200 -0
- data/lib/active_record/integration.rb +60 -0
- data/lib/active_record/locale/en.yml +8 -1
- data/lib/active_record/locking/optimistic.rb +69 -60
- data/lib/active_record/locking/pessimistic.rb +34 -12
- data/lib/active_record/log_subscriber.rb +40 -6
- 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 +614 -216
- data/lib/active_record/model_schema.rb +345 -0
- data/lib/active_record/nested_attributes.rb +248 -119
- data/lib/active_record/null_relation.rb +65 -0
- data/lib/active_record/persistence.rb +275 -57
- data/lib/active_record/query_cache.rb +29 -9
- data/lib/active_record/querying.rb +62 -0
- data/lib/active_record/railtie.rb +135 -21
- data/lib/active_record/railties/console_sandbox.rb +5 -0
- data/lib/active_record/railties/controller_runtime.rb +17 -5
- data/lib/active_record/railties/databases.rake +249 -359
- 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 +283 -103
- data/lib/active_record/relation/batches.rb +38 -34
- data/lib/active_record/relation/calculations.rb +252 -139
- data/lib/active_record/relation/delegation.rb +125 -0
- data/lib/active_record/relation/finder_methods.rb +182 -188
- data/lib/active_record/relation/merger.rb +161 -0
- data/lib/active_record/relation/predicate_builder.rb +86 -21
- data/lib/active_record/relation/query_methods.rb +917 -134
- data/lib/active_record/relation/spawn_methods.rb +53 -92
- data/lib/active_record/relation.rb +405 -143
- 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 +20 -14
- data/lib/active_record/schema_dumper.rb +55 -46
- 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 +8 -46
- data/lib/active_record/serializers/xml_serializer.rb +21 -68
- 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 +57 -28
- data/lib/active_record/timestamp.rb +49 -18
- data/lib/active_record/transactions.rb +106 -63
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations/associated.rb +25 -24
- data/lib/active_record/validations/presence.rb +65 -0
- data/lib/active_record/validations/uniqueness.rb +123 -83
- data/lib/active_record/validations.rb +29 -29
- data/lib/active_record/version.rb +7 -5
- data/lib/active_record.rb +83 -34
- data/lib/rails/generators/active_record/migration/migration_generator.rb +46 -9
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +30 -8
- data/lib/rails/generators/active_record/model/model_generator.rb +15 -5
- data/lib/rails/generators/active_record/model/templates/model.rb +7 -2
- data/lib/rails/generators/active_record/model/templates/module.rb +3 -1
- data/lib/rails/generators/active_record.rb +4 -8
- metadata +163 -121
- data/CHANGELOG +0 -6023
- data/examples/associations.png +0 -0
- data/lib/active_record/association_preload.rb +0 -403
- data/lib/active_record/associations/association_collection.rb +0 -562
- data/lib/active_record/associations/association_proxy.rb +0 -295
- data/lib/active_record/associations/through_association_scope.rb +0 -154
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -113
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -401
- data/lib/active_record/dynamic_finder_match.rb +0 -53
- data/lib/active_record/dynamic_scope_match.rb +0 -32
- data/lib/active_record/named_scope.rb +0 -138
- data/lib/active_record/observer.rb +0 -140
- data/lib/active_record/session_store.rb +0 -340
- data/lib/rails/generators/active_record/model/templates/migration.rb +0 -16
- data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
- data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -2
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -24
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -1,136 +1,64 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
# = Active Record Has And Belongs To Many Association
|
3
3
|
module Associations
|
4
|
-
class HasAndBelongsToManyAssociation <
|
5
|
-
|
6
|
-
create_record(attributes) { |record| insert_record(record) }
|
7
|
-
end
|
4
|
+
class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
|
5
|
+
attr_reader :join_table
|
8
6
|
|
9
|
-
def
|
10
|
-
|
7
|
+
def initialize(owner, reflection)
|
8
|
+
@join_table = Arel::Table.new(reflection.join_table)
|
9
|
+
super
|
11
10
|
end
|
12
11
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
12
|
+
def insert_record(record, validate = true, raise = false)
|
13
|
+
if record.new_record?
|
14
|
+
if raise
|
15
|
+
record.save!(:validate => validate)
|
16
|
+
else
|
17
|
+
return unless record.save(:validate => validate)
|
18
|
+
end
|
19
|
+
end
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
21
|
+
if options[:insert_sql]
|
22
|
+
owner.connection.insert(interpolate(options[:insert_sql], record))
|
23
|
+
else
|
24
|
+
stmt = join_table.compile_insert(
|
25
|
+
join_table[reflection.foreign_key] => owner.id,
|
26
|
+
join_table[reflection.association_foreign_key] => record.id
|
27
|
+
)
|
28
|
+
|
29
|
+
owner.class.connection.insert stmt
|
30
|
+
end
|
20
31
|
|
21
|
-
|
22
|
-
@has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
|
32
|
+
record
|
23
33
|
end
|
24
34
|
|
25
|
-
|
26
|
-
def construct_find_options!(options)
|
27
|
-
options[:joins] = Arel::SqlLiteral.new @join_sql
|
28
|
-
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
|
29
|
-
options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*'))
|
30
|
-
end
|
35
|
+
private
|
31
36
|
|
32
37
|
def count_records
|
33
38
|
load_target.size
|
34
39
|
end
|
35
40
|
|
36
|
-
def
|
37
|
-
if
|
38
|
-
if
|
39
|
-
|
40
|
-
else
|
41
|
-
return false unless record.save(:validate => validate)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
if @reflection.options[:insert_sql]
|
46
|
-
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
|
41
|
+
def delete_records(records, method)
|
42
|
+
if sql = options[:delete_sql]
|
43
|
+
records = load_target if records == :all
|
44
|
+
records.each { |record| owner.class.connection.delete(interpolate(sql, record)) }
|
47
45
|
else
|
48
|
-
relation
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
attrs[relation[name]] = @owner.id
|
57
|
-
when @reflection.association_foreign_key.to_s
|
58
|
-
attrs[relation[name]] = record.id
|
59
|
-
when *timestamps
|
60
|
-
attrs[relation[name]] = timezone
|
61
|
-
else
|
62
|
-
if record.has_attribute?(name)
|
63
|
-
value = @owner.send(:quote_value, record[name], column)
|
64
|
-
attrs[relation[name]] = value unless value.nil?
|
65
|
-
end
|
66
|
-
end
|
67
|
-
attrs
|
46
|
+
relation = join_table
|
47
|
+
condition = relation[reflection.foreign_key].eq(owner.id)
|
48
|
+
|
49
|
+
unless records == :all
|
50
|
+
condition = condition.and(
|
51
|
+
relation[reflection.association_foreign_key]
|
52
|
+
.in(records.map { |x| x.id }.compact)
|
53
|
+
)
|
68
54
|
end
|
69
55
|
|
70
|
-
relation.
|
71
|
-
end
|
72
|
-
|
73
|
-
return true
|
74
|
-
end
|
75
|
-
|
76
|
-
def delete_records(records)
|
77
|
-
if sql = @reflection.options[:delete_sql]
|
78
|
-
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
|
79
|
-
else
|
80
|
-
relation = Arel::Table.new(@reflection.options[:join_table])
|
81
|
-
relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
|
82
|
-
and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }))
|
83
|
-
).delete
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def construct_sql
|
88
|
-
if @reflection.options[:finder_sql]
|
89
|
-
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
90
|
-
else
|
91
|
-
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
|
92
|
-
@finder_sql << " AND (#{conditions})" if conditions
|
93
|
-
end
|
94
|
-
|
95
|
-
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
|
96
|
-
|
97
|
-
construct_counter_sql
|
98
|
-
end
|
99
|
-
|
100
|
-
def construct_scope
|
101
|
-
{ :find => { :conditions => @finder_sql,
|
102
|
-
:joins => @join_sql,
|
103
|
-
:readonly => false,
|
104
|
-
:order => @reflection.options[:order],
|
105
|
-
:include => @reflection.options[:include],
|
106
|
-
:limit => @reflection.options[:limit] } }
|
107
|
-
end
|
108
|
-
|
109
|
-
# Join tables with additional columns on top of the two foreign keys must be considered
|
110
|
-
# ambiguous unless a select clause has been explicitly defined. Otherwise you can get
|
111
|
-
# broken records back, if, for example, the join column also has an id column. This will
|
112
|
-
# then overwrite the id column of the records coming back.
|
113
|
-
def finding_with_ambiguous_select?(select_clause)
|
114
|
-
!select_clause && columns.size != 2
|
115
|
-
end
|
116
|
-
|
117
|
-
private
|
118
|
-
def create_record(attributes, &block)
|
119
|
-
# Can't use Base.create because the foreign key may be a protected attribute.
|
120
|
-
ensure_owner_is_not_new
|
121
|
-
if attributes.is_a?(Array)
|
122
|
-
attributes.collect { |attr| create(attr) }
|
123
|
-
else
|
124
|
-
build_record(attributes, &block)
|
56
|
+
owner.class.connection.delete(relation.where(condition).compile_delete)
|
125
57
|
end
|
126
58
|
end
|
127
59
|
|
128
|
-
def
|
129
|
-
|
130
|
-
record.send(:all_timestamp_attributes).map { |x| x.to_s }
|
131
|
-
else
|
132
|
-
[]
|
133
|
-
end
|
60
|
+
def invertible_for?(record)
|
61
|
+
false
|
134
62
|
end
|
135
63
|
end
|
136
64
|
end
|
@@ -5,25 +5,48 @@ module ActiveRecord
|
|
5
5
|
#
|
6
6
|
# If the association has a <tt>:through</tt> option further specialization
|
7
7
|
# is provided by its child HasManyThroughAssociation.
|
8
|
-
class HasManyAssociation <
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
class HasManyAssociation < CollectionAssociation #:nodoc:
|
9
|
+
|
10
|
+
def handle_dependency
|
11
|
+
case options[:dependent]
|
12
|
+
when :restrict, :restrict_with_exception
|
13
|
+
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
|
14
|
+
|
15
|
+
when :restrict_with_error
|
16
|
+
unless empty?
|
17
|
+
record = klass.human_attribute_name(reflection.name).downcase
|
18
|
+
owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
else
|
23
|
+
if options[:dependent] == :destroy
|
24
|
+
# No point in executing the counter update since we're going to destroy the parent anyway
|
25
|
+
load_target.each { |t| t.destroyed_by_association = reflection }
|
26
|
+
destroy_all
|
17
27
|
else
|
18
|
-
|
28
|
+
delete_all
|
19
29
|
end
|
20
30
|
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def insert_record(record, validate = true, raise = false)
|
34
|
+
set_owner_attributes(record)
|
35
|
+
|
36
|
+
if raise
|
37
|
+
record.save!(:validate => validate)
|
38
|
+
else
|
39
|
+
record.save(:validate => validate)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
21
44
|
|
22
45
|
# Returns the number of records in this collection.
|
23
46
|
#
|
24
47
|
# If the association has a counter cache it gets that value. Otherwise
|
25
48
|
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
26
|
-
# there's one.
|
49
|
+
# there's one. Some configuration options like :group make it impossible
|
27
50
|
# to do an SQL count, in those cases the array count will be used.
|
28
51
|
#
|
29
52
|
# That does not depend on whether the collection has already been loaded
|
@@ -34,94 +57,78 @@ module ActiveRecord
|
|
34
57
|
# the loaded flag is set to true as well.
|
35
58
|
def count_records
|
36
59
|
count = if has_cached_counter?
|
37
|
-
|
38
|
-
elsif
|
39
|
-
|
60
|
+
owner.send(:read_attribute, cached_counter_attribute_name)
|
61
|
+
elsif options[:counter_sql] || options[:finder_sql]
|
62
|
+
reflection.klass.count_by_sql(custom_counter_sql)
|
40
63
|
else
|
41
|
-
|
64
|
+
scope.count
|
42
65
|
end
|
43
66
|
|
44
67
|
# If there's nothing in the database and @target has no new records
|
45
68
|
# we are certain the current target is an empty array. This is a
|
46
69
|
# documented side-effect of the method that may avoid an extra SELECT.
|
47
|
-
@target ||= [] and loaded if count == 0
|
48
|
-
|
49
|
-
if @reflection.options[:limit]
|
50
|
-
count = [ @reflection.options[:limit], count ].min
|
51
|
-
end
|
70
|
+
@target ||= [] and loaded! if count == 0
|
52
71
|
|
53
|
-
|
72
|
+
[association_scope.limit_value, count].compact.min
|
54
73
|
end
|
55
74
|
|
56
|
-
def has_cached_counter?
|
57
|
-
|
75
|
+
def has_cached_counter?(reflection = reflection)
|
76
|
+
owner.attribute_present?(cached_counter_attribute_name(reflection))
|
58
77
|
end
|
59
78
|
|
60
|
-
def cached_counter_attribute_name
|
61
|
-
"#{
|
79
|
+
def cached_counter_attribute_name(reflection = reflection)
|
80
|
+
options[:counter_cache] || "#{reflection.name}_count"
|
62
81
|
end
|
63
82
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
def delete_records(records)
|
71
|
-
case @reflection.options[:dependent]
|
72
|
-
when :destroy
|
73
|
-
records.each { |r| r.destroy }
|
74
|
-
when :delete_all
|
75
|
-
@reflection.klass.delete(records.map { |record| record.id })
|
76
|
-
else
|
77
|
-
relation = Arel::Table.new(@reflection.table_name)
|
78
|
-
relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
|
79
|
-
and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id }))
|
80
|
-
).update(relation[@reflection.primary_key_name] => nil)
|
81
|
-
|
82
|
-
@owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
|
83
|
+
def update_counter(difference, reflection = reflection)
|
84
|
+
if has_cached_counter?(reflection)
|
85
|
+
counter = cached_counter_attribute_name(reflection)
|
86
|
+
owner.class.update_counters(owner.id, counter => difference)
|
87
|
+
owner[counter] += difference
|
88
|
+
owner.changed_attributes.delete(counter) # eww
|
83
89
|
end
|
84
90
|
end
|
85
91
|
|
86
|
-
|
87
|
-
|
92
|
+
# This shit is nasty. We need to avoid the following situation:
|
93
|
+
#
|
94
|
+
# * An associated record is deleted via record.destroy
|
95
|
+
# * Hence the callbacks run, and they find a belongs_to on the record with a
|
96
|
+
# :counter_cache options which points back at our owner. So they update the
|
97
|
+
# counter cache.
|
98
|
+
# * In which case, we must make sure to *not* update the counter cache, or else
|
99
|
+
# it will be decremented twice.
|
100
|
+
#
|
101
|
+
# Hence this method.
|
102
|
+
def inverse_updates_counter_cache?(reflection = reflection)
|
103
|
+
counter_name = cached_counter_attribute_name(reflection)
|
104
|
+
reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
|
105
|
+
inverse_reflection.counter_cache_column == counter_name
|
106
|
+
}
|
88
107
|
end
|
89
108
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
109
|
+
# Deletes the records according to the <tt>:dependent</tt> option.
|
110
|
+
def delete_records(records, method)
|
111
|
+
if method == :destroy
|
112
|
+
records.each { |r| r.destroy }
|
113
|
+
update_counter(-records.length) unless inverse_updates_counter_cache?
|
114
|
+
else
|
115
|
+
if records == :all
|
116
|
+
scope = self.scope
|
117
|
+
else
|
118
|
+
keys = records.map { |r| r[reflection.association_primary_key] }
|
119
|
+
scope = self.scope.where(reflection.association_primary_key => keys)
|
120
|
+
end
|
100
121
|
|
122
|
+
if method == :delete_all
|
123
|
+
update_counter(-scope.delete_all)
|
101
124
|
else
|
102
|
-
|
103
|
-
|
125
|
+
update_counter(-scope.update_all(reflection.foreign_key => nil))
|
126
|
+
end
|
104
127
|
end
|
105
|
-
|
106
|
-
construct_counter_sql
|
107
|
-
end
|
108
|
-
|
109
|
-
def construct_scope
|
110
|
-
create_scoping = {}
|
111
|
-
set_belongs_to_association_for(create_scoping)
|
112
|
-
{
|
113
|
-
:find => { :conditions => @finder_sql,
|
114
|
-
:readonly => false,
|
115
|
-
:order => @reflection.options[:order],
|
116
|
-
:limit => @reflection.options[:limit],
|
117
|
-
:include => @reflection.options[:include]},
|
118
|
-
:create => create_scoping
|
119
|
-
}
|
120
128
|
end
|
121
129
|
|
122
|
-
def
|
123
|
-
|
124
|
-
return !inverse.nil?
|
130
|
+
def foreign_key_present?
|
131
|
+
owner.attribute_present?(reflection.association_primary_key)
|
125
132
|
end
|
126
133
|
end
|
127
134
|
end
|
@@ -1,27 +1,15 @@
|
|
1
|
-
require "active_record/associations/through_association_scope"
|
2
|
-
require 'active_support/core_ext/object/blank'
|
3
1
|
|
4
2
|
module ActiveRecord
|
5
3
|
# = Active Record Has Many Through Association
|
6
4
|
module Associations
|
7
5
|
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
8
|
-
include
|
6
|
+
include ThroughAssociation
|
9
7
|
|
10
|
-
|
8
|
+
def initialize(owner, reflection)
|
9
|
+
super
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def create(attrs = nil)
|
17
|
-
create_record(attrs, false)
|
18
|
-
end
|
19
|
-
|
20
|
-
def destroy(*records)
|
21
|
-
transaction do
|
22
|
-
delete_records(flatten_deeper(records))
|
23
|
-
super
|
24
|
-
end
|
11
|
+
@through_records = {}
|
12
|
+
@through_association = nil
|
25
13
|
end
|
26
14
|
|
27
15
|
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
|
@@ -29,86 +17,179 @@ module ActiveRecord
|
|
29
17
|
# have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
|
30
18
|
# SELECT query if you use #length.
|
31
19
|
def size
|
32
|
-
|
33
|
-
|
34
|
-
|
20
|
+
if has_cached_counter?
|
21
|
+
owner.send(:read_attribute, cached_counter_attribute_name)
|
22
|
+
elsif loaded?
|
23
|
+
target.size
|
24
|
+
else
|
25
|
+
count
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def concat(*records)
|
30
|
+
unless owner.new_record?
|
31
|
+
records.flatten.each do |record|
|
32
|
+
raise_on_type_mismatch!(record)
|
33
|
+
record.save! if record.new_record?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
super
|
35
38
|
end
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
ensure_owner_is_not_new
|
40
|
+
def concat_records(records)
|
41
|
+
ensure_not_nested
|
40
42
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
records = super
|
44
|
+
|
45
|
+
if owner.new_record? && records
|
46
|
+
records.flatten.each do |record|
|
47
|
+
build_through_record(record)
|
45
48
|
end
|
46
49
|
end
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
+
records
|
52
|
+
end
|
53
|
+
|
54
|
+
def insert_record(record, validate = true, raise = false)
|
55
|
+
ensure_not_nested
|
56
|
+
|
57
|
+
if record.new_record?
|
58
|
+
if raise
|
59
|
+
record.save!(:validate => validate)
|
51
60
|
else
|
52
|
-
|
61
|
+
return unless record.save(:validate => validate)
|
53
62
|
end
|
54
63
|
end
|
55
64
|
|
56
|
-
|
57
|
-
|
58
|
-
|
65
|
+
save_through_record(record)
|
66
|
+
update_counter(1)
|
67
|
+
record
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def through_association
|
73
|
+
@through_association ||= owner.association(through_reflection.name)
|
59
74
|
end
|
60
75
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
76
|
+
# We temporarily cache through record that has been build, because if we build a
|
77
|
+
# through record in build_record and then subsequently call insert_record, then we
|
78
|
+
# want to use the exact same object.
|
79
|
+
#
|
80
|
+
# However, after insert_record has been called, we clear the cache entry because
|
81
|
+
# we want it to be possible to have multiple instances of the same record in an
|
82
|
+
# association
|
83
|
+
def build_through_record(record)
|
84
|
+
@through_records[record.object_id] ||= begin
|
85
|
+
ensure_mutable
|
86
|
+
|
87
|
+
through_record = through_association.build
|
88
|
+
through_record.send("#{source_reflection.name}=", record)
|
89
|
+
through_record
|
68
90
|
end
|
91
|
+
end
|
69
92
|
|
70
|
-
|
71
|
-
|
72
|
-
|
93
|
+
def save_through_record(record)
|
94
|
+
build_through_record(record).save!
|
95
|
+
ensure
|
96
|
+
@through_records.delete(record.object_id)
|
73
97
|
end
|
74
98
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
99
|
+
def build_record(attributes)
|
100
|
+
ensure_not_nested
|
101
|
+
|
102
|
+
record = super(attributes)
|
103
|
+
|
104
|
+
inverse = source_reflection.inverse_of
|
105
|
+
if inverse
|
106
|
+
if inverse.macro == :has_many
|
107
|
+
record.send(inverse.name) << build_through_record(record)
|
108
|
+
elsif inverse.macro == :has_one
|
109
|
+
record.send("#{inverse.name}=", build_through_record(record))
|
110
|
+
end
|
80
111
|
end
|
112
|
+
|
113
|
+
record
|
81
114
|
end
|
82
115
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
116
|
+
def target_reflection_has_associated_record?
|
117
|
+
!(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
|
118
|
+
end
|
119
|
+
|
120
|
+
def update_through_counter?(method)
|
121
|
+
case method
|
122
|
+
when :destroy
|
123
|
+
!inverse_updates_counter_cache?(through_reflection)
|
124
|
+
when :nullify
|
125
|
+
false
|
126
|
+
else
|
127
|
+
true
|
128
|
+
end
|
86
129
|
end
|
87
130
|
|
88
|
-
def
|
89
|
-
|
90
|
-
when @reflection.options[:finder_sql]
|
91
|
-
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
131
|
+
def delete_records(records, method)
|
132
|
+
ensure_not_nested
|
92
133
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
134
|
+
# This is unoptimised; it will load all the target records
|
135
|
+
# even when we just want to delete everything.
|
136
|
+
records = load_target if records == :all
|
137
|
+
|
138
|
+
scope = through_association.scope
|
139
|
+
scope.where! construct_join_attributes(*records)
|
140
|
+
|
141
|
+
case method
|
142
|
+
when :destroy
|
143
|
+
count = scope.destroy_all.length
|
144
|
+
when :nullify
|
145
|
+
count = scope.update_all(source_reflection.foreign_key => nil)
|
146
|
+
else
|
147
|
+
count = scope.delete_all
|
97
148
|
end
|
98
149
|
|
99
|
-
|
150
|
+
delete_through_records(records)
|
151
|
+
|
152
|
+
if source_reflection.options[:counter_cache]
|
153
|
+
counter = source_reflection.counter_cache_column
|
154
|
+
klass.decrement_counter counter, records.map(&:id)
|
155
|
+
end
|
156
|
+
|
157
|
+
if through_reflection.macro == :has_many && update_through_counter?(method)
|
158
|
+
update_counter(-count, through_reflection)
|
159
|
+
end
|
160
|
+
|
161
|
+
update_counter(-count)
|
100
162
|
end
|
101
163
|
|
102
|
-
def
|
103
|
-
|
164
|
+
def through_records_for(record)
|
165
|
+
attributes = construct_join_attributes(record)
|
166
|
+
candidates = Array.wrap(through_association.target)
|
167
|
+
candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
|
104
168
|
end
|
105
169
|
|
106
|
-
def
|
107
|
-
|
170
|
+
def delete_through_records(records)
|
171
|
+
records.each do |record|
|
172
|
+
through_records = through_records_for(record)
|
173
|
+
|
174
|
+
if through_reflection.macro == :has_many
|
175
|
+
through_records.each { |r| through_association.target.delete(r) }
|
176
|
+
else
|
177
|
+
if through_records.include?(through_association.target)
|
178
|
+
through_association.target = nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
@through_records.delete(record.object_id)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def find_target
|
187
|
+
return [] unless target_reflection_has_associated_record?
|
188
|
+
scope.to_a
|
108
189
|
end
|
109
190
|
|
110
191
|
# NOTE - not sure that we can actually cope with inverses here
|
111
|
-
def
|
192
|
+
def invertible_for?(record)
|
112
193
|
false
|
113
194
|
end
|
114
195
|
end
|