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,132 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module Associations
|
|
3
|
+
# Association proxies in Active Record are middlemen between the object that
|
|
4
|
+
# holds the association, known as the <tt>@owner</tt>, and the actual associated
|
|
5
|
+
# object, known as the <tt>@target</tt>. The kind of association any proxy is
|
|
6
|
+
# about is available in <tt>@reflection</tt>. That's an instance of the class
|
|
7
|
+
# ActiveRecord::Reflection::AssociationReflection.
|
|
8
|
+
#
|
|
9
|
+
# For example, given
|
|
10
|
+
#
|
|
11
|
+
# class Blog < ActiveRecord::Base
|
|
12
|
+
# has_many :posts
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# blog = Blog.first
|
|
16
|
+
#
|
|
17
|
+
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
|
|
18
|
+
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
|
|
19
|
+
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
|
|
20
|
+
#
|
|
21
|
+
# This class has most of the basic instance methods removed, and delegates
|
|
22
|
+
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
|
|
23
|
+
# corner case, it even removes the +class+ method and that's why you get
|
|
24
|
+
#
|
|
25
|
+
# blog.posts.class # => Array
|
|
26
|
+
#
|
|
27
|
+
# though the object behind <tt>blog.posts</tt> is not an Array, but an
|
|
28
|
+
# ActiveRecord::Associations::HasManyAssociation.
|
|
29
|
+
#
|
|
30
|
+
# The <tt>@target</tt> object is not \loaded until needed. For example,
|
|
31
|
+
#
|
|
32
|
+
# blog.posts.count
|
|
33
|
+
#
|
|
34
|
+
# is computed directly through SQL and does not trigger by itself the
|
|
35
|
+
# instantiation of the actual post records.
|
|
36
|
+
class CollectionProxy # :nodoc:
|
|
37
|
+
alias :proxy_extend :extend
|
|
38
|
+
|
|
39
|
+
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
|
|
40
|
+
|
|
41
|
+
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
|
|
42
|
+
:lock, :readonly, :having, :pluck, :to => :scoped
|
|
43
|
+
|
|
44
|
+
delegate :target, :load_target, :loaded?, :to => :@association
|
|
45
|
+
|
|
46
|
+
delegate :select, :find, :first, :last,
|
|
47
|
+
:build, :create, :create!,
|
|
48
|
+
:concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
|
|
49
|
+
:sum, :count, :size, :length, :empty?,
|
|
50
|
+
:any?, :many?, :include?,
|
|
51
|
+
:to => :@association
|
|
52
|
+
|
|
53
|
+
def initialize(association)
|
|
54
|
+
@association = association
|
|
55
|
+
Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
alias_method :new, :build
|
|
59
|
+
|
|
60
|
+
def proxy_association
|
|
61
|
+
@association
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def scoped
|
|
65
|
+
association = @association
|
|
66
|
+
association.scoped.extending do
|
|
67
|
+
define_method(:proxy_association) { association }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def respond_to?(name, include_private = false)
|
|
72
|
+
super ||
|
|
73
|
+
(load_target && target.respond_to?(name, include_private)) ||
|
|
74
|
+
proxy_association.klass.respond_to?(name, include_private)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def method_missing(method, *args, &block)
|
|
78
|
+
match = DynamicFinderMatch.match(method)
|
|
79
|
+
if match && match.instantiator?
|
|
80
|
+
send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
|
|
81
|
+
proxy_association.send :set_owner_attributes, r
|
|
82
|
+
proxy_association.send :add_to_target, r
|
|
83
|
+
yield(r) if block_given?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
|
|
88
|
+
if load_target
|
|
89
|
+
if target.respond_to?(method)
|
|
90
|
+
target.send(method, *args, &block)
|
|
91
|
+
else
|
|
92
|
+
begin
|
|
93
|
+
super
|
|
94
|
+
rescue NoMethodError => e
|
|
95
|
+
raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
else
|
|
101
|
+
scoped.readonly(nil).send(method, *args, &block)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Forwards <tt>===</tt> explicitly to the \target because the instance method
|
|
106
|
+
# removal above doesn't catch it. Loads the \target if needed.
|
|
107
|
+
def ===(other)
|
|
108
|
+
other === load_target
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_ary
|
|
112
|
+
load_target.dup
|
|
113
|
+
end
|
|
114
|
+
alias_method :to_a, :to_ary
|
|
115
|
+
|
|
116
|
+
def <<(*records)
|
|
117
|
+
proxy_association.concat(records) && self
|
|
118
|
+
end
|
|
119
|
+
alias_method :push, :<<
|
|
120
|
+
|
|
121
|
+
def clear
|
|
122
|
+
delete_all
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def reload
|
|
127
|
+
proxy_association.reload
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
# = Active Record Has And Belongs To Many Association
|
|
3
|
+
module Associations
|
|
4
|
+
class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
|
|
5
|
+
attr_reader :join_table
|
|
6
|
+
|
|
7
|
+
def initialize(owner, reflection)
|
|
8
|
+
@join_table = Arel::Table.new(reflection.options[:join_table])
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
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
|
|
20
|
+
|
|
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.connection.insert stmt
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
record
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# ActiveRecord::Relation#delete_all needs to support joins before we can use a
|
|
36
|
+
# SQL-only implementation.
|
|
37
|
+
alias delete_all_on_destroy delete_all
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def count_records
|
|
42
|
+
load_target.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_records(records, method)
|
|
46
|
+
if sql = options[:delete_sql]
|
|
47
|
+
records.each { |record| owner.connection.delete(interpolate(sql, record)) }
|
|
48
|
+
else
|
|
49
|
+
relation = join_table
|
|
50
|
+
stmt = relation.where(relation[reflection.foreign_key].eq(owner.id).
|
|
51
|
+
and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
|
|
52
|
+
).compile_delete
|
|
53
|
+
owner.connection.delete stmt
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def invertible_for?(record)
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
# = Active Record Has Many Association
|
|
3
|
+
module Associations
|
|
4
|
+
# This is the proxy that handles a has many association.
|
|
5
|
+
#
|
|
6
|
+
# If the association has a <tt>:through</tt> option further specialization
|
|
7
|
+
# is provided by its child HasManyThroughAssociation.
|
|
8
|
+
class HasManyAssociation < CollectionAssociation #:nodoc:
|
|
9
|
+
|
|
10
|
+
def insert_record(record, validate = true, raise = false)
|
|
11
|
+
set_owner_attributes(record)
|
|
12
|
+
|
|
13
|
+
if raise
|
|
14
|
+
record.save!(:validate => validate)
|
|
15
|
+
else
|
|
16
|
+
record.save(:validate => validate)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Returns the number of records in this collection.
|
|
23
|
+
#
|
|
24
|
+
# If the association has a counter cache it gets that value. Otherwise
|
|
25
|
+
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
|
26
|
+
# there's one. Some configuration options like :group make it impossible
|
|
27
|
+
# to do an SQL count, in those cases the array count will be used.
|
|
28
|
+
#
|
|
29
|
+
# That does not depend on whether the collection has already been loaded
|
|
30
|
+
# or not. The +size+ method is the one that takes the loaded flag into
|
|
31
|
+
# account and delegates to +count_records+ if needed.
|
|
32
|
+
#
|
|
33
|
+
# If the collection is empty the target is set to an empty array and
|
|
34
|
+
# the loaded flag is set to true as well.
|
|
35
|
+
def count_records
|
|
36
|
+
count = if has_cached_counter?
|
|
37
|
+
owner.send(:read_attribute, cached_counter_attribute_name)
|
|
38
|
+
elsif options[:counter_sql] || options[:finder_sql]
|
|
39
|
+
reflection.klass.count_by_sql(custom_counter_sql)
|
|
40
|
+
else
|
|
41
|
+
scoped.count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# If there's nothing in the database and @target has no new records
|
|
45
|
+
# we are certain the current target is an empty array. This is a
|
|
46
|
+
# documented side-effect of the method that may avoid an extra SELECT.
|
|
47
|
+
@target ||= [] and loaded! if count == 0
|
|
48
|
+
|
|
49
|
+
[options[:limit], count].compact.min
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def has_cached_counter?(reflection = reflection)
|
|
53
|
+
owner.attribute_present?(cached_counter_attribute_name(reflection))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cached_counter_attribute_name(reflection = reflection)
|
|
57
|
+
"#{reflection.name}_count"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def update_counter(difference, reflection = reflection)
|
|
61
|
+
if has_cached_counter?(reflection)
|
|
62
|
+
counter = cached_counter_attribute_name(reflection)
|
|
63
|
+
owner.class.update_counters(owner.id, counter => difference)
|
|
64
|
+
owner[counter] += difference
|
|
65
|
+
owner.changed_attributes.delete(counter) # eww
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# This shit is nasty. We need to avoid the following situation:
|
|
70
|
+
#
|
|
71
|
+
# * An associated record is deleted via record.destroy
|
|
72
|
+
# * Hence the callbacks run, and they find a belongs_to on the record with a
|
|
73
|
+
# :counter_cache options which points back at our owner. So they update the
|
|
74
|
+
# counter cache.
|
|
75
|
+
# * In which case, we must make sure to *not* update the counter cache, or else
|
|
76
|
+
# it will be decremented twice.
|
|
77
|
+
#
|
|
78
|
+
# Hence this method.
|
|
79
|
+
def inverse_updates_counter_cache?(reflection = reflection)
|
|
80
|
+
counter_name = cached_counter_attribute_name(reflection)
|
|
81
|
+
reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
|
|
82
|
+
inverse_reflection.counter_cache_column == counter_name
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Deletes the records according to the <tt>:dependent</tt> option.
|
|
87
|
+
def delete_records(records, method)
|
|
88
|
+
if method == :destroy
|
|
89
|
+
records.each { |r| r.destroy }
|
|
90
|
+
update_counter(-records.length) unless inverse_updates_counter_cache?
|
|
91
|
+
else
|
|
92
|
+
keys = records.map { |r| r[reflection.association_primary_key] }
|
|
93
|
+
scope = scoped.where(reflection.association_primary_key => keys)
|
|
94
|
+
|
|
95
|
+
if method == :delete_all
|
|
96
|
+
update_counter(-scope.delete_all)
|
|
97
|
+
else
|
|
98
|
+
update_counter(-scope.update_all(reflection.foreign_key => nil))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def foreign_key_present?
|
|
104
|
+
owner.attribute_present?(reflection.association_primary_key)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
# = Active Record Has Many Through Association
|
|
5
|
+
module Associations
|
|
6
|
+
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
|
7
|
+
include ThroughAssociation
|
|
8
|
+
|
|
9
|
+
def initialize(owner, reflection)
|
|
10
|
+
super
|
|
11
|
+
|
|
12
|
+
@through_records = {}
|
|
13
|
+
@through_association = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
|
|
17
|
+
# loaded and calling collection.size if it has. If it's more likely than not that the collection does
|
|
18
|
+
# have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
|
|
19
|
+
# SELECT query if you use #length.
|
|
20
|
+
def size
|
|
21
|
+
if has_cached_counter?
|
|
22
|
+
owner.send(:read_attribute, cached_counter_attribute_name)
|
|
23
|
+
elsif loaded?
|
|
24
|
+
target.size
|
|
25
|
+
else
|
|
26
|
+
count
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def concat(*records)
|
|
31
|
+
unless owner.new_record?
|
|
32
|
+
records.flatten.each do |record|
|
|
33
|
+
raise_on_type_mismatch(record)
|
|
34
|
+
record.save! if record.new_record?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def insert_record(record, validate = true, raise = false)
|
|
42
|
+
ensure_not_nested
|
|
43
|
+
|
|
44
|
+
if record.new_record?
|
|
45
|
+
if raise
|
|
46
|
+
record.save!(:validate => validate)
|
|
47
|
+
else
|
|
48
|
+
return unless record.save(:validate => validate)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
save_through_record(record)
|
|
53
|
+
update_counter(1)
|
|
54
|
+
record
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ActiveRecord::Relation#delete_all needs to support joins before we can use a
|
|
58
|
+
# SQL-only implementation.
|
|
59
|
+
alias delete_all_on_destroy delete_all
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def through_association
|
|
64
|
+
@through_association ||= owner.association(through_reflection.name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# We temporarily cache through record that has been build, because if we build a
|
|
68
|
+
# through record in build_record and then subsequently call insert_record, then we
|
|
69
|
+
# want to use the exact same object.
|
|
70
|
+
#
|
|
71
|
+
# However, after insert_record has been called, we clear the cache entry because
|
|
72
|
+
# we want it to be possible to have multiple instances of the same record in an
|
|
73
|
+
# association
|
|
74
|
+
def build_through_record(record)
|
|
75
|
+
@through_records[record.object_id] ||= begin
|
|
76
|
+
through_record = through_association.build(construct_join_attributes(record))
|
|
77
|
+
through_record.send("#{source_reflection.name}=", record)
|
|
78
|
+
through_record
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def save_through_record(record)
|
|
83
|
+
build_through_record(record).save!
|
|
84
|
+
ensure
|
|
85
|
+
@through_records.delete(record.object_id)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_record(attributes, options = {})
|
|
89
|
+
ensure_not_nested
|
|
90
|
+
|
|
91
|
+
record = super(attributes, options)
|
|
92
|
+
|
|
93
|
+
inverse = source_reflection.inverse_of
|
|
94
|
+
if inverse
|
|
95
|
+
if inverse.macro == :has_many
|
|
96
|
+
record.send(inverse.name) << build_through_record(record)
|
|
97
|
+
elsif inverse.macro == :has_one
|
|
98
|
+
record.send("#{inverse.name}=", build_through_record(record))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
record
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def target_reflection_has_associated_record?
|
|
106
|
+
if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
|
|
107
|
+
false
|
|
108
|
+
else
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def update_through_counter?(method)
|
|
114
|
+
case method
|
|
115
|
+
when :destroy
|
|
116
|
+
!inverse_updates_counter_cache?(through_reflection)
|
|
117
|
+
when :nullify
|
|
118
|
+
false
|
|
119
|
+
else
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def delete_records(records, method)
|
|
125
|
+
ensure_not_nested
|
|
126
|
+
|
|
127
|
+
scope = through_association.scoped.where(construct_join_attributes(*records))
|
|
128
|
+
|
|
129
|
+
case method
|
|
130
|
+
when :destroy
|
|
131
|
+
count = scope.destroy_all.length
|
|
132
|
+
when :nullify
|
|
133
|
+
count = scope.update_all(source_reflection.foreign_key => nil)
|
|
134
|
+
else
|
|
135
|
+
count = scope.delete_all
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
delete_through_records(records)
|
|
139
|
+
|
|
140
|
+
if through_reflection.macro == :has_many && update_through_counter?(method)
|
|
141
|
+
update_counter(-count, through_reflection)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
update_counter(-count)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def through_records_for(record)
|
|
148
|
+
attributes = construct_join_attributes(record)
|
|
149
|
+
candidates = Array.wrap(through_association.target)
|
|
150
|
+
candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def delete_through_records(records)
|
|
154
|
+
records.each do |record|
|
|
155
|
+
through_records = through_records_for(record)
|
|
156
|
+
|
|
157
|
+
if through_reflection.macro == :has_many
|
|
158
|
+
through_records.each { |r| through_association.target.delete(r) }
|
|
159
|
+
else
|
|
160
|
+
if through_records.include?(through_association.target)
|
|
161
|
+
through_association.target = nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@through_records.delete(record.object_id)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def find_target
|
|
170
|
+
return [] unless target_reflection_has_associated_record?
|
|
171
|
+
scoped.all
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# NOTE - not sure that we can actually cope with inverses here
|
|
175
|
+
def invertible_for?(record)
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|