square-activerecord 3.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +6140 -0
- data/README.rdoc +222 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +179 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record.rb +124 -0
- data/lib/active_record/aggregations.rb +277 -0
- data/lib/active_record/association_preload.rb +430 -0
- data/lib/active_record/associations.rb +2307 -0
- data/lib/active_record/associations/association_collection.rb +572 -0
- data/lib/active_record/associations/association_proxy.rb +299 -0
- data/lib/active_record/associations/belongs_to_association.rb +91 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +82 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
- data/lib/active_record/associations/has_many_association.rb +128 -0
- data/lib/active_record/associations/has_many_through_association.rb +115 -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/attribute_methods.rb +60 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +30 -0
- data/lib/active_record/attribute_methods/dirty.rb +95 -0
- data/lib/active_record/attribute_methods/primary_key.rb +56 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +145 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +64 -0
- data/lib/active_record/attribute_methods/write.rb +43 -0
- data/lib/active_record/autosave_association.rb +369 -0
- data/lib/active_record/base.rb +1904 -0
- data/lib/active_record/callbacks.rb +284 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +364 -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 +333 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +73 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +539 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +217 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +657 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1031 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +401 -0
- data/lib/active_record/counter_cache.rb +115 -0
- data/lib/active_record/dynamic_finder_match.rb +56 -0
- data/lib/active_record/dynamic_scope_match.rb +23 -0
- data/lib/active_record/errors.rb +172 -0
- data/lib/active_record/fixtures.rb +1006 -0
- 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 +419 -0
- data/lib/active_record/observer.rb +125 -0
- data/lib/active_record/persistence.rb +290 -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 +411 -0
- data/lib/active_record/relation.rb +394 -0
- data/lib/active_record/relation/batches.rb +89 -0
- data/lib/active_record/relation/calculations.rb +295 -0
- data/lib/active_record/relation/finder_methods.rb +363 -0
- data/lib/active_record/relation/predicate_builder.rb +48 -0
- data/lib/active_record/relation/query_methods.rb +303 -0
- data/lib/active_record/relation/spawn_methods.rb +132 -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 +359 -0
- data/lib/active_record/validations.rb +84 -0
- data/lib/active_record/validations/associated.rb +48 -0
- data/lib/active_record/validations/uniqueness.rb +190 -0
- data/lib/active_record/version.rb +10 -0
- data/lib/rails/generators/active_record.rb +19 -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 +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
- metadata +223 -0
@@ -0,0 +1,145 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module AttributeMethods
|
3
|
+
module Read
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
|
7
|
+
|
8
|
+
included do
|
9
|
+
attribute_method_suffix ""
|
10
|
+
|
11
|
+
cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
|
12
|
+
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
|
13
|
+
|
14
|
+
# Undefine id so it can be used as an attribute name
|
15
|
+
undef_method(:id) if method_defined?(:id)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
# +cache_attributes+ allows you to declare which converted attribute values should
|
20
|
+
# be cached. Usually caching only pays off for attributes with expensive conversion
|
21
|
+
# methods, like time related columns (e.g. +created_at+, +updated_at+).
|
22
|
+
def cache_attributes(*attribute_names)
|
23
|
+
attribute_names.each {|attr| cached_attributes << attr.to_s}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the attributes which are cached. By default time related columns
|
27
|
+
# with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
|
28
|
+
def cached_attributes
|
29
|
+
@cached_attributes ||=
|
30
|
+
columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns +true+ if the provided attribute is being cached.
|
34
|
+
def cache_attribute?(attr_name)
|
35
|
+
cached_attributes.include?(attr_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
def define_method_attribute(attr_name)
|
40
|
+
if self.serialized_attributes[attr_name]
|
41
|
+
define_read_method_for_serialized_attribute(attr_name)
|
42
|
+
else
|
43
|
+
define_read_method(attr_name, attr_name, columns_hash[attr_name])
|
44
|
+
end
|
45
|
+
|
46
|
+
if attr_name == primary_key && attr_name != "id"
|
47
|
+
define_read_method('id', attr_name, columns_hash[attr_name])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
# Define read method for serialized attribute.
|
53
|
+
def define_read_method_for_serialized_attribute(attr_name)
|
54
|
+
generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Define an attribute reader method. Cope with nil column.
|
58
|
+
# method_name is the same as attr_name except when a non-standard primary key is used,
|
59
|
+
# we still define #id as an accessor for the key
|
60
|
+
def define_read_method(method_name, attr_name, column)
|
61
|
+
cast_code = column.type_cast_code('v') if column
|
62
|
+
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
63
|
+
|
64
|
+
unless attr_name.to_s == self.primary_key.to_s
|
65
|
+
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
66
|
+
end
|
67
|
+
|
68
|
+
if cache_attribute?(attr_name)
|
69
|
+
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Where possible, generate the method by evalling a string, as this will result in
|
73
|
+
# faster accesses because it avoids the block eval and then string eval incurred
|
74
|
+
# by the second branch.
|
75
|
+
#
|
76
|
+
# The second, slower, branch is necessary to support instances where the database
|
77
|
+
# returns columns with extra stuff in (like 'my_column(omg)').
|
78
|
+
if method_name =~ /^[a-zA-Z_]\w*[!?=]?$/
|
79
|
+
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
|
80
|
+
def _#{method_name}
|
81
|
+
#{access_code}
|
82
|
+
end
|
83
|
+
|
84
|
+
alias #{method_name} _#{method_name}
|
85
|
+
STR
|
86
|
+
else
|
87
|
+
generated_attribute_methods.module_eval do
|
88
|
+
define_method("_#{method_name}") { eval(access_code) }
|
89
|
+
alias_method(method_name, "_#{method_name}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
96
|
+
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
97
|
+
def read_attribute(attr_name)
|
98
|
+
if respond_to? "_#{attr_name}"
|
99
|
+
send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s)
|
100
|
+
else
|
101
|
+
_read_attribute attr_name
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def _read_attribute(attr_name)
|
106
|
+
attr_name = attr_name.to_s
|
107
|
+
attr_name = self.class.primary_key if attr_name == 'id'
|
108
|
+
value = @attributes[attr_name]
|
109
|
+
unless value.nil?
|
110
|
+
if column = column_for_attribute(attr_name)
|
111
|
+
if unserializable_attribute?(attr_name, column)
|
112
|
+
unserialize_attribute(attr_name)
|
113
|
+
else
|
114
|
+
column.type_cast(value)
|
115
|
+
end
|
116
|
+
else
|
117
|
+
value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns true if the attribute is of a text column and marked for serialization.
|
123
|
+
def unserializable_attribute?(attr_name, column)
|
124
|
+
column.text? && self.class.serialized_attributes[attr_name]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns the unserialized object of the attribute.
|
128
|
+
def unserialize_attribute(attr_name)
|
129
|
+
unserialized_object = object_from_yaml(@attributes[attr_name])
|
130
|
+
|
131
|
+
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
|
132
|
+
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
|
133
|
+
else
|
134
|
+
raise SerializationTypeMismatch,
|
135
|
+
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def attribute(attribute_name)
|
141
|
+
read_attribute(attribute_name)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module AttributeMethods
|
3
|
+
module TimeZoneConversion
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
cattr_accessor :time_zone_aware_attributes, :instance_writer => false
|
8
|
+
self.time_zone_aware_attributes = false
|
9
|
+
|
10
|
+
class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
11
|
+
self.skip_time_zone_conversion_for_attributes = []
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
protected
|
16
|
+
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
17
|
+
# This enhanced read method automatically converts the UTC time stored in the database to the time
|
18
|
+
# zone stored in Time.zone.
|
19
|
+
def define_method_attribute(attr_name)
|
20
|
+
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
21
|
+
method_body, line = <<-EOV, __LINE__ + 1
|
22
|
+
def _#{attr_name}(reload = false)
|
23
|
+
cached = @attributes_cache['#{attr_name}']
|
24
|
+
return cached if cached && !reload
|
25
|
+
time = _read_attribute('#{attr_name}')
|
26
|
+
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
27
|
+
end
|
28
|
+
alias #{attr_name} _#{attr_name}
|
29
|
+
EOV
|
30
|
+
generated_attribute_methods.module_eval(method_body, __FILE__, line)
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
37
|
+
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
38
|
+
def define_method_attribute=(attr_name)
|
39
|
+
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
40
|
+
method_body, line = <<-EOV, __LINE__ + 1
|
41
|
+
def #{attr_name}=(original_time)
|
42
|
+
time = original_time
|
43
|
+
unless time.acts_like?(:time)
|
44
|
+
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
45
|
+
end
|
46
|
+
time = time.in_time_zone rescue nil if time
|
47
|
+
write_attribute(:#{attr_name}, original_time)
|
48
|
+
@attributes_cache["#{attr_name}"] = time
|
49
|
+
end
|
50
|
+
EOV
|
51
|
+
generated_attribute_methods.module_eval(method_body, __FILE__, line)
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def create_time_zone_conversion_attribute?(name, column)
|
59
|
+
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module AttributeMethods
|
3
|
+
module Write
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attribute_method_suffix "="
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
protected
|
12
|
+
def define_method_attribute=(attr_name)
|
13
|
+
if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/
|
14
|
+
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
|
15
|
+
else
|
16
|
+
generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
|
17
|
+
write_attribute(attr_name, new_value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings
|
24
|
+
# for fixnum and float columns are turned into +nil+.
|
25
|
+
def write_attribute(attr_name, value)
|
26
|
+
attr_name = attr_name.to_s
|
27
|
+
attr_name = self.class.primary_key if attr_name == 'id'
|
28
|
+
@attributes_cache.delete(attr_name)
|
29
|
+
if (column = column_for_attribute(attr_name)) && column.number?
|
30
|
+
@attributes[attr_name] = convert_number_column_value(value)
|
31
|
+
else
|
32
|
+
@attributes[attr_name] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
# Handle *= for method_missing.
|
38
|
+
def attribute=(attribute_name, value)
|
39
|
+
write_attribute(attribute_name, value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,369 @@
|
|
1
|
+
require 'active_support/core_ext/array/wrap'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
# = Active Record Autosave Association
|
5
|
+
#
|
6
|
+
# +AutosaveAssociation+ is a module that takes care of automatically saving
|
7
|
+
# associacted records when their parent is saved. In addition to saving, it
|
8
|
+
# also destroys any associated records that were marked for destruction.
|
9
|
+
# (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
|
10
|
+
#
|
11
|
+
# Saving of the parent, its associations, and the destruction of marked
|
12
|
+
# associations, all happen inside a transaction. This should never leave the
|
13
|
+
# database in an inconsistent state.
|
14
|
+
#
|
15
|
+
# If validations for any of the associations fail, their error messages will
|
16
|
+
# be applied to the parent.
|
17
|
+
#
|
18
|
+
# Note that it also means that associations marked for destruction won't
|
19
|
+
# be destroyed directly. They will however still be marked for destruction.
|
20
|
+
#
|
21
|
+
# Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
|
22
|
+
# When the <tt>:autosave</tt> option is not present new associations are saved.
|
23
|
+
#
|
24
|
+
# === One-to-one Example
|
25
|
+
#
|
26
|
+
# class Post
|
27
|
+
# has_one :author, :autosave => true
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# Saving changes to the parent and its associated model can now be performed
|
31
|
+
# automatically _and_ atomically:
|
32
|
+
#
|
33
|
+
# post = Post.find(1)
|
34
|
+
# post.title # => "The current global position of migrating ducks"
|
35
|
+
# post.author.name # => "alloy"
|
36
|
+
#
|
37
|
+
# post.title = "On the migration of ducks"
|
38
|
+
# post.author.name = "Eloy Duran"
|
39
|
+
#
|
40
|
+
# post.save
|
41
|
+
# post.reload
|
42
|
+
# post.title # => "On the migration of ducks"
|
43
|
+
# post.author.name # => "Eloy Duran"
|
44
|
+
#
|
45
|
+
# Destroying an associated model, as part of the parent's save action, is as
|
46
|
+
# simple as marking it for destruction:
|
47
|
+
#
|
48
|
+
# post.author.mark_for_destruction
|
49
|
+
# post.author.marked_for_destruction? # => true
|
50
|
+
#
|
51
|
+
# Note that the model is _not_ yet removed from the database:
|
52
|
+
#
|
53
|
+
# id = post.author.id
|
54
|
+
# Author.find_by_id(id).nil? # => false
|
55
|
+
#
|
56
|
+
# post.save
|
57
|
+
# post.reload.author # => nil
|
58
|
+
#
|
59
|
+
# Now it _is_ removed from the database:
|
60
|
+
#
|
61
|
+
# Author.find_by_id(id).nil? # => true
|
62
|
+
#
|
63
|
+
# === One-to-many Example
|
64
|
+
#
|
65
|
+
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
|
66
|
+
#
|
67
|
+
# class Post
|
68
|
+
# has_many :comments # :autosave option is no declared
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# post = Post.new(:title => 'ruby rocks')
|
72
|
+
# post.comments.build(:body => 'hello world')
|
73
|
+
# post.save # => saves both post and comment
|
74
|
+
#
|
75
|
+
# post = Post.create(:title => 'ruby rocks')
|
76
|
+
# post.comments.build(:body => 'hello world')
|
77
|
+
# post.save # => saves both post and comment
|
78
|
+
#
|
79
|
+
# post = Post.create(:title => 'ruby rocks')
|
80
|
+
# post.comments.create(:body => 'hello world')
|
81
|
+
# post.save # => saves both post and comment
|
82
|
+
#
|
83
|
+
# When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
|
84
|
+
#
|
85
|
+
# class Post
|
86
|
+
# has_many :comments, :autosave => true
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# post = Post.create(:title => 'ruby rocks')
|
90
|
+
# post.comments.create(:body => 'hello world')
|
91
|
+
# post.comments[0].body = 'hi everyone'
|
92
|
+
# post.save # => saves both post and comment, with 'hi everyone' as body
|
93
|
+
#
|
94
|
+
# Destroying one of the associated models as part of the parent's save action
|
95
|
+
# is as simple as marking it for destruction:
|
96
|
+
#
|
97
|
+
# post.comments.last.mark_for_destruction
|
98
|
+
# post.comments.last.marked_for_destruction? # => true
|
99
|
+
# post.comments.length # => 2
|
100
|
+
#
|
101
|
+
# Note that the model is _not_ yet removed from the database:
|
102
|
+
#
|
103
|
+
# id = post.comments.last.id
|
104
|
+
# Comment.find_by_id(id).nil? # => false
|
105
|
+
#
|
106
|
+
# post.save
|
107
|
+
# post.reload.comments.length # => 1
|
108
|
+
#
|
109
|
+
# Now it _is_ removed from the database:
|
110
|
+
#
|
111
|
+
# Comment.find_by_id(id).nil? # => true
|
112
|
+
#
|
113
|
+
# === Validation
|
114
|
+
#
|
115
|
+
# Children records are validated unless <tt>:validate</tt> is +false+.
|
116
|
+
module AutosaveAssociation
|
117
|
+
extend ActiveSupport::Concern
|
118
|
+
|
119
|
+
ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
|
120
|
+
|
121
|
+
included do
|
122
|
+
ASSOCIATION_TYPES.each do |type|
|
123
|
+
send("valid_keys_for_#{type}_association") << :autosave
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module ClassMethods
|
128
|
+
private
|
129
|
+
|
130
|
+
# def belongs_to(name, options = {})
|
131
|
+
# super
|
132
|
+
# add_autosave_association_callbacks(reflect_on_association(name))
|
133
|
+
# end
|
134
|
+
ASSOCIATION_TYPES.each do |type|
|
135
|
+
module_eval <<-CODE, __FILE__, __LINE__ + 1
|
136
|
+
def #{type}(name, options = {})
|
137
|
+
super
|
138
|
+
add_autosave_association_callbacks(reflect_on_association(name))
|
139
|
+
end
|
140
|
+
CODE
|
141
|
+
end
|
142
|
+
|
143
|
+
# Adds validation and save callbacks for the association as specified by
|
144
|
+
# the +reflection+.
|
145
|
+
#
|
146
|
+
# For performance reasons, we don't check whether to validate at runtime.
|
147
|
+
# However the validation and callback methods are lazy and those methods
|
148
|
+
# get created when they are invoked for the very first time. However,
|
149
|
+
# this can change, for instance, when using nested attributes, which is
|
150
|
+
# called _after_ the association has been defined. Since we don't want
|
151
|
+
# the callbacks to get defined multiple times, there are guards that
|
152
|
+
# check if the save or validation methods have already been defined
|
153
|
+
# before actually defining them.
|
154
|
+
def add_autosave_association_callbacks(reflection)
|
155
|
+
save_method = :"autosave_associated_records_for_#{reflection.name}"
|
156
|
+
validation_method = :"validate_associated_records_for_#{reflection.name}"
|
157
|
+
collection = reflection.collection?
|
158
|
+
|
159
|
+
unless method_defined?(save_method)
|
160
|
+
if collection
|
161
|
+
before_save :before_save_collection_association
|
162
|
+
|
163
|
+
define_method(save_method) { save_collection_association(reflection) }
|
164
|
+
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
165
|
+
after_create save_method
|
166
|
+
after_update save_method
|
167
|
+
else
|
168
|
+
if reflection.macro == :has_one
|
169
|
+
define_method(save_method) { save_has_one_association(reflection) }
|
170
|
+
after_save save_method
|
171
|
+
else
|
172
|
+
define_method(save_method) { save_belongs_to_association(reflection) }
|
173
|
+
before_save save_method
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
if reflection.validate? && !method_defined?(validation_method)
|
179
|
+
method = (collection ? :validate_collection_association : :validate_single_association)
|
180
|
+
define_method(validation_method) { send(method, reflection) }
|
181
|
+
validate validation_method
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
|
187
|
+
def reload(options = nil)
|
188
|
+
@marked_for_destruction = false
|
189
|
+
super
|
190
|
+
end
|
191
|
+
|
192
|
+
# Marks this record to be destroyed as part of the parents save transaction.
|
193
|
+
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
|
194
|
+
# when <tt>parent.save</tt> is called.
|
195
|
+
#
|
196
|
+
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
197
|
+
def mark_for_destruction
|
198
|
+
@marked_for_destruction = true
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns whether or not this record will be destroyed as part of the parents save transaction.
|
202
|
+
#
|
203
|
+
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
204
|
+
def marked_for_destruction?
|
205
|
+
@marked_for_destruction
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns whether or not this record has been changed in any way (including whether
|
209
|
+
# any of its nested autosave associations are likewise changed)
|
210
|
+
def changed_for_autosave?
|
211
|
+
new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
# Returns the record for an association collection that should be validated
|
217
|
+
# or saved. If +autosave+ is +false+ only new records will be returned,
|
218
|
+
# unless the parent is/was a new record itself.
|
219
|
+
def associated_records_to_validate_or_save(association, new_record, autosave)
|
220
|
+
if new_record
|
221
|
+
association
|
222
|
+
elsif autosave
|
223
|
+
association.target.find_all { |record| record.changed_for_autosave? }
|
224
|
+
else
|
225
|
+
association.target.find_all { |record| record.new_record? }
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# go through nested autosave associations that are loaded in memory (without loading
|
230
|
+
# any new ones), and return true if is changed for autosave
|
231
|
+
def nested_records_changed_for_autosave?
|
232
|
+
self.class.reflect_on_all_autosave_associations.any? do |reflection|
|
233
|
+
association = association_instance_get(reflection.name)
|
234
|
+
association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
239
|
+
# turned on for the association.
|
240
|
+
def validate_single_association(reflection)
|
241
|
+
if (association = association_instance_get(reflection.name)) && !association.target.nil?
|
242
|
+
association_valid?(reflection, association)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Validate the associated records if <tt>:validate</tt> or
|
247
|
+
# <tt>:autosave</tt> is turned on for the association specified by
|
248
|
+
# +reflection+.
|
249
|
+
def validate_collection_association(reflection)
|
250
|
+
if association = association_instance_get(reflection.name)
|
251
|
+
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
|
252
|
+
records.each { |record| association_valid?(reflection, record) }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns whether or not the association is valid and applies any errors to
|
258
|
+
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
|
259
|
+
# enabled records if they're marked_for_destruction? or destroyed.
|
260
|
+
def association_valid?(reflection, association)
|
261
|
+
return true if association.destroyed? || association.marked_for_destruction?
|
262
|
+
|
263
|
+
unless valid = association.valid?
|
264
|
+
if reflection.options[:autosave]
|
265
|
+
association.errors.each do |attribute, message|
|
266
|
+
attribute = "#{reflection.name}.#{attribute}"
|
267
|
+
errors[attribute] << message
|
268
|
+
errors[attribute].uniq!
|
269
|
+
end
|
270
|
+
else
|
271
|
+
errors.add(reflection.name)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
valid
|
275
|
+
end
|
276
|
+
|
277
|
+
# Is used as a before_save callback to check while saving a collection
|
278
|
+
# association whether or not the parent was a new record before saving.
|
279
|
+
def before_save_collection_association
|
280
|
+
@new_record_before_save = new_record?
|
281
|
+
true
|
282
|
+
end
|
283
|
+
|
284
|
+
# Saves any new associated records, or all loaded autosave associations if
|
285
|
+
# <tt>:autosave</tt> is enabled on the association.
|
286
|
+
#
|
287
|
+
# In addition, it destroys all children that were marked for destruction
|
288
|
+
# with mark_for_destruction.
|
289
|
+
#
|
290
|
+
# This all happens inside a transaction, _if_ the Transactions module is included into
|
291
|
+
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
292
|
+
def save_collection_association(reflection)
|
293
|
+
if association = association_instance_get(reflection.name)
|
294
|
+
autosave = reflection.options[:autosave]
|
295
|
+
|
296
|
+
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
|
297
|
+
records.each do |record|
|
298
|
+
next if record.destroyed?
|
299
|
+
|
300
|
+
if autosave && record.marked_for_destruction?
|
301
|
+
association.destroy(record)
|
302
|
+
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
303
|
+
if autosave
|
304
|
+
saved = association.send(:insert_record, record, false, false)
|
305
|
+
else
|
306
|
+
association.send(:insert_record, record)
|
307
|
+
end
|
308
|
+
elsif autosave
|
309
|
+
saved = record.save(:validate => false)
|
310
|
+
end
|
311
|
+
|
312
|
+
raise ActiveRecord::Rollback if saved == false
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# reconstruct the SQL queries now that we know the owner's id
|
317
|
+
association.send(:construct_sql) if association.respond_to?(:construct_sql)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
|
322
|
+
# on the association.
|
323
|
+
#
|
324
|
+
# In addition, it will destroy the association if it was marked for
|
325
|
+
# destruction with mark_for_destruction.
|
326
|
+
#
|
327
|
+
# This all happens inside a transaction, _if_ the Transactions module is included into
|
328
|
+
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
329
|
+
def save_has_one_association(reflection)
|
330
|
+
if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
|
331
|
+
autosave = reflection.options[:autosave]
|
332
|
+
|
333
|
+
if autosave && association.marked_for_destruction?
|
334
|
+
association.destroy
|
335
|
+
else
|
336
|
+
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
337
|
+
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
338
|
+
association[reflection.primary_key_name] = key
|
339
|
+
saved = association.save(:validate => !autosave)
|
340
|
+
raise ActiveRecord::Rollback if !saved && autosave
|
341
|
+
saved
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
|
348
|
+
#
|
349
|
+
# In addition, it will destroy the association if it was marked for destruction.
|
350
|
+
def save_belongs_to_association(reflection)
|
351
|
+
if (association = association_instance_get(reflection.name)) && !association.destroyed?
|
352
|
+
autosave = reflection.options[:autosave]
|
353
|
+
|
354
|
+
if autosave && association.marked_for_destruction?
|
355
|
+
association.destroy
|
356
|
+
elsif autosave != false
|
357
|
+
saved = association.save(:validate => !autosave) if association.new_record? || autosave
|
358
|
+
|
359
|
+
if association.updated?
|
360
|
+
association_id = association.send(reflection.options[:primary_key] || :id)
|
361
|
+
self[reflection.primary_key_name] = association_id
|
362
|
+
end
|
363
|
+
|
364
|
+
saved if autosave
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|