123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- 'use strict';
- /**
- * 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.
- *
- * @class Transaction
- * @see {@link Sequelize.transaction}
- */
- class Transaction {
- /**
- * Creates a new transaction instance
- *
- * @param {Sequelize} sequelize A configured sequelize Instance
- * @param {object} options An object with options
- * @param {string} [options.type] Sets the type of the transaction. Sqlite only
- * @param {string} [options.isolationLevel] Sets the isolation level of the transaction.
- * @param {string} [options.deferrable] Sets the constraints to be deferred or immediately checked. PostgreSQL only
- */
- constructor(sequelize, options) {
- this.sequelize = sequelize;
- this.savepoints = [];
- this._afterCommitHooks = [];
- // get dialect specific transaction options
- const generateTransactionId = this.sequelize.dialect.queryGenerator.generateTransactionId;
- this.options = {
- type: sequelize.options.transactionType,
- isolationLevel: sequelize.options.isolationLevel,
- readOnly: false,
- ...options
- };
- this.parent = this.options.transaction;
- 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
- *
- * @returns {Promise}
- */
- async commit() {
- if (this.finished) {
- throw new Error(`Transaction cannot be committed because it has been finished with state: ${this.finished}`);
- }
- try {
- return await this.sequelize.getQueryInterface().commitTransaction(this, this.options);
- } finally {
- this.finished = 'commit';
- this.cleanup();
- for (const hook of this._afterCommitHooks) {
- await hook.apply(this, [this]);
- }
- }
- }
- /**
- * Rollback (abort) the transaction
- *
- * @returns {Promise}
- */
- async rollback() {
- if (this.finished) {
- throw new Error(`Transaction cannot be rolled back because it has been finished with state: ${this.finished}`);
- }
- if (!this.connection) {
- throw new Error('Transaction cannot be rolled back because it never started');
- }
- try {
- return await this
- .sequelize
- .getQueryInterface()
- .rollbackTransaction(this, this.options);
- } finally {
- this.cleanup();
- }
- }
- /**
- * Called to acquire a connection to use and set the correct options on the connection.
- * We should ensure all of the environment that's set up is cleaned up in `cleanup()` below.
- *
- * @param {boolean} useCLS Defaults to true: Use CLS (Continuation Local Storage) with Sequelize. With CLS, all queries within the transaction callback will automatically receive the transaction object.
- * @returns {Promise}
- */
- async prepareEnvironment(useCLS) {
- let connectionPromise;
- if (useCLS === undefined) {
- useCLS = true;
- }
- if (this.parent) {
- connectionPromise = Promise.resolve(this.parent.connection);
- } else {
- const acquireOptions = { uuid: this.id };
- if (this.options.readOnly) {
- acquireOptions.type = 'SELECT';
- }
- connectionPromise = this.sequelize.connectionManager.getConnection(acquireOptions);
- }
- let result;
- const connection = await connectionPromise;
- this.connection = connection;
- this.connection.uuid = this.id;
- try {
- await this.begin();
- result = await this.setDeferrable();
- } catch (setupErr) {
- try {
- result = await this.rollback();
- } finally {
- throw setupErr; // eslint-disable-line no-unsafe-finally
- }
- }
- if (useCLS && this.sequelize.constructor._cls) {
- this.sequelize.constructor._cls.set('transaction', this);
- }
- return result;
- }
- async setDeferrable() {
- if (this.options.deferrable) {
- return await this
- .sequelize
- .getQueryInterface()
- .deferConstraints(this, this.options);
- }
- }
- async begin() {
- const queryInterface = this.sequelize.getQueryInterface();
- if ( this.sequelize.dialect.supports.settingIsolationLevelDuringTransaction ) {
- await queryInterface.startTransaction(this, this.options);
- return queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
- }
- await queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
- return queryInterface.startTransaction(this, this.options);
- }
- cleanup() {
- // Don't release the connection if there's a parent transaction or
- // if we've already cleaned up
- if (this.parent || this.connection.uuid === undefined) return;
- this._clearCls();
- 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:
- *
- * @example
- * try {
- * await sequelize.transaction({ type: Sequelize.Transaction.TYPES.EXCLUSIVE }, transaction => {
- * // your transactions
- * });
- * // 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'
- };
- }
- /**
- * Isolation levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`.
- * Sequelize uses the default isolation level of the database, you can override this by passing `options.isolationLevel` in Sequelize constructor options.
- *
- * Pass in the desired level as the first argument:
- *
- * @example
- * try {
- * const result = await sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => {
- * // your transactions
- * });
- * // 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:
- *
- * @example
- * // t1 is a transaction
- * Model.findAll({
- * where: ...,
- * transaction: t1,
- * lock: t1.LOCK...
- * });
- *
- * @example <caption>Postgres also supports specific locks while eager loading by using OF:</caption>
- * UserModel.findAll({
- * where: ...,
- * include: [TaskModel, ...],
- * transaction: t1,
- * lock: {
- * level: t1.LOCK...,
- * of: UserModel
- * }
- * });
- *
- * # UserModel will be locked but TaskModel won't!
- *
- * @example <caption>You can also skip locked rows:</caption>
- * // t1 is a transaction
- * Model.findAll({
- * where: ...,
- * transaction: t1,
- * lock: true,
- * skipLocked: true
- * });
- * # The query will now return any rows that aren't locked by another transaction
- *
- * @returns {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'
- };
- }
- /**
- * Please see {@link Transaction.LOCK}
- */
- get LOCK() {
- return Transaction.LOCK;
- }
- }
- module.exports = Transaction;
- module.exports.Transaction = Transaction;
- module.exports.default = Transaction;
|