You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
8.4 KiB
323 lines
8.4 KiB
'use strict';
|
|
|
|
const _ = require('lodash');
|
|
const Utils = require('./utils');
|
|
|
|
/**
|
|
* The transaction object is used to identify a running transaction. It is created by calling `Sequelize.transaction()`.
|
|
*
|
|
* To run a query under a transaction, you should pass the transaction in the options object.
|
|
*
|
|
* @see {@link Sequelize.transaction}
|
|
*/
|
|
class Transaction {
|
|
/**
|
|
* @param {Sequelize} sequelize A configured sequelize Instance
|
|
* @param {Object} options An object with options
|
|
* @param {Boolean} options.autocommit Sets the autocommit property of the transaction.
|
|
* @param {String} options.type=true Sets the type of the transaction.
|
|
* @param {String} options.isolationLevel=true Sets the isolation level of the transaction.
|
|
* @param {String} options.deferrable Sets the constraints to be deferred or immediately checked.
|
|
*/
|
|
constructor(sequelize, options) {
|
|
this.sequelize = sequelize;
|
|
this.savepoints = [];
|
|
this._afterCommitHooks = [];
|
|
|
|
// get dialect specific transaction options
|
|
const transactionOptions = sequelize.dialect.supports.transactionOptions || {};
|
|
const generateTransactionId = this.sequelize.dialect.QueryGenerator.generateTransactionId;
|
|
|
|
this.options = _.extend({
|
|
autocommit: transactionOptions.autocommit || null,
|
|
type: sequelize.options.transactionType,
|
|
isolationLevel: sequelize.options.isolationLevel,
|
|
readOnly: false
|
|
}, options || {});
|
|
|
|
this.parent = this.options.transaction;
|
|
this.id = this.parent ? this.parent.id : generateTransactionId();
|
|
|
|
if (this.parent) {
|
|
this.id = this.parent.id;
|
|
this.parent.savepoints.push(this);
|
|
this.name = this.id + '-sp-' + this.parent.savepoints.length;
|
|
} else {
|
|
this.id = this.name = generateTransactionId();
|
|
}
|
|
|
|
delete this.options.transaction;
|
|
}
|
|
|
|
/**
|
|
* Commit the transaction
|
|
*
|
|
* @return {Promise}
|
|
*/
|
|
commit() {
|
|
|
|
if (this.finished) {
|
|
return Utils.Promise.reject(new Error('Transaction cannot be committed because it has been finished with state: ' + this.finished));
|
|
}
|
|
|
|
this._clearCls();
|
|
|
|
return this
|
|
.sequelize
|
|
.getQueryInterface()
|
|
.commitTransaction(this, this.options)
|
|
.finally(() => {
|
|
this.finished = 'commit';
|
|
if (!this.parent) {
|
|
return this.cleanup();
|
|
}
|
|
return null;
|
|
}).tap(
|
|
() => Utils.Promise.each(
|
|
this._afterCommitHooks,
|
|
hook => Promise.resolve(hook.apply(this, [this])))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rollback (abort) the transaction
|
|
*
|
|
* @return {Promise}
|
|
*/
|
|
rollback() {
|
|
|
|
if (this.finished) {
|
|
return Utils.Promise.reject(new Error('Transaction cannot be rolled back because it has been finished with state: ' + this.finished));
|
|
}
|
|
|
|
if (!this.connection) {
|
|
return Promise.reject(new Error('Transaction cannot be rolled back because it never started'));
|
|
}
|
|
|
|
this._clearCls();
|
|
|
|
return this
|
|
.sequelize
|
|
.getQueryInterface()
|
|
.rollbackTransaction(this, this.options)
|
|
.finally(() => {
|
|
if (!this.parent) {
|
|
return this.cleanup();
|
|
}
|
|
return this;
|
|
});
|
|
}
|
|
|
|
prepareEnvironment(useCLS) {
|
|
let connectionPromise;
|
|
|
|
if (typeof useCLS === 'undefined') {
|
|
useCLS = true;
|
|
}
|
|
|
|
if (this.parent) {
|
|
connectionPromise = Utils.Promise.resolve(this.parent.connection);
|
|
} else {
|
|
const acquireOptions = { uuid: this.id };
|
|
if (this.options.readOnly) {
|
|
acquireOptions.type = 'SELECT';
|
|
}
|
|
connectionPromise = this.sequelize.connectionManager.getConnection(acquireOptions);
|
|
}
|
|
|
|
return connectionPromise
|
|
.then(connection => {
|
|
this.connection = connection;
|
|
this.connection.uuid = this.id;
|
|
})
|
|
.then(() => {
|
|
return this.begin()
|
|
.then(() => this.setDeferrable())
|
|
.then(() => this.setIsolationLevel())
|
|
.then(() => this.setAutocommit())
|
|
.catch(setupErr => this.rollback().finally(() => {
|
|
throw setupErr;
|
|
}));
|
|
})
|
|
.tap(() => {
|
|
if (useCLS && this.sequelize.constructor._cls) {
|
|
this.sequelize.constructor._cls.set('transaction', this);
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
begin() {
|
|
return this
|
|
.sequelize
|
|
.getQueryInterface()
|
|
.startTransaction(this, this.options);
|
|
}
|
|
|
|
setDeferrable() {
|
|
if (this.options.deferrable) {
|
|
return this
|
|
.sequelize
|
|
.getQueryInterface()
|
|
.deferConstraints(this, this.options);
|
|
}
|
|
}
|
|
|
|
setAutocommit() {
|
|
return this
|
|
.sequelize
|
|
.getQueryInterface()
|
|
.setAutocommit(this, this.options.autocommit, this.options);
|
|
}
|
|
|
|
setIsolationLevel() {
|
|
return this
|
|
.sequelize
|
|
.getQueryInterface()
|
|
.setIsolationLevel(this, this.options.isolationLevel, this.options);
|
|
}
|
|
|
|
cleanup() {
|
|
const res = this.sequelize.connectionManager.releaseConnection(this.connection);
|
|
this.connection.uuid = undefined;
|
|
return res;
|
|
}
|
|
|
|
_clearCls() {
|
|
const cls = this.sequelize.constructor._cls;
|
|
|
|
if (cls) {
|
|
if (cls.get('transaction') === this) {
|
|
cls.set('transaction', null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A hook that is run after a transaction is committed
|
|
*
|
|
* @param {Function} fn A callback function that is called with the committed transaction
|
|
* @name afterCommit
|
|
* @memberof Sequelize.Transaction
|
|
*/
|
|
afterCommit(fn) {
|
|
if (!fn || typeof fn !== 'function') {
|
|
throw new Error('"fn" must be a function');
|
|
}
|
|
this._afterCommitHooks.push(fn);
|
|
}
|
|
|
|
/**
|
|
* Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
|
|
* Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
|
|
* Sqlite only.
|
|
*
|
|
* Pass in the desired level as the first argument:
|
|
*
|
|
* ```js
|
|
* return sequelize.transaction({type: Sequelize.Transaction.TYPES.EXCLUSIVE}, transaction => {
|
|
*
|
|
* // your transactions
|
|
*
|
|
* }).then(result => {
|
|
* // transaction has been committed. Do something after the commit if required.
|
|
* }).catch(err => {
|
|
* // do something with the err.
|
|
* });
|
|
* ```
|
|
* @property DEFERRED
|
|
* @property IMMEDIATE
|
|
* @property EXCLUSIVE
|
|
*/
|
|
static get TYPES() {
|
|
return {
|
|
DEFERRED: 'DEFERRED',
|
|
IMMEDIATE: 'IMMEDIATE',
|
|
EXCLUSIVE: 'EXCLUSIVE'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Isolations levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`.
|
|
* Default to `REPEATABLE_READ` but you can override the default isolation level by passing `options.isolationLevel` in `new Sequelize`.
|
|
*
|
|
* Pass in the desired level as the first argument:
|
|
*
|
|
* ```js
|
|
* return sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => {
|
|
*
|
|
* // your transactions
|
|
*
|
|
* }).then(result => {
|
|
* // transaction has been committed. Do something after the commit if required.
|
|
* }).catch(err => {
|
|
* // do something with the err.
|
|
* });
|
|
* ```
|
|
* @property READ_UNCOMMITTED
|
|
* @property READ_COMMITTED
|
|
* @property REPEATABLE_READ
|
|
* @property SERIALIZABLE
|
|
*/
|
|
static get ISOLATION_LEVELS() {
|
|
return {
|
|
READ_UNCOMMITTED: 'READ UNCOMMITTED',
|
|
READ_COMMITTED: 'READ COMMITTED',
|
|
REPEATABLE_READ: 'REPEATABLE READ',
|
|
SERIALIZABLE: 'SERIALIZABLE'
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Possible options for row locking. Used in conjunction with `find` calls:
|
|
*
|
|
* ```js
|
|
* t1 // is a transaction
|
|
* Model.findAll({
|
|
* where: ...,
|
|
* transaction: t1,
|
|
* lock: t1.LOCK...
|
|
* });
|
|
* ```
|
|
*
|
|
* Postgres also supports specific locks while eager loading by using OF:
|
|
* ```js
|
|
* UserModel.findAll({
|
|
* where: ...,
|
|
* include: [TaskModel, ...],
|
|
* transaction: t1,
|
|
* lock: {
|
|
* level: t1.LOCK...,
|
|
* of: UserModel
|
|
* }
|
|
* });
|
|
* ```
|
|
* UserModel will be locked but TaskModel won't!
|
|
*
|
|
* @return {Object}
|
|
* @property UPDATE
|
|
* @property SHARE
|
|
* @property KEY_SHARE Postgres 9.3+ only
|
|
* @property NO_KEY_UPDATE Postgres 9.3+ only
|
|
*/
|
|
static get LOCK() {
|
|
return {
|
|
UPDATE: 'UPDATE',
|
|
SHARE: 'SHARE',
|
|
KEY_SHARE: 'KEY SHARE',
|
|
NO_KEY_UPDATE: 'NO KEY UPDATE'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @see {@link Transaction.LOCK}
|
|
*/
|
|
get LOCK() {
|
|
return Transaction.LOCK;
|
|
}
|
|
}
|
|
|
|
module.exports = Transaction;
|
|
module.exports.Transaction = Transaction;
|
|
module.exports.default = Transaction;
|
|
|