UNPKG

8.47 kBJavaScriptView Raw
1'use strict';
2
3const Promise = require('./promise');
4
5/**
6 * The transaction object is used to identify a running transaction.
7 * It is created by calling `Sequelize.transaction()`.
8 * To run a query under a transaction, you should pass the transaction in the options object.
9 *
10 * @class Transaction
11 * @see {@link Sequelize.transaction}
12 */
13class Transaction {
14 /**
15 * Creates a new transaction instance
16 *
17 * @param {Sequelize} sequelize A configured sequelize Instance
18 * @param {Object} options An object with options
19 * @param {string} [options.type] Sets the type of the transaction. Sqlite only
20 * @param {string} [options.isolationLevel] Sets the isolation level of the transaction.
21 * @param {string} [options.deferrable] Sets the constraints to be deferred or immediately checked. PostgreSQL only
22 */
23 constructor(sequelize, options) {
24 this.sequelize = sequelize;
25 this.savepoints = [];
26 this._afterCommitHooks = [];
27
28 // get dialect specific transaction options
29 const generateTransactionId = this.sequelize.dialect.QueryGenerator.generateTransactionId;
30
31 this.options = Object.assign({
32 type: sequelize.options.transactionType,
33 isolationLevel: sequelize.options.isolationLevel,
34 readOnly: false
35 }, options || {});
36
37 this.parent = this.options.transaction;
38
39 if (this.parent) {
40 this.id = this.parent.id;
41 this.parent.savepoints.push(this);
42 this.name = `${this.id}-sp-${this.parent.savepoints.length}`;
43 } else {
44 this.id = this.name = generateTransactionId();
45 }
46
47 delete this.options.transaction;
48 }
49
50 /**
51 * Commit the transaction
52 *
53 * @returns {Promise}
54 */
55 commit() {
56 if (this.finished) {
57 return Promise.reject(new Error(`Transaction cannot be committed because it has been finished with state: ${this.finished}`));
58 }
59
60 this._clearCls();
61
62 return this
63 .sequelize
64 .getQueryInterface()
65 .commitTransaction(this, this.options)
66 .finally(() => {
67 this.finished = 'commit';
68 if (!this.parent) {
69 return this.cleanup();
70 }
71 return null;
72 }).tap(
73 () => Promise.each(
74 this._afterCommitHooks,
75 hook => Promise.resolve(hook.apply(this, [this])))
76 );
77 }
78
79 /**
80 * Rollback (abort) the transaction
81 *
82 * @returns {Promise}
83 */
84 rollback() {
85 if (this.finished) {
86 return Promise.reject(new Error(`Transaction cannot be rolled back because it has been finished with state: ${this.finished}`));
87 }
88
89 if (!this.connection) {
90 return Promise.reject(new Error('Transaction cannot be rolled back because it never started'));
91 }
92
93 this._clearCls();
94
95 return this
96 .sequelize
97 .getQueryInterface()
98 .rollbackTransaction(this, this.options)
99 .finally(() => {
100 if (!this.parent) {
101 return this.cleanup();
102 }
103 return this;
104 });
105 }
106
107 prepareEnvironment(useCLS) {
108 let connectionPromise;
109
110 if (useCLS === undefined) {
111 useCLS = true;
112 }
113
114 if (this.parent) {
115 connectionPromise = Promise.resolve(this.parent.connection);
116 } else {
117 const acquireOptions = { uuid: this.id };
118 if (this.options.readOnly) {
119 acquireOptions.type = 'SELECT';
120 }
121 connectionPromise = this.sequelize.connectionManager.getConnection(acquireOptions);
122 }
123
124 return connectionPromise
125 .then(connection => {
126 this.connection = connection;
127 this.connection.uuid = this.id;
128 })
129 .then(() => {
130 return this.begin()
131 .then(() => this.setDeferrable())
132 .then(() => this.setIsolationLevel())
133 .catch(setupErr => this.rollback().finally(() => {
134 throw setupErr;
135 }));
136 })
137 .tap(() => {
138 if (useCLS && this.sequelize.constructor._cls) {
139 this.sequelize.constructor._cls.set('transaction', this);
140 }
141 return null;
142 });
143 }
144
145 begin() {
146 return this
147 .sequelize
148 .getQueryInterface()
149 .startTransaction(this, this.options);
150 }
151
152 setDeferrable() {
153 if (this.options.deferrable) {
154 return this
155 .sequelize
156 .getQueryInterface()
157 .deferConstraints(this, this.options);
158 }
159 }
160
161 setIsolationLevel() {
162 return this
163 .sequelize
164 .getQueryInterface()
165 .setIsolationLevel(this, this.options.isolationLevel, this.options);
166 }
167
168 cleanup() {
169 const res = this.sequelize.connectionManager.releaseConnection(this.connection);
170 this.connection.uuid = undefined;
171 return res;
172 }
173
174 _clearCls() {
175 const cls = this.sequelize.constructor._cls;
176
177 if (cls) {
178 if (cls.get('transaction') === this) {
179 cls.set('transaction', null);
180 }
181 }
182 }
183
184 /**
185 * A hook that is run after a transaction is committed
186 *
187 * @param {Function} fn A callback function that is called with the committed transaction
188 * @name afterCommit
189 * @memberof Sequelize.Transaction
190 */
191 afterCommit(fn) {
192 if (!fn || typeof fn !== 'function') {
193 throw new Error('"fn" must be a function');
194 }
195 this._afterCommitHooks.push(fn);
196 }
197
198 /**
199 * Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
200 * Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
201 * Sqlite only.
202 *
203 * Pass in the desired level as the first argument:
204 *
205 * @example
206 * return sequelize.transaction({type: Sequelize.Transaction.TYPES.EXCLUSIVE}, transaction => {
207 * // your transactions
208 * }).then(result => {
209 * // transaction has been committed. Do something after the commit if required.
210 * }).catch(err => {
211 * // do something with the err.
212 * });
213 *
214 * @property DEFERRED
215 * @property IMMEDIATE
216 * @property EXCLUSIVE
217 */
218 static get TYPES() {
219 return {
220 DEFERRED: 'DEFERRED',
221 IMMEDIATE: 'IMMEDIATE',
222 EXCLUSIVE: 'EXCLUSIVE'
223 };
224 }
225
226 /**
227 * Isolation levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`.
228 * Sequelize uses the default isolation level of the database, you can override this by passing `options.isolationLevel` in Sequelize constructor options.
229 *
230 * Pass in the desired level as the first argument:
231 *
232 * @example
233 * return sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => {
234 * // your transactions
235 * }).then(result => {
236 * // transaction has been committed. Do something after the commit if required.
237 * }).catch(err => {
238 * // do something with the err.
239 * });
240 *
241 * @property READ_UNCOMMITTED
242 * @property READ_COMMITTED
243 * @property REPEATABLE_READ
244 * @property SERIALIZABLE
245 */
246 static get ISOLATION_LEVELS() {
247 return {
248 READ_UNCOMMITTED: 'READ UNCOMMITTED',
249 READ_COMMITTED: 'READ COMMITTED',
250 REPEATABLE_READ: 'REPEATABLE READ',
251 SERIALIZABLE: 'SERIALIZABLE'
252 };
253 }
254
255
256 /**
257 * Possible options for row locking. Used in conjunction with `find` calls:
258 *
259 * @example
260 * // t1 is a transaction
261 * Model.findAll({
262 * where: ...,
263 * transaction: t1,
264 * lock: t1.LOCK...
265 * });
266 *
267 * @example <caption>Postgres also supports specific locks while eager loading by using OF:</caption>
268 * UserModel.findAll({
269 * where: ...,
270 * include: [TaskModel, ...],
271 * transaction: t1,
272 * lock: {
273 * level: t1.LOCK...,
274 * of: UserModel
275 * }
276 * });
277 *
278 * # UserModel will be locked but TaskModel won't!
279 *
280 * @example <caption>You can also skip locked rows:</caption>
281 * // t1 is a transaction
282 * Model.findAll({
283 * where: ...,
284 * transaction: t1,
285 * lock: true,
286 * skipLocked: true
287 * });
288 * # The query will now return any rows that aren't locked by another transaction
289 *
290 * @returns {Object}
291 * @property UPDATE
292 * @property SHARE
293 * @property KEY_SHARE Postgres 9.3+ only
294 * @property NO_KEY_UPDATE Postgres 9.3+ only
295 */
296 static get LOCK() {
297 return {
298 UPDATE: 'UPDATE',
299 SHARE: 'SHARE',
300 KEY_SHARE: 'KEY SHARE',
301 NO_KEY_UPDATE: 'NO KEY UPDATE'
302 };
303 }
304
305 /**
306 * Please see {@link Transaction.LOCK}
307 */
308 get LOCK() {
309 return Transaction.LOCK;
310 }
311}
312
313module.exports = Transaction;
314module.exports.Transaction = Transaction;
315module.exports.default = Transaction;