3
3
require "active_support/core_ext/digest"
4
4
5
5
module ActiveRecord
6
+ # This abstract class specifies the interface to interact with the current transaction state.
7
+ #
8
+ # Any other methods not specified here are considered to be private interfaces.
9
+ #
10
+ # == Callbacks
11
+ #
12
+ # After updating the database state, you may sometimes need to perform some extra work, or reflect these
13
+ # changes in a remote system like clearing or updating a cache:
14
+ #
15
+ # def publish_article(article)
16
+ # article.update!(published: true)
17
+ # NotificationService.article_published(article)
18
+ # end
19
+ #
20
+ # The above code works but has one important flaw, which is that it no longer works properly if called inside
21
+ # a transaction, as it will interact with the remote system before the changes are persisted:
22
+ #
23
+ # Article.transaction do
24
+ # article = create_article(article)
25
+ # publish_article(article)
26
+ # end
27
+ #
28
+ # The callbacks offered by ActiveRecord::Transaction allow to rewriting this method in a way that is compatible
29
+ # with transactions:
30
+ #
31
+ # def publish_article(article)
32
+ # article.update!(published: true)
33
+ # Article.current_transaction.after_commit do
34
+ # NotificationService.article_published(article)
35
+ # end
36
+ # end
37
+ #
38
+ # In the above example, if +publish_article+ is called inside a transaction, the callback will be invoked
39
+ # after the transaction is successfully committed, and if called outside a transaction, the callback will be invoked
40
+ # immediately.
41
+ #
42
+ # == Caveats
43
+ #
44
+ # When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction
45
+ # won't be rolled back. Relying solely on these to synchronize state between multiple systems may lead to consistency issues.
6
46
class Transaction
7
47
class Callback # :nodoc:
8
48
def initialize ( event , callback )
@@ -35,6 +75,8 @@ def initialize # :nodoc:
35
75
# If the current transaction has a parent transaction, the callback is transferred to
36
76
# the parent when the current transaction commits, or dropped when the current transaction
37
77
# is rolled back. This operation is repeated until the outermost transaction is reached.
78
+ #
79
+ # If the callback raises an error, the transaction is rolled back.
38
80
def before_commit ( &block )
39
81
( @callbacks ||= [ ] ) << Callback . new ( :before_commit , block )
40
82
end
@@ -46,6 +88,8 @@ def before_commit(&block)
46
88
# If the current transaction has a parent transaction, the callback is transferred to
47
89
# the parent when the current transaction commits, or dropped when the current transaction
48
90
# is rolled back. This operation is repeated until the outermost transaction is reached.
91
+ #
92
+ # If the callback raises an error, the transaction remains committed.
49
93
def after_commit ( &block )
50
94
( @callbacks ||= [ ] ) << Callback . new ( :after_commit , block )
51
95
end
@@ -63,13 +107,24 @@ def after_rollback(&block)
63
107
( @callbacks ||= [ ] ) << Callback . new ( :after_rollback , block )
64
108
end
65
109
110
+ # Returns true if a transaction was started.
111
+ def open?
112
+ true
113
+ end
114
+ alias_method :blank? , :open?
115
+
116
+ # Returns true if no transaction is currently active.
117
+ def closed?
118
+ false
119
+ end
120
+
66
121
# Returns a UUID for this transaction.
67
122
def uuid
68
123
@uuid ||= Digest ::UUID . uuid_v4
69
124
end
70
125
71
126
protected
72
- def append_callbacks ( callbacks )
127
+ def append_callbacks ( callbacks ) # :nodoc:
73
128
( @callbacks ||= [ ] ) . concat ( callbacks )
74
129
end
75
130
end
0 commit comments