activerecord 1.0.0 → 3.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.
- data/CHANGELOG +5518 -76
- data/README.rdoc +222 -0
- data/examples/performance.rb +162 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record/aggregations.rb +192 -80
- data/lib/active_record/association_preload.rb +403 -0
- data/lib/active_record/associations/association_collection.rb +545 -53
- data/lib/active_record/associations/association_proxy.rb +295 -0
- data/lib/active_record/associations/belongs_to_association.rb +91 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
- data/lib/active_record/associations/has_many_association.rb +108 -84
- data/lib/active_record/associations/has_many_through_association.rb +116 -0
- data/lib/active_record/associations/has_one_association.rb +143 -0
- data/lib/active_record/associations/has_one_through_association.rb +40 -0
- data/lib/active_record/associations/through_association_scope.rb +154 -0
- data/lib/active_record/associations.rb +2086 -368
- data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
- data/lib/active_record/attribute_methods/dirty.rb +95 -0
- data/lib/active_record/attribute_methods/primary_key.rb +50 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +116 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
- data/lib/active_record/attribute_methods/write.rb +37 -0
- data/lib/active_record/attribute_methods.rb +60 -0
- data/lib/active_record/autosave_association.rb +369 -0
- data/lib/active_record/base.rb +1603 -721
- data/lib/active_record/callbacks.rb +176 -225
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
- data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
- data/lib/active_record/counter_cache.rb +115 -0
- data/lib/active_record/dynamic_finder_match.rb +53 -0
- data/lib/active_record/dynamic_scope_match.rb +32 -0
- data/lib/active_record/errors.rb +172 -0
- data/lib/active_record/fixtures.rb +941 -105
- data/lib/active_record/locale/en.yml +40 -0
- data/lib/active_record/locking/optimistic.rb +172 -0
- data/lib/active_record/locking/pessimistic.rb +55 -0
- data/lib/active_record/log_subscriber.rb +48 -0
- data/lib/active_record/migration.rb +617 -0
- data/lib/active_record/named_scope.rb +138 -0
- data/lib/active_record/nested_attributes.rb +417 -0
- data/lib/active_record/observer.rb +105 -36
- data/lib/active_record/persistence.rb +291 -0
- data/lib/active_record/query_cache.rb +36 -0
- data/lib/active_record/railtie.rb +91 -0
- data/lib/active_record/railties/controller_runtime.rb +38 -0
- data/lib/active_record/railties/databases.rake +512 -0
- data/lib/active_record/reflection.rb +364 -87
- data/lib/active_record/relation/batches.rb +89 -0
- data/lib/active_record/relation/calculations.rb +286 -0
- data/lib/active_record/relation/finder_methods.rb +355 -0
- data/lib/active_record/relation/predicate_builder.rb +41 -0
- data/lib/active_record/relation/query_methods.rb +261 -0
- data/lib/active_record/relation/spawn_methods.rb +112 -0
- data/lib/active_record/relation.rb +393 -0
- data/lib/active_record/schema.rb +59 -0
- data/lib/active_record/schema_dumper.rb +195 -0
- data/lib/active_record/serialization.rb +60 -0
- data/lib/active_record/serializers/xml_serializer.rb +244 -0
- data/lib/active_record/session_store.rb +340 -0
- data/lib/active_record/test_case.rb +67 -0
- data/lib/active_record/timestamp.rb +88 -0
- data/lib/active_record/transactions.rb +329 -75
- data/lib/active_record/validations/associated.rb +48 -0
- data/lib/active_record/validations/uniqueness.rb +185 -0
- data/lib/active_record/validations.rb +58 -179
- data/lib/active_record/version.rb +9 -0
- data/lib/active_record.rb +100 -24
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
- data/lib/rails/generators/active_record.rb +27 -0
- metadata +216 -158
- data/README +0 -361
- data/RUNNING_UNIT_TESTS +0 -36
- data/dev-utils/eval_debugger.rb +0 -9
- 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/deprecated_associations.rb +0 -70
- 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
@@ -1,104 +1,128 @@
|
|
1
1
|
module ActiveRecord
|
2
|
+
# = Active Record Has Many Association
|
2
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.
|
3
8
|
class HasManyAssociation < AssociationCollection #:nodoc:
|
4
|
-
def initialize(owner,
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
if options[:finder_sql]
|
9
|
-
@counter_sql = options[:finder_sql].gsub(/SELECT (.*) FROM/, "SELECT COUNT(*) FROM")
|
10
|
-
@finder_sql = options[:finder_sql]
|
11
|
-
else
|
12
|
-
@counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + @conditions : ""}"
|
13
|
-
@finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def <<(record)
|
18
|
-
raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
|
19
|
-
record.send(@association_class_primary_key_name + "=", @owner.id)
|
20
|
-
record.save(false)
|
21
|
-
@collection_array << record unless @collection_array.nil?
|
22
|
-
end
|
23
|
-
|
24
|
-
def delete(records)
|
25
|
-
duplicated_records_array(records).each do |record|
|
26
|
-
next if record.send(@association_class_primary_key_name) != @owner.id
|
27
|
-
record.send(@association_class_primary_key_name + "=", nil)
|
28
|
-
record.save(false)
|
29
|
-
@collection_array.delete(record) unless @collection_array.nil?
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def create(attributes = {})
|
34
|
-
# We can't use the regular Base.create method as the foreign key might be a protected attribute, hence the repetion
|
35
|
-
record = @association_class.new(attributes || {})
|
36
|
-
record.send(@association_class_primary_key_name + "=", @owner.id)
|
37
|
-
record.save
|
38
|
-
|
39
|
-
@collection_array << record unless @collection_array.nil?
|
40
|
-
|
41
|
-
return record
|
9
|
+
def initialize(owner, reflection)
|
10
|
+
@finder_sql = nil
|
11
|
+
super
|
42
12
|
end
|
43
|
-
|
44
|
-
def build(attributes = {})
|
45
|
-
association = @association_class.new
|
46
|
-
association.attributes = attributes.merge({ "#{@association_class_primary_key_name}" => @owner.id})
|
47
|
-
association
|
48
|
-
end
|
49
|
-
|
50
|
-
def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
|
51
|
-
if block_given? || @options[:finder_sql]
|
52
|
-
load_collection_to_array
|
53
|
-
@collection_array.send(:find_all, &block)
|
54
|
-
else
|
55
|
-
@association_class.find_all(
|
56
|
-
"#{@association_class_primary_key_name} = '#{@owner.id}' " +
|
57
|
-
"#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + runtime_conditions : ""}",
|
58
|
-
orderings,
|
59
|
-
limit,
|
60
|
-
joins
|
61
|
-
)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def find(association_id = nil, &block)
|
66
|
-
if block_given? || @options[:finder_sql]
|
67
|
-
load_collection_to_array
|
68
|
-
return @collection_array.send(:find, &block)
|
69
|
-
else
|
70
|
-
@association_class.find_on_conditions(
|
71
|
-
association_id, "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
|
72
|
-
)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
13
|
protected
|
77
|
-
def
|
78
|
-
if @options[:
|
79
|
-
@
|
14
|
+
def owner_quoted_id
|
15
|
+
if @reflection.options[:primary_key]
|
16
|
+
quote_value(@owner.send(@reflection.options[:primary_key]))
|
80
17
|
else
|
81
|
-
@
|
18
|
+
@owner.quoted_id
|
82
19
|
end
|
83
20
|
end
|
84
|
-
|
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.
|
85
35
|
def count_records
|
86
|
-
if has_cached_counter?
|
36
|
+
count = if has_cached_counter?
|
87
37
|
@owner.send(:read_attribute, cached_counter_attribute_name)
|
88
|
-
elsif @options[:
|
89
|
-
@
|
38
|
+
elsif @reflection.options[:counter_sql]
|
39
|
+
@reflection.klass.count_by_sql(@counter_sql)
|
90
40
|
else
|
91
|
-
@
|
41
|
+
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
|
92
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
|
+
if @reflection.options[:limit]
|
50
|
+
count = [ @reflection.options[:limit], count ].min
|
51
|
+
end
|
52
|
+
|
53
|
+
return count
|
93
54
|
end
|
94
|
-
|
55
|
+
|
95
56
|
def has_cached_counter?
|
96
57
|
@owner.attribute_present?(cached_counter_attribute_name)
|
97
58
|
end
|
98
|
-
|
59
|
+
|
99
60
|
def cached_counter_attribute_name
|
100
|
-
@
|
61
|
+
"#{@reflection.name}_count"
|
62
|
+
end
|
63
|
+
|
64
|
+
def insert_record(record, force = false, validate = true)
|
65
|
+
set_belongs_to_association_for(record)
|
66
|
+
force ? record.save! : record.save(:validate => validate)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Deletes the records according to the <tt>:dependent</tt> option.
|
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
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def target_obsolete?
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
def construct_sql
|
91
|
+
case
|
92
|
+
when @reflection.options[:finder_sql]
|
93
|
+
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
94
|
+
|
95
|
+
when @reflection.options[:as]
|
96
|
+
@finder_sql =
|
97
|
+
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
|
98
|
+
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
|
99
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
100
|
+
|
101
|
+
else
|
102
|
+
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
103
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
104
|
+
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
|
+
end
|
121
|
+
|
122
|
+
def we_can_set_the_inverse_on_this?(record)
|
123
|
+
inverse = @reflection.inverse_of
|
124
|
+
return !inverse.nil?
|
101
125
|
end
|
102
126
|
end
|
103
127
|
end
|
104
|
-
end
|
128
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "active_record/associations/through_association_scope"
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
# = Active Record Has Many Through Association
|
6
|
+
module Associations
|
7
|
+
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
8
|
+
include ThroughAssociationScope
|
9
|
+
|
10
|
+
alias_method :new, :build
|
11
|
+
|
12
|
+
def create!(attrs = nil)
|
13
|
+
create_record(attrs, true)
|
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
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
|
28
|
+
# loaded and calling collection.size if it has. If it's more likely than not that the collection does
|
29
|
+
# have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
|
30
|
+
# SELECT query if you use #length.
|
31
|
+
def size
|
32
|
+
return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
|
33
|
+
return @target.size if loaded?
|
34
|
+
return count
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
def create_record(attrs, force = true)
|
39
|
+
ensure_owner_is_not_new
|
40
|
+
|
41
|
+
transaction do
|
42
|
+
object = @reflection.klass.new(attrs)
|
43
|
+
add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
|
44
|
+
object
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_reflection_has_associated_record?
|
49
|
+
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
|
50
|
+
false
|
51
|
+
else
|
52
|
+
true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def construct_find_options!(options)
|
57
|
+
options[:joins] = construct_joins(options[:joins])
|
58
|
+
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
|
59
|
+
end
|
60
|
+
|
61
|
+
def insert_record(record, force = true, validate = true)
|
62
|
+
if record.new_record?
|
63
|
+
if force
|
64
|
+
record.save!
|
65
|
+
else
|
66
|
+
return false unless record.save(:validate => validate)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
through_association = @owner.send(@reflection.through_reflection.name)
|
71
|
+
through_record = through_association.create!(construct_join_attributes(record))
|
72
|
+
through_association.proxy_target << through_record
|
73
|
+
end
|
74
|
+
|
75
|
+
# TODO - add dependent option support
|
76
|
+
def delete_records(records)
|
77
|
+
klass = @reflection.through_reflection.klass
|
78
|
+
records.each do |associate|
|
79
|
+
klass.delete_all(construct_join_attributes(associate))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_target
|
84
|
+
return [] unless target_reflection_has_associated_record?
|
85
|
+
with_scope(construct_scope) { @reflection.klass.find(:all) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def construct_sql
|
89
|
+
case
|
90
|
+
when @reflection.options[:finder_sql]
|
91
|
+
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
92
|
+
|
93
|
+
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
94
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
95
|
+
else
|
96
|
+
@finder_sql = construct_conditions
|
97
|
+
end
|
98
|
+
|
99
|
+
construct_counter_sql
|
100
|
+
end
|
101
|
+
|
102
|
+
def has_cached_counter?
|
103
|
+
@owner.attribute_present?(cached_counter_attribute_name)
|
104
|
+
end
|
105
|
+
|
106
|
+
def cached_counter_attribute_name
|
107
|
+
"#{@reflection.name}_count"
|
108
|
+
end
|
109
|
+
|
110
|
+
# NOTE - not sure that we can actually cope with inverses here
|
111
|
+
def we_can_set_the_inverse_on_this?(record)
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
# = Active Record Belongs To Has One Association
|
3
|
+
module Associations
|
4
|
+
class HasOneAssociation < AssociationProxy #:nodoc:
|
5
|
+
def initialize(owner, reflection)
|
6
|
+
super
|
7
|
+
construct_sql
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(attrs = {}, replace_existing = true)
|
11
|
+
new_record(replace_existing) do |reflection|
|
12
|
+
attrs = merge_with_conditions(attrs)
|
13
|
+
reflection.create_association(attrs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def create!(attrs = {}, replace_existing = true)
|
18
|
+
new_record(replace_existing) do |reflection|
|
19
|
+
attrs = merge_with_conditions(attrs)
|
20
|
+
reflection.create_association!(attrs)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def build(attrs = {}, replace_existing = true)
|
25
|
+
new_record(replace_existing) do |reflection|
|
26
|
+
attrs = merge_with_conditions(attrs)
|
27
|
+
reflection.build_association(attrs)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def replace(obj, dont_save = false)
|
32
|
+
load_target
|
33
|
+
|
34
|
+
unless @target.nil? || @target == obj
|
35
|
+
if dependent? && !dont_save
|
36
|
+
case @reflection.options[:dependent]
|
37
|
+
when :delete
|
38
|
+
@target.delete unless @target.new_record?
|
39
|
+
@owner.clear_association_cache
|
40
|
+
when :destroy
|
41
|
+
@target.destroy unless @target.new_record?
|
42
|
+
@owner.clear_association_cache
|
43
|
+
when :nullify
|
44
|
+
@target[@reflection.primary_key_name] = nil
|
45
|
+
@target.save unless @owner.new_record? || @target.new_record?
|
46
|
+
end
|
47
|
+
else
|
48
|
+
@target[@reflection.primary_key_name] = nil
|
49
|
+
@target.save unless @owner.new_record? || @target.new_record?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if obj.nil?
|
54
|
+
@target = nil
|
55
|
+
else
|
56
|
+
raise_on_type_mismatch(obj)
|
57
|
+
set_belongs_to_association_for(obj)
|
58
|
+
@target = (AssociationProxy === obj ? obj.target : obj)
|
59
|
+
end
|
60
|
+
|
61
|
+
set_inverse_instance(obj, @owner)
|
62
|
+
@loaded = true
|
63
|
+
|
64
|
+
unless @owner.new_record? or obj.nil? or dont_save
|
65
|
+
return (obj.save ? self : false)
|
66
|
+
else
|
67
|
+
return (obj.nil? ? nil : self)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
def owner_quoted_id
|
73
|
+
if @reflection.options[:primary_key]
|
74
|
+
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
|
75
|
+
else
|
76
|
+
@owner.quoted_id
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def find_target
|
82
|
+
options = @reflection.options.dup
|
83
|
+
(options.keys - [:select, :order, :include, :readonly]).each do |key|
|
84
|
+
options.delete key
|
85
|
+
end
|
86
|
+
options[:conditions] = @finder_sql
|
87
|
+
|
88
|
+
the_target = @reflection.klass.find(:first, options)
|
89
|
+
set_inverse_instance(the_target, @owner)
|
90
|
+
the_target
|
91
|
+
end
|
92
|
+
|
93
|
+
def construct_sql
|
94
|
+
case
|
95
|
+
when @reflection.options[:as]
|
96
|
+
@finder_sql =
|
97
|
+
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
|
98
|
+
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
|
99
|
+
else
|
100
|
+
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
101
|
+
end
|
102
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
103
|
+
end
|
104
|
+
|
105
|
+
def construct_scope
|
106
|
+
create_scoping = {}
|
107
|
+
set_belongs_to_association_for(create_scoping)
|
108
|
+
{ :create => create_scoping }
|
109
|
+
end
|
110
|
+
|
111
|
+
def new_record(replace_existing)
|
112
|
+
# Make sure we load the target first, if we plan on replacing the existing
|
113
|
+
# instance. Otherwise, if the target has not previously been loaded
|
114
|
+
# elsewhere, the instance we create will get orphaned.
|
115
|
+
load_target if replace_existing
|
116
|
+
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
|
117
|
+
yield @reflection
|
118
|
+
end
|
119
|
+
|
120
|
+
if replace_existing
|
121
|
+
replace(record, true)
|
122
|
+
else
|
123
|
+
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
124
|
+
self.target = record
|
125
|
+
set_inverse_instance(record, @owner)
|
126
|
+
end
|
127
|
+
|
128
|
+
record
|
129
|
+
end
|
130
|
+
|
131
|
+
def we_can_set_the_inverse_on_this?(record)
|
132
|
+
inverse = @reflection.inverse_of
|
133
|
+
return !inverse.nil?
|
134
|
+
end
|
135
|
+
|
136
|
+
def merge_with_conditions(attrs={})
|
137
|
+
attrs ||= {}
|
138
|
+
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
139
|
+
attrs
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "active_record/associations/through_association_scope"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
# = Active Record Has One Through Association
|
5
|
+
module Associations
|
6
|
+
class HasOneThroughAssociation < HasOneAssociation
|
7
|
+
include ThroughAssociationScope
|
8
|
+
|
9
|
+
def replace(new_value)
|
10
|
+
create_through_record(new_value)
|
11
|
+
@target = new_value
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def create_through_record(new_value) #nodoc:
|
17
|
+
klass = @reflection.through_reflection.klass
|
18
|
+
|
19
|
+
current_object = @owner.send(@reflection.through_reflection.name)
|
20
|
+
|
21
|
+
if current_object
|
22
|
+
new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
|
23
|
+
elsif new_value
|
24
|
+
if @owner.new_record?
|
25
|
+
self.target = new_value
|
26
|
+
through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
|
27
|
+
through_association.build(construct_join_attributes(new_value))
|
28
|
+
else
|
29
|
+
@owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def find_target
|
36
|
+
with_scope(construct_scope) { @reflection.klass.find(:first) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
# = Active Record Through Association Scope
|
3
|
+
module Associations
|
4
|
+
module ThroughAssociationScope
|
5
|
+
|
6
|
+
protected
|
7
|
+
|
8
|
+
def construct_scope
|
9
|
+
{ :create => construct_owner_attributes(@reflection),
|
10
|
+
:find => { :conditions => construct_conditions,
|
11
|
+
:joins => construct_joins,
|
12
|
+
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
|
13
|
+
:select => construct_select,
|
14
|
+
:order => @reflection.options[:order],
|
15
|
+
:limit => @reflection.options[:limit],
|
16
|
+
:readonly => @reflection.options[:readonly],
|
17
|
+
} }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Build SQL conditions from attributes, qualified by table name.
|
21
|
+
def construct_conditions
|
22
|
+
table_name = @reflection.through_reflection.quoted_table_name
|
23
|
+
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
|
24
|
+
"#{table_name}.#{attr} = #{value}"
|
25
|
+
end
|
26
|
+
conditions << sql_conditions if sql_conditions
|
27
|
+
"(" + conditions.join(') AND (') + ")"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Associate attributes pointing to owner, quoted.
|
31
|
+
def construct_quoted_owner_attributes(reflection)
|
32
|
+
if as = reflection.options[:as]
|
33
|
+
{ "#{as}_id" => owner_quoted_id,
|
34
|
+
"#{as}_type" => reflection.klass.quote_value(
|
35
|
+
@owner.class.base_class.name.to_s,
|
36
|
+
reflection.klass.columns_hash["#{as}_type"]) }
|
37
|
+
elsif reflection.macro == :belongs_to
|
38
|
+
{ reflection.klass.primary_key => @owner.class.quote_value(@owner[reflection.primary_key_name]) }
|
39
|
+
else
|
40
|
+
{ reflection.primary_key_name => owner_quoted_id }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def construct_from
|
45
|
+
@reflection.table_name
|
46
|
+
end
|
47
|
+
|
48
|
+
def construct_select(custom_select = nil)
|
49
|
+
distinct = "DISTINCT " if @reflection.options[:uniq]
|
50
|
+
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
|
51
|
+
end
|
52
|
+
|
53
|
+
def construct_joins(custom_joins = nil)
|
54
|
+
polymorphic_join = nil
|
55
|
+
if @reflection.source_reflection.macro == :belongs_to
|
56
|
+
reflection_primary_key = @reflection.klass.primary_key
|
57
|
+
source_primary_key = @reflection.source_reflection.primary_key_name
|
58
|
+
if @reflection.options[:source_type]
|
59
|
+
polymorphic_join = "AND %s.%s = %s" % [
|
60
|
+
@reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
|
61
|
+
@owner.class.quote_value(@reflection.options[:source_type])
|
62
|
+
]
|
63
|
+
end
|
64
|
+
else
|
65
|
+
reflection_primary_key = @reflection.source_reflection.primary_key_name
|
66
|
+
source_primary_key = @reflection.through_reflection.klass.primary_key
|
67
|
+
if @reflection.source_reflection.options[:as]
|
68
|
+
polymorphic_join = "AND %s.%s = %s" % [
|
69
|
+
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
|
70
|
+
@owner.class.quote_value(@reflection.through_reflection.klass.name)
|
71
|
+
]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
"INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
|
76
|
+
@reflection.through_reflection.quoted_table_name,
|
77
|
+
@reflection.quoted_table_name, reflection_primary_key,
|
78
|
+
@reflection.through_reflection.quoted_table_name, source_primary_key,
|
79
|
+
polymorphic_join
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Construct attributes for associate pointing to owner.
|
84
|
+
def construct_owner_attributes(reflection)
|
85
|
+
if as = reflection.options[:as]
|
86
|
+
{ "#{as}_id" => @owner.id,
|
87
|
+
"#{as}_type" => @owner.class.base_class.name.to_s }
|
88
|
+
else
|
89
|
+
{ reflection.primary_key_name => @owner.id }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Construct attributes for :through pointing to owner and associate.
|
94
|
+
def construct_join_attributes(associate)
|
95
|
+
# TODO: revisit this to allow it for deletion, supposing dependent option is supported
|
96
|
+
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
|
97
|
+
|
98
|
+
join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
|
99
|
+
|
100
|
+
if @reflection.options[:source_type]
|
101
|
+
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
|
102
|
+
end
|
103
|
+
|
104
|
+
if @reflection.through_reflection.options[:conditions].is_a?(Hash)
|
105
|
+
join_attributes.merge!(@reflection.through_reflection.options[:conditions])
|
106
|
+
end
|
107
|
+
|
108
|
+
join_attributes
|
109
|
+
end
|
110
|
+
|
111
|
+
def conditions
|
112
|
+
@conditions = build_conditions unless defined?(@conditions)
|
113
|
+
@conditions
|
114
|
+
end
|
115
|
+
|
116
|
+
def build_conditions
|
117
|
+
association_conditions = @reflection.options[:conditions]
|
118
|
+
through_conditions = build_through_conditions
|
119
|
+
source_conditions = @reflection.source_reflection.options[:conditions]
|
120
|
+
uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
|
121
|
+
|
122
|
+
if association_conditions || through_conditions || source_conditions || uses_sti
|
123
|
+
all = []
|
124
|
+
|
125
|
+
[association_conditions, source_conditions].each do |conditions|
|
126
|
+
all << interpolate_sql(sanitize_sql(conditions)) if conditions
|
127
|
+
end
|
128
|
+
|
129
|
+
all << through_conditions if through_conditions
|
130
|
+
all << build_sti_condition if uses_sti
|
131
|
+
|
132
|
+
all.map { |sql| "(#{sql})" } * ' AND '
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def build_through_conditions
|
137
|
+
conditions = @reflection.through_reflection.options[:conditions]
|
138
|
+
if conditions.is_a?(Hash)
|
139
|
+
interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub(
|
140
|
+
@reflection.quoted_table_name,
|
141
|
+
@reflection.through_reflection.quoted_table_name)
|
142
|
+
elsif conditions
|
143
|
+
interpolate_sql(sanitize_sql(conditions))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def build_sti_condition
|
148
|
+
@reflection.through_reflection.klass.send(:type_condition).to_sql
|
149
|
+
end
|
150
|
+
|
151
|
+
alias_method :sql_conditions, :conditions
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|