File: transactions.rdoc

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (250 lines) | stat: -rw-r--r-- 9,930 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
= Database Transactions 

Sequel uses autocommit mode by default for all of its database adapters, so in general in Sequel if you want to use database transactions, you need to be explicit about it.  There are a few cases where transactions are used implicitly by default:

* Dataset#import to insert many records at once
* Dataset#paged_each to iterate over large datasets in batches
* Model#save
* Model#destroy
* Migrations if the database supports transactional schema
* Database#use_cursor in the postgres adapter
* Dataset#lock on PostgreSQL if given a block
* setter methods created by the association_pks plugin
* move* methods in the list plugin

Everywhere else, it is up to you to use a database transaction if you want to.

== Basic Transaction Usage

In Sequel, the <tt>Database#transaction</tt> method should be called if you want to use a database transaction.  This method must be called with a block.  If the block does not raise an exception, the transaction is committed:

  DB.transaction do # BEGIN
    DB[:foo].insert(1) # INSERT
  end # COMMIT
   
If the block raises a Sequel::Rollback exception, the transaction is rolled back, but no exception is raised outside the block:

  DB.transaction do # BEGIN
    raise Sequel::Rollback
  end # ROLLBACK
  # no exception raised

If any other exception is raised, the transaction is rolled back, and the exception is raised outside the block:

  DB.transaction do # BEGIN
    raise ArgumentError
  end # ROLLBACK
  # ArgumentError raised

If you want the current transaction to be rolled back when the transaction block exits instead of being committed (even if an exception is not raised), use <tt>Database#rollback_on_exit</tt>

  DB.transaction do # BEGIN
    DB.rollback_on_exit
  end # ROLLBACK

If you want Sequel::Rollback exceptions to be reraised, use the <tt>rollback: :reraise</tt> option:

  DB.transaction(rollback: :reraise) do # BEGIN
    raise Sequel::Rollback
  end # ROLLBACK
  # Sequel::Rollback raised

If you always want to rollback (useful for testing), use the <tt>rollback: :always</tt> option:

  DB.transaction(rollback: :always) do # BEGIN
    DB[:foo].insert(1) # INSERT
  end # ROLLBACK
  # no exception raised

If you want to check whether you are currently in a transaction, use the Database#in_transaction? method:

  DB.in_transaction? # false
  DB.transaction do
    DB.in_transaction? # true
  end

== Transaction Hooks

You can add hooks to an in progress transaction that are called after the transaction commits or rolls back:

  x = nil
  DB.transaction do
    DB.after_commit{x = 1}
    DB.after_rollback{x = 2}
    x # nil
  end
  x # 1
  
  x = nil
  DB.transaction do
    DB.after_commit{x = 1}
    DB.after_rollback{x = 2}
    raise Sequel::Rollback
  end
  x # 2

== Nested Transaction Calls / Savepoints

You can nest calls to transaction, which by default just reuses the existing transaction:

  DB.transaction do # BEGIN
    DB.transaction do
      DB[:foo].insert(1) # INSERT
    end
  end # COMMIT

You can use the <tt>savepoint: true</tt> option in the inner transaction to explicitly use a savepoint (if the database supports it):

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB[:foo].insert(1) # INSERT
    end # RELEASE SAVEPOINT
  end # COMMIT

You can use the <tt>auto_savepoint: true</tt> option in the outer transaction to explicitly use a savepoint in the inner transaction (if the database supports it):

  DB.transaction(auto_savepoint: true) do # BEGIN
    DB.transaction do # SAVEPOINT
      DB[:foo].insert(1) # INSERT
    end # RELEASE SAVEPOINT
  end # COMMIT

If a Sequel::Rollback exception is raised inside the savepoint block, it will only rollback to the savepoint:

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      raise Sequel::Rollback
    end # ROLLBACK TO SAVEPOINT
    # no exception raised
  end # COMMIT

Other exceptions, unless rescued inside the outer transaction block, will rollback the savepoint and the outer transactions, since they are reraised by the transaction code:

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      raise ArgumentError
    end # ROLLBACK TO SAVEPOINT
  end # ROLLBACK
  # ArgumentError raised

If you want the current savepoint to be rolled back when the savepoint block exits instead of being committed (even if an exception is not raised), use <tt>Database#rollback_on_exit(savepoint: true)</tt>

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.rollback_on_exit(savepoint: true)
    end # ROLLBACK TO SAVEPOINT
  end # COMMIT

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.transaction(savepoint: true) do # SAVEPOINT
        DB.rollback_on_exit(savepoint: true)
      end # ROLLBACK TO SAVEPOINT
    end # RELEASE SAVEPOINT
  end # COMMIT

If you want the current savepoint and potentially enclosing savepoints to be rolled back when the savepoint blocks exit  (even if an exception is not raised), use <tt>Database#rollback_on_exit(savepoint: integer)</tt>

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.transaction(savepoint: true) do # SAVEPOINT
        DB.rollback_on_exit(savepoint: 2)
      end # ROLLBACK TO SAVEPOINT
    end # ROLLBACK TO SAVEPOINT
  end # COMMIT

  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.transaction(savepoint: true) do # SAVEPOINT
        DB.rollback_on_exit(savepoint: 3)
      end # ROLLBACK TO SAVEPOINT
    end # ROLLBACK TO SAVEPOINT
  end # ROLLBACK

=== Savepoint Hooks

When using savepoints, you can use the +:savepoint+ option to +after_commit+ or +after_rollback+ to use a savepoint hook.  For +after_commit+, this will only run the hook after transaction commit if all enclosing savepoints are released (not rolled back). For +after_rollback+, this will run the hook after any enclosing savepoint is rolled back (before transaction commit), or after the transaction is rolled back if all enclosing savepoints are released:

  x = nil
  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.after_commit(savepoint: true){x = 1}
      DB.after_rollback(savepoint: true){x = 2}
      x # nil
    end # RELEASE SAVEPOINT
    x # nil
  end # COMMIT
  x # 1
  
  x = nil
  DB.transaction do  # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.after_commit(savepoint: true){x = 1}
      DB.after_rollback(savepoint: true){x = 2}
      x # nil
      raise Sequel::Rollback
    end # ROLLBACK TO SAVEPOINT
    x # 2
  end # COMMIT
  x # 2
  
  x = nil
  DB.transaction do # BEGIN
    DB.transaction(savepoint: true) do # SAVEPOINT
      DB.after_commit(savepoint: true){x = 1}
      DB.after_rollback(savepoint: true){x = 2}
    end # RELEASE SAVEPOINT
    x # nil
    raise Sequel::Rollback
  end
  x # 2

== Prepared Transactions / Two-Phase Commit

Sequel supports database prepared transactions on PostgreSQL, MySQL, and H2.  With prepared transactions, at the end of the transaction, the transaction is not immediately committed (it acts like a rollback).  Later, you can call +commit_prepared_transaction+ to commit the transaction or +rollback_prepared_transaction+ to roll the transaction back.  Prepared transactions are usually used with distributed databases to make sure all databases commit the same transaction or none of them do.

To use prepared transactions in Sequel, you provide a string as the value of the :prepare option:

  DB.transaction(prepare: 'foo') do # BEGIN
    DB[:foo].insert(1) # INSERT
  end # PREPARE TRANSACTION 'foo'
   
Later, you can commit the prepared transaction:

  DB.commit_prepared_transaction('foo')

or roll the prepared transaction back:

  DB.rollback_prepared_transaction('foo')

== Transaction Isolation Levels

The SQL standard supports 4 isolation levels: READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE.  Not all databases implement the levels as specified in the standard (or implement the levels at all), but on most databases, you can specify which transaction isolation level you want to use via the :isolation option to <tt>Database#transaction</tt>.  The isolation level is specified as one of the following symbols: :uncommitted, :committed, :repeatable, and :serializable.  Using this option makes Sequel use the correct transaction isolation syntax for your database:

  DB.transaction(isolation: :serializable) do # BEGIN
    # SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    DB[:foo].insert(1) # INSERT
  end # COMMIT

== Automatically Restarting Transactions

Sequel offers the ability to automatically restart transactions if specific types of errors are detected.  For example, if you want to automatically restart a transaction if a serialization failure is detected:

  DB.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
    ModelClass.find_or_create(name: 'Foo')
  end

At the serializable transaction isolation level, find_or_create may raises a Sequel::SerializationFailure exception if multiple threads simultaneously run that code.  With the :retry_on option set, the transaction will be automatically retried until it succeeds.

Note that automatic retrying should not be used unless the entire transaction
block is idempotent, as otherwise it can cause non-idempotent
behavior to execute multiple times.  For example, with the following code:

  DB.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
    logger.info 'Ensuring existence of ModelClass with name Foo'
    ModelClass.find_or_create(name: 'Foo')
  end

The logger.info method will be called multiple times if there is a serialization failure.

The :num_retries option can be used to set the maximum number of times to retry.  It is set to 5 times by default.