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.

Files changed (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -0,0 +1,88 @@
1
+ module ActiveRecord
2
+ # = Active Record Timestamp
3
+ #
4
+ # Active Record automatically timestamps create and update operations if the
5
+ # table has fields named <tt>created_at/created_on</tt> or
6
+ # <tt>updated_at/updated_on</tt>.
7
+ #
8
+ # Timestamping can be turned off by setting:
9
+ #
10
+ # <tt>ActiveRecord::Base.record_timestamps = false</tt>
11
+ #
12
+ # Timestamps are in the local timezone by default but you can use UTC by setting:
13
+ #
14
+ # <tt>ActiveRecord::Base.default_timezone = :utc</tt>
15
+ #
16
+ # == Time Zone aware attributes
17
+ #
18
+ # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code.
19
+ #
20
+ # ActiveRecord::Base.time_zone_aware_attributes = true
21
+ #
22
+ # This feature can easily be turned off by assigning value <tt>false</tt> .
23
+ #
24
+ # If your attributes are time zone aware and you desire to skip time zone conversion for certain
25
+ # attributes then you can do following:
26
+ #
27
+ # Topic.skip_time_zone_conversion_for_attributes = [:written_on]
28
+ module Timestamp
29
+ extend ActiveSupport::Concern
30
+
31
+ included do
32
+ class_inheritable_accessor :record_timestamps, :instance_writer => false
33
+ self.record_timestamps = true
34
+ end
35
+
36
+ private
37
+
38
+ def create #:nodoc:
39
+ if record_timestamps
40
+ current_time = current_time_from_proper_timezone
41
+
42
+ all_timestamp_attributes.each do |column|
43
+ write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil?
44
+ end
45
+ end
46
+
47
+ super
48
+ end
49
+
50
+ def update(*args) #:nodoc:
51
+ if should_record_timestamps?
52
+ current_time = current_time_from_proper_timezone
53
+
54
+ timestamp_attributes_for_update_in_model.each do |column|
55
+ column = column.to_s
56
+ next if attribute_changed?(column)
57
+ write_attribute(column, current_time)
58
+ end
59
+ end
60
+ super
61
+ end
62
+
63
+ def should_record_timestamps?
64
+ record_timestamps && (!partial_updates? || changed?)
65
+ end
66
+
67
+ def timestamp_attributes_for_update_in_model
68
+ timestamp_attributes_for_update.select { |c| respond_to?(c) }
69
+ end
70
+
71
+ def timestamp_attributes_for_update #:nodoc:
72
+ [:updated_at, :updated_on]
73
+ end
74
+
75
+ def timestamp_attributes_for_create #:nodoc:
76
+ [:created_at, :created_on]
77
+ end
78
+
79
+ def all_timestamp_attributes #:nodoc:
80
+ timestamp_attributes_for_create + timestamp_attributes_for_update
81
+ end
82
+
83
+ def current_time_from_proper_timezone #:nodoc:
84
+ self.class.default_timezone == :utc ? Time.now.utc : Time.now
85
+ end
86
+ end
87
+ end
88
+
@@ -1,102 +1,356 @@
1
- require 'active_record/vendor/simple.rb'
2
1
  require 'thread'
3
2
 
4
3
  module ActiveRecord
5
- module Transactions # :nodoc:
6
- TRANSACTION_MUTEX = Mutex.new
4
+ # See ActiveRecord::Transactions::ClassMethods for documentation.
5
+ module Transactions
6
+ extend ActiveSupport::Concern
7
7
 
8
- def self.append_features(base)
9
- super
10
- base.extend(ClassMethods)
11
-
12
- base.class_eval do
13
- alias_method :destroy_without_transactions, :destroy
14
- alias_method :destroy, :destroy_with_transactions
15
-
16
- alias_method :save_without_transactions, :save
17
- alias_method :save, :save_with_transactions
18
- end
8
+ class TransactionError < ActiveRecordError # :nodoc:
19
9
  end
20
10
 
21
- # Transactions are protective blocks where SQL statements are only permanent if they can all succed as one atomic action.
22
- # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and
23
- # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs.
24
- # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
25
- # not at all. Example:
11
+ included do
12
+ define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
13
+ end
14
+ # = Active Record Transactions
26
15
  #
27
- # Account.transaction do
16
+ # Transactions are protective blocks where SQL statements are only permanent
17
+ # if they can all succeed as one atomic action. The classic example is a
18
+ # transfer between two accounts where you can only have a deposit if the
19
+ # withdrawal succeeded and vice versa. Transactions enforce the integrity of
20
+ # the database and guard the data against program errors or database
21
+ # break-downs. So basically you should use transaction blocks whenever you
22
+ # have a number of statements that must be executed together or not at all.
23
+ #
24
+ # For example:
25
+ #
26
+ # ActiveRecord::Base.transaction do
28
27
  # david.withdrawal(100)
29
28
  # mary.deposit(100)
30
29
  # end
31
30
  #
32
- # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
33
- # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
34
- # that the objects by default will _not_ have their instance data returned to their pre-transactional state.
31
+ # This example will only take money from David and give it to Mary if neither
32
+ # +withdrawal+ nor +deposit+ raise an exception. Exceptions will force a
33
+ # ROLLBACK that returns the database to the state before the transaction
34
+ # began. Be aware, though, that the objects will _not_ have their instance
35
+ # data returned to their pre-transactional state.
35
36
  #
36
- # == Save and destroy are automatically wrapped in a transaction
37
+ # == Different Active Record classes in a single transaction
37
38
  #
38
- # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
39
- # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
40
- # depend on or you can raise exceptions in the callbacks to rollback.
39
+ # Though the transaction class method is called on some Active Record class,
40
+ # the objects within the transaction block need not all be instances of
41
+ # that class. This is because transactions are per-database connection, not
42
+ # per-model.
41
43
  #
42
- # == Object-level transactions
44
+ # In this example a +balance+ record is transactionally saved even
45
+ # though +transaction+ is called on the +Account+ class:
43
46
  #
44
- # You can enable object-level transactions for Active Record objects, though. You do this by naming the each of the Active Records
45
- # that you want to enable object-level transactions for, like this:
47
+ # Account.transaction do
48
+ # balance.save!
49
+ # account.save!
50
+ # end
46
51
  #
47
- # Account.transaction(david, mary) do
48
- # david.withdrawal(100)
49
- # mary.deposit(100)
52
+ # The +transaction+ method is also available as a model instance method.
53
+ # For example, you can also do this:
54
+ #
55
+ # balance.transaction do
56
+ # balance.save!
57
+ # account.save!
58
+ # end
59
+ #
60
+ # == Transactions are not distributed across database connections
61
+ #
62
+ # A transaction acts on a single database connection. If you have
63
+ # multiple class-specific databases, the transaction will not protect
64
+ # interaction among them. One workaround is to begin a transaction
65
+ # on each class whose models you alter:
66
+ #
67
+ # Student.transaction do
68
+ # Course.transaction do
69
+ # course.enroll(student)
70
+ # student.units += course.units
71
+ # end
50
72
  # end
51
73
  #
52
- # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in
53
- # neither object nor database.
54
- #
55
- # == Exception handling
56
- #
57
- # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
58
- # should be ready to catch those in your application code.
59
- #
60
- # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
61
- module ClassMethods
62
- def transaction(*objects, &block)
63
- TRANSACTION_MUTEX.lock
64
-
65
- begin
66
- objects.each { |o| o.extend(Transaction::Simple) }
67
- objects.each { |o| o.start_transaction }
68
- connection.begin_db_transaction
69
-
70
- block.call
71
-
72
- connection.commit_db_transaction
73
- objects.each { |o| o.commit_transaction }
74
- rescue Exception => exception
75
- connection.rollback_db_transaction
76
- objects.each { |o| o.abort_transaction }
77
- raise exception
78
- ensure
79
- TRANSACTION_MUTEX.unlock
74
+ # This is a poor solution, but fully distributed transactions are beyond
75
+ # the scope of Active Record.
76
+ #
77
+ # == +save+ and +destroy+ are automatically wrapped in a transaction
78
+ #
79
+ # Both +save+ and +destroy+ come wrapped in a transaction that ensures
80
+ # that whatever you do in validations or callbacks will happen under its
81
+ # protected cover. So you can use validations to check for values that
82
+ # the transaction depends on or you can raise exceptions in the callbacks
83
+ # to rollback, including <tt>after_*</tt> callbacks.
84
+ #
85
+ # As a consequence changes to the database are not seen outside your connection
86
+ # until the operation is complete. For example, if you try to update the index
87
+ # of a search engine in +after_save+ the indexer won't see the updated record.
88
+ # The +after_commit+ callback is the only one that is triggered once the update
89
+ # is committed. See below.
90
+ #
91
+ # == Exception handling and rolling back
92
+ #
93
+ # Also have in mind that exceptions thrown within a transaction block will
94
+ # be propagated (after triggering the ROLLBACK), so you should be ready to
95
+ # catch those in your application code.
96
+ #
97
+ # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger
98
+ # a ROLLBACK when raised, but not be re-raised by the transaction block.
99
+ #
100
+ # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions
101
+ # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an
102
+ # error occurred at the database level, for example when a unique constraint
103
+ # is violated. On some database systems, such as PostgreSQL, database errors
104
+ # inside a transaction cause the entire transaction to become unusable
105
+ # until it's restarted from the beginning. Here is an example which
106
+ # demonstrates the problem:
107
+ #
108
+ # # Suppose that we have a Number model with a unique column called 'i'.
109
+ # Number.transaction do
110
+ # Number.create(:i => 0)
111
+ # begin
112
+ # # This will raise a unique constraint error...
113
+ # Number.create(:i => 0)
114
+ # rescue ActiveRecord::StatementInvalid
115
+ # # ...which we ignore.
116
+ # end
117
+ #
118
+ # # On PostgreSQL, the transaction is now unusable. The following
119
+ # # statement will cause a PostgreSQL error, even though the unique
120
+ # # constraint is no longer violated:
121
+ # Number.create(:i => 1)
122
+ # # => "PGError: ERROR: current transaction is aborted, commands
123
+ # # ignored until end of transaction block"
124
+ # end
125
+ #
126
+ # One should restart the entire transaction if an
127
+ # <tt>ActiveRecord::StatementInvalid</tt> occurred.
128
+ #
129
+ # == Nested transactions
130
+ #
131
+ # +transaction+ calls can be nested. By default, this makes all database
132
+ # statements in the nested transaction block become part of the parent
133
+ # transaction. For example:
134
+ #
135
+ # User.transaction do
136
+ # User.create(:username => 'Kotori')
137
+ # User.transaction do
138
+ # User.create(:username => 'Nemu')
139
+ # raise ActiveRecord::Rollback
140
+ # end
141
+ # end
142
+ #
143
+ # User.find(:all) # => empty
144
+ #
145
+ # It is also possible to requires a sub-transaction by passing
146
+ # <tt>:requires_new => true</tt>. If anything goes wrong, the
147
+ # database rolls back to the beginning of the sub-transaction
148
+ # without rolling back the parent transaction. For example:
149
+ #
150
+ # User.transaction do
151
+ # User.create(:username => 'Kotori')
152
+ # User.transaction(:requires_new => true) do
153
+ # User.create(:username => 'Nemu')
154
+ # raise ActiveRecord::Rollback
155
+ # end
156
+ # end
157
+ #
158
+ # User.find(:all) # => Returns only Kotori
159
+ #
160
+ # Most databases don't support true nested transactions. At the time of
161
+ # writing, the only database that we're aware of that supports true nested
162
+ # transactions, is MS-SQL. Because of this, Active Record emulates nested
163
+ # transactions by using savepoints. See
164
+ # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
165
+ # for more information about savepoints.
166
+ #
167
+ # === Callbacks
168
+ #
169
+ # There are two types of callbacks associated with committing and rolling back transactions:
170
+ # +after_commit+ and +after_rollback+.
171
+ #
172
+ # +after_commit+ callbacks are called on every record saved or destroyed within a
173
+ # transaction immediately after the transaction is committed. +after_rollback+ callbacks
174
+ # are called on every record saved or destroyed within a transaction immediately after the
175
+ # transaction or savepoint is rolled back.
176
+ #
177
+ # These callbacks are useful for interacting with other systems since you will be guaranteed
178
+ # that the callback is only executed when the database is in a permanent state. For example,
179
+ # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
180
+ # within a transaction could trigger the cache to be regenerated before the database is updated.
181
+ #
182
+ # === Caveats
183
+ #
184
+ # If you're on MySQL, then do not use DDL operations in nested transactions
185
+ # blocks that are emulated with savepoints. That is, do not execute statements
186
+ # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
187
+ # releases all savepoints upon executing a DDL operation. When +transaction+
188
+ # is finished and tries to release the savepoint it created earlier, a
189
+ # database error will occur because the savepoint has already been
190
+ # automatically released. The following example demonstrates the problem:
191
+ #
192
+ # Model.connection.transaction do # BEGIN
193
+ # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
194
+ # Model.connection.create_table(...) # active_record_1 now automatically released
195
+ # end # RELEASE savepoint active_record_1
196
+ # # ^^^^ BOOM! database error!
197
+ # end
198
+ #
199
+ # Note that "TRUNCATE" is also a MySQL DDL statement!
200
+ module ClassMethods
201
+ # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
202
+ def transaction(options = {}, &block)
203
+ # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
204
+ connection.transaction(options, &block)
205
+ end
206
+
207
+ def after_commit(*args, &block)
208
+ options = args.last
209
+ if options.is_a?(Hash) && options[:on]
210
+ options[:if] = Array.wrap(options[:if])
211
+ options[:if] << "transaction_include_action?(:#{options[:on]})"
80
212
  end
213
+ set_callback(:commit, :after, *args, &block)
214
+ end
215
+
216
+ def after_rollback(*args, &block)
217
+ options = args.last
218
+ if options.is_a?(Hash) && options[:on]
219
+ options[:if] = Array.wrap(options[:if])
220
+ options[:if] << "transaction_include_action?(:#{options[:on]})"
221
+ end
222
+ set_callback(:rollback, :after, *args, &block)
223
+ end
224
+ end
225
+
226
+ # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
227
+ def transaction(&block)
228
+ self.class.transaction(&block)
229
+ end
230
+
231
+ def destroy #:nodoc:
232
+ with_transaction_returning_status { super }
233
+ end
234
+
235
+ def save(*) #:nodoc:
236
+ rollback_active_record_state! do
237
+ with_transaction_returning_status { super }
238
+ end
239
+ end
240
+
241
+ def save!(*) #:nodoc:
242
+ with_transaction_returning_status { super }
243
+ end
244
+
245
+ # Reset id and @new_record if the transaction rolls back.
246
+ def rollback_active_record_state!
247
+ remember_transaction_record_state
248
+ yield
249
+ rescue Exception
250
+ restore_transaction_record_state
251
+ raise
252
+ ensure
253
+ clear_transaction_record_state
254
+ end
255
+
256
+ # Call the after_commit callbacks
257
+ def committed! #:nodoc:
258
+ _run_commit_callbacks
259
+ ensure
260
+ clear_transaction_record_state
261
+ end
262
+
263
+ # Call the after rollback callbacks. The restore_state argument indicates if the record
264
+ # state should be rolled back to the beginning or just to the last savepoint.
265
+ def rolledback!(force_restore_state = false) #:nodoc:
266
+ _run_rollback_callbacks
267
+ ensure
268
+ restore_transaction_record_state(force_restore_state)
269
+ end
270
+
271
+ # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
272
+ # can be called.
273
+ def add_to_transaction
274
+ if self.class.connection.add_transaction_record(self)
275
+ remember_transaction_record_state
81
276
  end
82
277
  end
83
278
 
84
- def destroy_with_transactions #:nodoc:
85
- if TRANSACTION_MUTEX.locked?
86
- destroy_without_transactions
87
- else
88
- ActiveRecord::Base.transaction { destroy_without_transactions }
279
+ # Executes +method+ within a transaction and captures its return value as a
280
+ # status flag. If the status is true the transaction is committed, otherwise
281
+ # a ROLLBACK is issued. In any case the status flag is returned.
282
+ #
283
+ # This method is available within the context of an ActiveRecord::Base
284
+ # instance.
285
+ def with_transaction_returning_status
286
+ status = nil
287
+ self.class.transaction do
288
+ add_to_transaction
289
+ status = yield
290
+ raise ActiveRecord::Rollback unless status
291
+ end
292
+ status
293
+ end
294
+
295
+ protected
296
+
297
+ # Save the new record state and id of a record so it can be restored later if a transaction fails.
298
+ def remember_transaction_record_state #:nodoc
299
+ @_start_transaction_state ||= {}
300
+ unless @_start_transaction_state.include?(:new_record)
301
+ @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
302
+ @_start_transaction_state[:new_record] = @new_record
303
+ end
304
+ unless @_start_transaction_state.include?(:destroyed)
305
+ @_start_transaction_state[:destroyed] = @destroyed
89
306
  end
307
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
90
308
  end
91
-
92
- def save_with_transactions(perform_validation = true) #:nodoc:
93
- result = nil
94
- if TRANSACTION_MUTEX.locked?
95
- result = save_without_transactions(perform_validation)
96
- else
97
- ActiveRecord::Base.transaction { result = save_without_transactions(perform_validation) }
309
+
310
+ # Clear the new record state and id of a record.
311
+ def clear_transaction_record_state #:nodoc
312
+ if defined?(@_start_transaction_state)
313
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
314
+ remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
315
+ end
316
+ end
317
+
318
+ # Restore the new record state and id of a record that was previously saved by a call to save_record_state.
319
+ def restore_transaction_record_state(force = false) #:nodoc
320
+ if defined?(@_start_transaction_state)
321
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
322
+ if @_start_transaction_state[:level] < 1
323
+ restore_state = remove_instance_variable(:@_start_transaction_state)
324
+ if restore_state
325
+ @attributes = @attributes.dup if @attributes.frozen?
326
+ @new_record = restore_state[:new_record]
327
+ @destroyed = restore_state[:destroyed]
328
+ if restore_state[:id]
329
+ self.id = restore_state[:id]
330
+ else
331
+ @attributes.delete(self.class.primary_key)
332
+ @attributes_cache.delete(self.class.primary_key)
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+ # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
340
+ def transaction_record_state(state) #:nodoc
341
+ @_start_transaction_state[state] if defined?(@_start_transaction_state)
342
+ end
343
+
344
+ # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
345
+ def transaction_include_action?(action) #:nodoc
346
+ case action
347
+ when :create
348
+ transaction_record_state(:new_record)
349
+ when :destroy
350
+ destroyed?
351
+ when :update
352
+ !(transaction_record_state(:new_record) || destroyed?)
98
353
  end
99
- return result
100
354
  end
101
355
  end
102
- end
356
+ end
@@ -0,0 +1,48 @@
1
+ module ActiveRecord
2
+ module Validations
3
+ class AssociatedValidator < ActiveModel::EachValidator
4
+ def validate_each(record, attribute, value)
5
+ return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
6
+ record.errors.add(attribute, :invalid, options.merge(:value => value))
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+ # Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
12
+ #
13
+ # class Book < ActiveRecord::Base
14
+ # has_many :pages
15
+ # belongs_to :library
16
+ #
17
+ # validates_associated :pages, :library
18
+ # end
19
+ #
20
+ # Warning: If, after the above definition, you then wrote:
21
+ #
22
+ # class Page < ActiveRecord::Base
23
+ # belongs_to :book
24
+ #
25
+ # validates_associated :book
26
+ # end
27
+ #
28
+ # this would specify a circular dependency and cause infinite recursion.
29
+ #
30
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to
31
+ # ensure that the association is both present and guaranteed to be valid, you also need to
32
+ # use +validates_presence_of+.
33
+ #
34
+ # Configuration options:
35
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid")
36
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
37
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
38
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
39
+ # method, proc or string should return or evaluate to a true or false value.
40
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
41
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
42
+ # method, proc or string should return or evaluate to a true or false value.
43
+ def validates_associated(*attr_names)
44
+ validates_with AssociatedValidator, _merge_attributes(attr_names)
45
+ end
46
+ end
47
+ end
48
+ end