activerecord 2.1.2 → 2.2.2
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 +32 -6
- data/README +0 -0
- data/Rakefile +4 -5
- data/lib/active_record.rb +11 -10
- data/lib/active_record/aggregations.rb +110 -38
- data/lib/active_record/association_preload.rb +104 -15
- data/lib/active_record/associations.rb +427 -212
- data/lib/active_record/associations/association_collection.rb +101 -16
- data/lib/active_record/associations/association_proxy.rb +65 -13
- data/lib/active_record/associations/belongs_to_association.rb +2 -2
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -3
- data/lib/active_record/associations/has_many_association.rb +28 -28
- data/lib/active_record/associations/has_many_through_association.rb +21 -19
- data/lib/active_record/associations/has_one_association.rb +24 -7
- data/lib/active_record/associations/has_one_through_association.rb +3 -4
- data/lib/active_record/attribute_methods.rb +13 -5
- data/lib/active_record/base.rb +435 -212
- data/lib/active_record/calculations.rb +12 -5
- data/lib/active_record/callbacks.rb +28 -9
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +355 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +42 -215
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -5
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +48 -7
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +10 -4
- data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -26
- data/lib/active_record/connection_adapters/mysql_adapter.rb +71 -45
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +155 -84
- data/lib/active_record/dirty.rb +25 -7
- data/lib/active_record/dynamic_finder_match.rb +41 -0
- data/lib/active_record/fixtures.rb +10 -9
- data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
- data/lib/active_record/locale/en.yml +54 -0
- data/lib/active_record/migration.rb +47 -10
- data/lib/active_record/named_scope.rb +29 -16
- data/lib/active_record/reflection.rb +118 -54
- data/lib/active_record/schema_dumper.rb +13 -7
- data/lib/active_record/test_case.rb +18 -5
- data/lib/active_record/transactions.rb +89 -34
- data/lib/active_record/validations.rb +270 -180
- data/lib/active_record/version.rb +1 -1
- data/test/cases/active_schema_test_mysql.rb +5 -0
- data/test/cases/adapter_test.rb +6 -0
- data/test/cases/aggregations_test.rb +39 -0
- data/test/cases/associations/belongs_to_associations_test.rb +10 -0
- data/test/cases/associations/eager_load_nested_include_test.rb +30 -12
- data/test/cases/associations/eager_test.rb +54 -5
- data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +77 -10
- data/test/cases/associations/has_many_associations_test.rb +74 -7
- data/test/cases/associations/has_many_through_associations_test.rb +50 -3
- data/test/cases/associations/has_one_associations_test.rb +17 -0
- data/test/cases/associations/has_one_through_associations_test.rb +49 -1
- data/test/cases/associations_test.rb +0 -0
- data/test/cases/attribute_methods_test.rb +59 -4
- data/test/cases/base_test.rb +93 -21
- data/test/cases/binary_test.rb +1 -5
- data/test/cases/calculations_test.rb +5 -0
- data/test/cases/callbacks_observers_test.rb +38 -0
- data/test/cases/connection_test_mysql.rb +1 -1
- data/test/cases/defaults_test.rb +32 -1
- data/test/cases/deprecated_finder_test.rb +0 -0
- data/test/cases/dirty_test.rb +13 -0
- data/test/cases/finder_test.rb +162 -12
- data/test/cases/fixtures_test.rb +32 -3
- data/test/cases/helper.rb +15 -0
- data/test/cases/i18n_test.rb +41 -0
- data/test/cases/inheritance_test.rb +2 -2
- data/test/cases/lifecycle_test.rb +0 -0
- data/test/cases/locking_test.rb +4 -9
- data/test/cases/method_scoping_test.rb +109 -2
- data/test/cases/migration_test.rb +43 -8
- data/test/cases/multiple_db_test.rb +25 -0
- data/test/cases/named_scope_test.rb +74 -0
- data/test/cases/pooled_connections_test.rb +103 -0
- data/test/cases/readonly_test.rb +0 -0
- data/test/cases/reflection_test.rb +11 -3
- data/test/cases/reload_models_test.rb +20 -0
- data/test/cases/sanitize_test.rb +25 -0
- data/test/cases/schema_authorization_test_postgresql.rb +2 -2
- data/test/cases/transactions_test.rb +62 -12
- data/test/cases/unconnected_test.rb +0 -0
- data/test/cases/validations_i18n_test.rb +921 -0
- data/test/cases/validations_test.rb +44 -33
- data/test/connections/native_mysql/connection.rb +1 -3
- data/test/fixtures/companies.yml +1 -0
- data/test/fixtures/customers.yml +10 -1
- data/test/fixtures/fixture_database.sqlite3 +0 -0
- data/test/fixtures/fixture_database_2.sqlite3 +0 -0
- data/test/fixtures/organizations.yml +5 -0
- data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
- data/test/models/author.rb +3 -0
- data/test/models/category.rb +3 -0
- data/test/models/club.rb +6 -0
- data/test/models/company.rb +25 -1
- data/test/models/customer.rb +19 -1
- data/test/models/member.rb +2 -0
- data/test/models/member_detail.rb +4 -0
- data/test/models/organization.rb +4 -0
- data/test/models/parrot.rb +1 -0
- data/test/models/post.rb +3 -0
- data/test/models/reply.rb +0 -0
- data/test/models/topic.rb +3 -0
- data/test/schema/schema.rb +12 -1
- metadata +22 -10
- data/lib/active_record/vendor/mysql.rb +0 -1214
- data/test/cases/adapter_test_sqlserver.rb +0 -95
- data/test/cases/table_name_test_sqlserver.rb +0 -23
- data/test/cases/threaded_connections_test.rb +0 -48
- data/test/schema/sqlserver_specific_schema.rb +0 -5
@@ -13,14 +13,15 @@ module ActiveRecord
|
|
13
13
|
def create_reflection(macro, name, options, active_record)
|
14
14
|
case macro
|
15
15
|
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
16
|
-
|
16
|
+
klass = options[:through] ? ThroughReflection : AssociationReflection
|
17
|
+
reflection = klass.new(macro, name, options, active_record)
|
17
18
|
when :composed_of
|
18
19
|
reflection = AggregateReflection.new(macro, name, options, active_record)
|
19
20
|
end
|
20
21
|
write_inheritable_hash :reflections, name => reflection
|
21
22
|
reflection
|
22
23
|
end
|
23
|
-
|
24
|
+
|
24
25
|
# Returns a hash containing all AssociationReflection objects for the current class
|
25
26
|
# Example:
|
26
27
|
#
|
@@ -30,7 +31,7 @@ module ActiveRecord
|
|
30
31
|
def reflections
|
31
32
|
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
|
32
33
|
end
|
33
|
-
|
34
|
+
|
34
35
|
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
35
36
|
def reflect_on_all_aggregations
|
36
37
|
reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
|
@@ -109,13 +110,18 @@ module ActiveRecord
|
|
109
110
|
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
|
110
111
|
# and +other_aggregation+ has an options hash assigned to it.
|
111
112
|
def ==(other_aggregation)
|
112
|
-
name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
113
|
+
other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
113
114
|
end
|
114
115
|
|
115
116
|
def sanitized_conditions #:nodoc:
|
116
117
|
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
117
118
|
end
|
118
119
|
|
120
|
+
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
121
|
+
def belongs_to?
|
122
|
+
macro == :belongs_to
|
123
|
+
end
|
124
|
+
|
119
125
|
private
|
120
126
|
def derive_class_name
|
121
127
|
name.to_s.camelize
|
@@ -129,10 +135,45 @@ module ActiveRecord
|
|
129
135
|
|
130
136
|
# Holds all the meta-data about an association as it was specified in the Active Record class.
|
131
137
|
class AssociationReflection < MacroReflection #:nodoc:
|
138
|
+
# Returns the target association's class:
|
139
|
+
#
|
140
|
+
# class Author < ActiveRecord::Base
|
141
|
+
# has_many :books
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
# Author.reflect_on_association(:books).klass
|
145
|
+
# # => Book
|
146
|
+
#
|
147
|
+
# <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate
|
148
|
+
# a new association object. Use +build_association+ or +create_association+
|
149
|
+
# instead. This allows plugins to hook into association object creation.
|
132
150
|
def klass
|
133
151
|
@klass ||= active_record.send(:compute_type, class_name)
|
134
152
|
end
|
135
153
|
|
154
|
+
# Returns a new, unsaved instance of the associated class. +options+ will
|
155
|
+
# be passed to the class's constructor.
|
156
|
+
def build_association(*options)
|
157
|
+
klass.new(*options)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Creates a new instance of the associated class, and immediates saves it
|
161
|
+
# with ActiveRecord::Base#save. +options+ will be passed to the class's
|
162
|
+
# creation method. Returns the newly created object.
|
163
|
+
def create_association(*options)
|
164
|
+
klass.create(*options)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Creates a new instance of the associated class, and immediates saves it
|
168
|
+
# with ActiveRecord::Base#save!. +options+ will be passed to the class's
|
169
|
+
# creation method. If the created record doesn't pass validations, then an
|
170
|
+
# exception will be raised.
|
171
|
+
#
|
172
|
+
# Returns the newly created object.
|
173
|
+
def create_association!(*options)
|
174
|
+
klass.create!(*options)
|
175
|
+
end
|
176
|
+
|
136
177
|
def table_name
|
137
178
|
@table_name ||= klass.table_name
|
138
179
|
end
|
@@ -157,6 +198,52 @@ module ActiveRecord
|
|
157
198
|
end
|
158
199
|
end
|
159
200
|
|
201
|
+
def check_validity!
|
202
|
+
end
|
203
|
+
|
204
|
+
def through_reflection
|
205
|
+
false
|
206
|
+
end
|
207
|
+
|
208
|
+
def through_reflection_primary_key_name
|
209
|
+
end
|
210
|
+
|
211
|
+
def source_reflection
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
def derive_class_name
|
217
|
+
class_name = name.to_s.camelize
|
218
|
+
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
219
|
+
class_name
|
220
|
+
end
|
221
|
+
|
222
|
+
def derive_primary_key_name
|
223
|
+
if belongs_to?
|
224
|
+
"#{name}_id"
|
225
|
+
elsif options[:as]
|
226
|
+
"#{options[:as]}_id"
|
227
|
+
else
|
228
|
+
active_record.name.foreign_key
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Holds all the meta-data about a :through association as it was specified in the Active Record class.
|
234
|
+
class ThroughReflection < AssociationReflection #:nodoc:
|
235
|
+
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
236
|
+
# (The <tt>:tags</tt> association on Tagging below.)
|
237
|
+
#
|
238
|
+
# class Post < ActiveRecord::Base
|
239
|
+
# has_many :taggings
|
240
|
+
# has_many :tags, :through => :taggings
|
241
|
+
# end
|
242
|
+
#
|
243
|
+
def source_reflection
|
244
|
+
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
245
|
+
end
|
246
|
+
|
160
247
|
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
|
161
248
|
# of a HasManyThrough or HasOneThrough association. Example:
|
162
249
|
#
|
@@ -169,7 +256,7 @@ module ActiveRecord
|
|
169
256
|
# taggings_reflection = tags_reflection.through_reflection
|
170
257
|
#
|
171
258
|
def through_reflection
|
172
|
-
@through_reflection ||=
|
259
|
+
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
173
260
|
end
|
174
261
|
|
175
262
|
# Gets an array of possible <tt>:through</tt> source reflection names:
|
@@ -180,63 +267,40 @@ module ActiveRecord
|
|
180
267
|
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
|
181
268
|
end
|
182
269
|
|
183
|
-
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
184
|
-
# (The <tt>:tags</tt> association on Tagging below.)
|
185
|
-
#
|
186
|
-
# class Post < ActiveRecord::Base
|
187
|
-
# has_many :taggings
|
188
|
-
# has_many :tags, :through => :taggings
|
189
|
-
# end
|
190
|
-
#
|
191
|
-
def source_reflection
|
192
|
-
return nil unless through_reflection
|
193
|
-
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
194
|
-
end
|
195
|
-
|
196
270
|
def check_validity!
|
197
|
-
if
|
198
|
-
|
199
|
-
|
200
|
-
end
|
201
|
-
|
202
|
-
if source_reflection.nil?
|
203
|
-
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
204
|
-
end
|
271
|
+
if through_reflection.nil?
|
272
|
+
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
|
273
|
+
end
|
205
274
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
end
|
213
|
-
|
214
|
-
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
215
|
-
raise HasManyThroughSourceAssociationMacroError.new(self)
|
216
|
-
end
|
275
|
+
if source_reflection.nil?
|
276
|
+
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
277
|
+
end
|
278
|
+
|
279
|
+
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
280
|
+
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
217
281
|
end
|
282
|
+
|
283
|
+
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
284
|
+
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
|
285
|
+
end
|
286
|
+
|
287
|
+
unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
288
|
+
raise HasManyThroughSourceAssociationMacroError.new(self)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def through_reflection_primary_key
|
293
|
+
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
|
294
|
+
end
|
295
|
+
|
296
|
+
def through_reflection_primary_key_name
|
297
|
+
through_reflection.primary_key_name if through_reflection.belongs_to?
|
218
298
|
end
|
219
299
|
|
220
300
|
private
|
221
301
|
def derive_class_name
|
222
302
|
# get the class_name of the belongs_to association of the through reflection
|
223
|
-
|
224
|
-
options[:source_type] || source_reflection.class_name
|
225
|
-
else
|
226
|
-
class_name = name.to_s.camelize
|
227
|
-
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
228
|
-
class_name
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
def derive_primary_key_name
|
233
|
-
if macro == :belongs_to
|
234
|
-
"#{name}_id"
|
235
|
-
elsif options[:as]
|
236
|
-
"#{options[:as]}_id"
|
237
|
-
else
|
238
|
-
active_record.name.foreign_key
|
239
|
-
end
|
303
|
+
options[:source_type] || source_reflection.class_name
|
240
304
|
end
|
241
305
|
end
|
242
306
|
end
|
@@ -102,7 +102,7 @@ HEADER
|
|
102
102
|
spec[:precision] = column.precision.inspect if !column.precision.nil?
|
103
103
|
spec[:scale] = column.scale.inspect if !column.scale.nil?
|
104
104
|
spec[:null] = 'false' if !column.null
|
105
|
-
spec[:default] = default_string(column.default) if
|
105
|
+
spec[:default] = default_string(column.default) if column.has_default?
|
106
106
|
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
|
107
107
|
spec
|
108
108
|
end.compact
|
@@ -159,13 +159,19 @@ HEADER
|
|
159
159
|
end
|
160
160
|
|
161
161
|
def indexes(table, stream)
|
162
|
-
indexes = @connection.indexes(table)
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
if (indexes = @connection.indexes(table)).any?
|
163
|
+
add_index_statements = indexes.map do |index|
|
164
|
+
statment_parts = [ ('add_index ' + index.table.inspect) ]
|
165
|
+
statment_parts << index.columns.inspect
|
166
|
+
statment_parts << (':name => ' + index.name.inspect)
|
167
|
+
statment_parts << ':unique => true' if index.unique
|
168
|
+
|
169
|
+
' ' + statment_parts.join(', ')
|
170
|
+
end
|
171
|
+
|
172
|
+
stream.puts add_index_statements.sort.join("\n")
|
166
173
|
stream.puts
|
167
174
|
end
|
168
|
-
stream.puts unless indexes.empty?
|
169
175
|
end
|
170
176
|
end
|
171
|
-
end
|
177
|
+
end
|
@@ -11,11 +11,9 @@ module ActiveRecord
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def assert_date_from_db(expected, actual, message = nil)
|
14
|
-
#
|
14
|
+
# SybaseAdapter doesn't have a separate column type just for dates,
|
15
15
|
# so the time is in the string and incorrectly formatted
|
16
|
-
if current_adapter?(:
|
17
|
-
assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00")
|
18
|
-
elsif current_adapter?(:SybaseAdapter)
|
16
|
+
if current_adapter?(:SybaseAdapter)
|
19
17
|
assert_equal expected.to_s, actual.to_date.to_s, message
|
20
18
|
else
|
21
19
|
assert_equal expected.to_s, actual.to_s, message
|
@@ -37,11 +35,26 @@ module ActiveRecord
|
|
37
35
|
$queries_executed = []
|
38
36
|
yield
|
39
37
|
ensure
|
40
|
-
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
|
38
|
+
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
|
41
39
|
end
|
42
40
|
|
43
41
|
def assert_no_queries(&block)
|
44
42
|
assert_queries(0, &block)
|
45
43
|
end
|
44
|
+
|
45
|
+
def self.use_concurrent_connections
|
46
|
+
setup :connection_allow_concurrency_setup
|
47
|
+
teardown :connection_allow_concurrency_teardown
|
48
|
+
end
|
49
|
+
|
50
|
+
def connection_allow_concurrency_setup
|
51
|
+
@connection = ActiveRecord::Base.remove_connection
|
52
|
+
ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
|
53
|
+
end
|
54
|
+
|
55
|
+
def connection_allow_concurrency_teardown
|
56
|
+
ActiveRecord::Base.clear_all_connections!
|
57
|
+
ActiveRecord::Base.establish_connection(@connection)
|
58
|
+
end
|
46
59
|
end
|
47
60
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'thread'
|
2
2
|
|
3
3
|
module ActiveRecord
|
4
|
-
|
4
|
+
# See ActiveRecord::Transactions::ClassMethods for documentation.
|
5
|
+
module Transactions
|
5
6
|
class TransactionError < ActiveRecordError # :nodoc:
|
6
7
|
end
|
7
8
|
|
@@ -15,26 +16,33 @@ module ActiveRecord
|
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
|
-
# Transactions are protective blocks where SQL statements are only permanent
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
19
|
+
# Transactions are protective blocks where SQL statements are only permanent
|
20
|
+
# if they can all succeed as one atomic action. The classic example is a
|
21
|
+
# transfer between two accounts where you can only have a deposit if the
|
22
|
+
# withdrawal succeeded and vice versa. Transactions enforce the integrity of
|
23
|
+
# the database and guard the data against program errors or database
|
24
|
+
# break-downs. So basically you should use transaction blocks whenever you
|
25
|
+
# have a number of statements that must be executed together or not at all.
|
26
|
+
# Example:
|
23
27
|
#
|
24
|
-
# transaction do
|
28
|
+
# ActiveRecord::Base.transaction do
|
25
29
|
# david.withdrawal(100)
|
26
30
|
# mary.deposit(100)
|
27
31
|
# end
|
28
32
|
#
|
29
|
-
# This example will only take money from David and give to Mary if neither
|
30
|
-
#
|
31
|
-
# that the
|
33
|
+
# This example will only take money from David and give to Mary if neither
|
34
|
+
# +withdrawal+ nor +deposit+ raises an exception. Exceptions will force a
|
35
|
+
# ROLLBACK that returns the database to the state before the transaction was
|
36
|
+
# begun. Be aware, though, that the objects will _not_ have their instance
|
37
|
+
# data returned to their pre-transactional state.
|
32
38
|
#
|
33
39
|
# == Different Active Record classes in a single transaction
|
34
40
|
#
|
35
41
|
# Though the transaction class method is called on some Active Record class,
|
36
42
|
# the objects within the transaction block need not all be instances of
|
37
|
-
# that class.
|
43
|
+
# that class. This is because transactions are per-database connection, not
|
44
|
+
# per-model.
|
45
|
+
#
|
38
46
|
# In this example a <tt>Balance</tt> record is transactionally saved even
|
39
47
|
# though <tt>transaction</tt> is called on the <tt>Account</tt> class:
|
40
48
|
#
|
@@ -43,6 +51,14 @@ module ActiveRecord
|
|
43
51
|
# account.save!
|
44
52
|
# end
|
45
53
|
#
|
54
|
+
# Note that the +transaction+ method is also available as a model instance
|
55
|
+
# method. For example, you can also do this:
|
56
|
+
#
|
57
|
+
# balance.transaction do
|
58
|
+
# balance.save!
|
59
|
+
# account.save!
|
60
|
+
# end
|
61
|
+
#
|
46
62
|
# == Transactions are not distributed across database connections
|
47
63
|
#
|
48
64
|
# A transaction acts on a single database connection. If you have
|
@@ -62,48 +78,72 @@ module ActiveRecord
|
|
62
78
|
#
|
63
79
|
# == Save and destroy are automatically wrapped in a transaction
|
64
80
|
#
|
65
|
-
# Both Base#save and Base#destroy come wrapped in a transaction that ensures
|
66
|
-
#
|
67
|
-
#
|
81
|
+
# Both Base#save and Base#destroy come wrapped in a transaction that ensures
|
82
|
+
# that whatever you do in validations or callbacks will happen under the
|
83
|
+
# protected cover of a transaction. So you can use validations to check for
|
84
|
+
# values that the transaction depends on or you can raise exceptions in the
|
85
|
+
# callbacks to rollback, including <tt>after_*</tt> callbacks.
|
86
|
+
#
|
87
|
+
# == Exception handling and rolling back
|
88
|
+
#
|
89
|
+
# Also have in mind that exceptions thrown within a transaction block will
|
90
|
+
# be propagated (after triggering the ROLLBACK), so you should be ready to
|
91
|
+
# catch those in your application code.
|
92
|
+
#
|
93
|
+
# One exception is the ActiveRecord::Rollback exception, which will trigger
|
94
|
+
# a ROLLBACK when raised, but not be re-raised by the transaction block.
|
95
|
+
#
|
96
|
+
# *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
|
97
|
+
# inside a transaction block. StatementInvalid exceptions indicate that an
|
98
|
+
# error occurred at the database level, for example when a unique constraint
|
99
|
+
# is violated. On some database systems, such as PostgreSQL, database errors
|
100
|
+
# inside a transaction causes the entire transaction to become unusable
|
101
|
+
# until it's restarted from the beginning. Here is an example which
|
102
|
+
# demonstrates the problem:
|
68
103
|
#
|
69
|
-
#
|
104
|
+
# # Suppose that we have a Number model with a unique column called 'i'.
|
105
|
+
# Number.transaction do
|
106
|
+
# Number.create(:i => 0)
|
107
|
+
# begin
|
108
|
+
# # This will raise a unique constraint error...
|
109
|
+
# Number.create(:i => 0)
|
110
|
+
# rescue ActiveRecord::StatementInvalid
|
111
|
+
# # ...which we ignore.
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# # On PostgreSQL, the transaction is now unusable. The following
|
115
|
+
# # statement will cause a PostgreSQL error, even though the unique
|
116
|
+
# # constraint is no longer violated:
|
117
|
+
# Number.create(:i => 1)
|
118
|
+
# # => "PGError: ERROR: current transaction is aborted, commands
|
119
|
+
# # ignored until end of transaction block"
|
120
|
+
# end
|
70
121
|
#
|
71
|
-
#
|
72
|
-
# should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
|
73
|
-
# trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
|
122
|
+
# One should restart the entire transaction if a StatementError occurred.
|
74
123
|
module ClassMethods
|
124
|
+
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
75
125
|
def transaction(&block)
|
76
|
-
increment_open_transactions
|
126
|
+
connection.increment_open_transactions
|
77
127
|
|
78
128
|
begin
|
79
|
-
connection.transaction(
|
129
|
+
connection.transaction(connection.open_transactions == 1, &block)
|
80
130
|
ensure
|
81
|
-
decrement_open_transactions
|
131
|
+
connection.decrement_open_transactions
|
82
132
|
end
|
83
133
|
end
|
84
|
-
|
85
|
-
private
|
86
|
-
def increment_open_transactions #:nodoc:
|
87
|
-
open = Thread.current['open_transactions'] ||= 0
|
88
|
-
Thread.current['start_db_transaction'] = open.zero?
|
89
|
-
Thread.current['open_transactions'] = open + 1
|
90
|
-
end
|
91
|
-
|
92
|
-
def decrement_open_transactions #:nodoc:
|
93
|
-
Thread.current['open_transactions'] -= 1
|
94
|
-
end
|
95
134
|
end
|
96
135
|
|
136
|
+
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
97
137
|
def transaction(&block)
|
98
138
|
self.class.transaction(&block)
|
99
139
|
end
|
100
140
|
|
101
141
|
def destroy_with_transactions #:nodoc:
|
102
|
-
|
142
|
+
with_transaction_returning_status(:destroy_without_transactions)
|
103
143
|
end
|
104
144
|
|
105
145
|
def save_with_transactions(perform_validation = true) #:nodoc:
|
106
|
-
rollback_active_record_state! {
|
146
|
+
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
|
107
147
|
end
|
108
148
|
|
109
149
|
def save_with_transactions! #:nodoc:
|
@@ -126,5 +166,20 @@ module ActiveRecord
|
|
126
166
|
end
|
127
167
|
raise
|
128
168
|
end
|
169
|
+
|
170
|
+
# Executes +method+ within a transaction and captures its return value as a
|
171
|
+
# status flag. If the status is true the transaction is committed, otherwise
|
172
|
+
# a ROLLBACK is issued. In any case the status flag is returned.
|
173
|
+
#
|
174
|
+
# This method is available within the context of an ActiveRecord::Base
|
175
|
+
# instance.
|
176
|
+
def with_transaction_returning_status(method, *args)
|
177
|
+
status = nil
|
178
|
+
transaction do
|
179
|
+
status = send(method, *args)
|
180
|
+
raise ActiveRecord::Rollback unless status
|
181
|
+
end
|
182
|
+
status
|
183
|
+
end
|
129
184
|
end
|
130
185
|
end
|