不要怂,就是干,撸起袖子干!

Commit f4b3517d by Jan Aagaard Meier

Changed CLS to only work for autocall and added docs

1 parent 5e794684
# Next
- [FEATURE] CLS Support. CLS is also used to automatically pass the transaction to any calls within the callback chain when using `sequelize.transaction(function() ...`.
- [BUG] Fixed issue with paranoid deletes and `deletedAt` with a custom field.
- [BUG] No longer crahes on `where: []`
......
## Transactions
Sequelize supports two ways of using transactions:
Sequelize supports two ways of using transactions, one will automatically commit or rollback the transaction based on a promise chain and the other leaves it up to the user.
* One which will automatically commit or rollback the transaction based on the result of a promise chain and, (if enabled) pass the transaction to all calls within the callback
* And one which leaves committing, rolling back and passing the transaction to the user.
The key difference is that the managed transaction uses a callback that expects a promise to be returned to it while the unmanaged transaction returns a promise.
### Auto commit/rollback
# Managed transaction (auto-callback)
```js
return sequelize.transaction(function (t) {
return User.create({
......@@ -14,18 +15,69 @@ return sequelize.transaction(function (t) {
return user.setShooter({
firstName: 'John',
lastName: 'Boothe'
}, {transction: t});
}, {transaction: t});
});
}).then(function (result) {
// Transaction has been committed
// result is whatever the result of the promise chain returned to the transaction callback is
// result is whatever the result of the promise chain returned to the transaction callback
}).catch(function (err) {
// Transaction has been rolled back
// err is whatever rejected the promise chain returned to the transaction callback is
// err is whatever rejected the promise chain returned to the transaction callback
});
```
In the example above, the transaction is still manually passed, by passing `{ transaction: t }` as the second argument. To automatically pass the transaction to all queries you must install the [continuation local storage](https://github.com/othiym23/node-continuation-local-storage) (CLS) module and instantiate a namespace in your own code:
```js
var cls = require('continuation-local-storage'),
namespace = cls.createNamespace('my-very-own-namespace');
```
To enable CLS you must tell sequelize which namespace to use by setting it as a property on the sequelize constructor:
```js
var Sequelize = require('sequelize');
Sequelize.cls = namespace;
new Sequelize(....);
```
Notice, that the `cls` property must be set on the *constructor*, not on an instance of sequelize. This means that all instances will share the same namespace, and that CLS is all-or-nothing - you cannot enable it only for some instances.
CLS works like a thread-local storage for callbacks. What this means in practice is, that different callback chains can access local variables by using the CLS namespace. When CLS is enabled sequelize will set the `transaction` property on the namespace when a new transaction is created. Since variables set within a callback chain are private to that chain several concurrent transactions can exist at the same time:
```js
sequelize.transaction(function (t1) {
namespace.get('transaction') === t1;
});
sequelize.transaction(function (t2) {
namespace.get('transaction') === t2;
});
```
In most case you won't need to access `namespace.get('transaction')` directly, since all queries will automatically look for a transaction on the namespace:
```js
sequelize.transaction(function (t1) {
// With CLS enabled, the user will be created inside the transaction
User.create({ name: 'Alice' });
});
```
If you want to execute queries inside the callback without using the transaction you can pass `{ transaction: null }`, or another transaction if you have several concurrent ones:
```js
sequelize.transaction(function (t1) {
sequelize.transaction(function (t2) {
// By default queries here will use t2
User.create({ name: 'Bob' }, { transaction: null });
User.create({ name: 'Mallory' }, { transaction: t1 });
});
});
```
### Handled manually
# Unmanaged transaction (then-callback)
```js
return sequelize.transaction().then(function (t) {
return User.create({
......@@ -44,8 +96,8 @@ return sequelize.transaction().then(function (t) {
});
```
### Using transactions with other sequelize methods
# Using transactions with other sequelize methods
The `transaction` option goes with most other options, which are usually the first argument of a method.
For methods that take values, like `.create`, `.update()`, `.updateAttributes()` and more `transaction` should be passed to the option in the second argument.
If unsure, refer to the api documentation for the method you are using to be sure of the signature.
For methods that take values, like `.create`, `.update()`, `.updateAttributes()` etc. `transaction` should be passed to the option in the second argument.
If unsure, refer to the API documentation for the method you are using to be sure of the signature.
......@@ -815,7 +815,7 @@ module.exports = (function() {
* @param {String|Array|Object} fields If a string is provided, that column is incremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is incremented by the value given
* @param {Object} [options]
* @param {Integer} [options.by=1] The number to increment by
* @param {Transaction} [options.transaction=null]
* @param {Transaction} [options.transaction]
*
* @return {Promise}
*/
......@@ -880,7 +880,7 @@ module.exports = (function() {
* @param {String|Array|Object} fields If a string is provided, that column is decremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is decremented by the value given
* @param {Object} [options]
* @param {Integer} [options.by=1] The number to decrement by
* @param {Transaction} [options.transaction=null]
* @param {Transaction} [options.transaction]
*
* @return {Promise}
*/
......
......@@ -677,9 +677,6 @@ module.exports = (function() {
, tableNames = { };
tableNames[this.getTableName(options)] = true;
var cls = require('continuation-local-storage')
, sequelize_cls = cls.getNamespace('sequelize');
options = optClone(options || {});
options = Utils._.defaults(options, {
hooks: true
......
......@@ -50,8 +50,8 @@ for (var method in Promise) {
var bluebirdThen = Promise.prototype._then;
Promise.prototype._then = function (didFulfill, didReject, didProgress, receiver, internalData) {
if (SequelizePromise.prototype.sequelize.options.namespace) {
var ns = SequelizePromise.prototype.sequelize.options.namespace;
if (SequelizePromise.Sequelize.cls) {
var ns = SequelizePromise.Sequelize.cls;
if (typeof didFulfill === 'function') didFulfill = ns.bind(didFulfill);
if (typeof didReject === 'function') didReject = ns.bind(didReject);
if (typeof didProgress === 'function') didProgress = ns.bind(didProgress);
......
......@@ -199,8 +199,6 @@ module.exports = (function() {
this.importCache = {};
Sequelize.runHooks('afterInit', this);
Promise.prototype.sequelize = this;
};
Sequelize.options = {hooks: {}};
......@@ -630,7 +628,7 @@ module.exports = (function() {
* @param {Instance} [callee] If callee is provided, the returned data will be put into the callee
* @param {Object} [options={}] Query options.
* @param {Boolean} [options.raw] If true, sequelize will not try to format the results of the query, or build an instance of a model from the result
* @param {Transaction} [options.transaction=null] The transaction that the query should be executed under
* @param {Transaction} [options.transaction] The transaction that the query should be executed under
* @param {String} [options.type='SELECT'] The type of query you are executing. The query type affects how results are formatted before they are passed back. If no type is provided sequelize will try to guess the right type based on the sql, and fall back to SELECT. The type is a string, but `Sequelize.QueryTypes` is provided is convenience shortcuts. Current options are SELECT, BULKUPDATE and BULKDELETE
* @param {Boolean} [options.nest=false] If true, transforms objects with `.` separated property names into nested objects using [dottie.js](https://github.com/mickhansen/dottie.js). For example { 'user.username': 'john' } becomes { user: { username: 'john' }}
* @param {Object|Array} [replacements] Either an object of named parameter replacements in the format `:param` or an array of unnamed replacements to replace `?` in your SQL.
......@@ -662,12 +660,8 @@ module.exports = (function() {
type: (sql.toLowerCase().indexOf('select') === 0) ? QueryTypes.SELECT : false
});
if (
(!options.transaction && options.transaction !== null)
&& this.options.namespace
&& this.options.namespace.get('transaction')
) {
options.transaction = this.options.namespace.get('transaction');
if (options.transaction === undefined && Sequelize.cls) {
options.transaction = Sequelize.cls.get('transaction');
}
if (options.transaction && options.transaction.finished) {
......@@ -1026,6 +1020,17 @@ module.exports = (function() {
* });
* ```
*
* If you have [CLS](https://github.com/othiym23/node-continuation-local-storage) enabled, the transaction will automatically be passed to any query that runs witin the callback.
* To enable CLS, add it do your project, create a namespace and set it on the sequelize constructor:
*
* ```js
* var cls = require('continuation-local-storage'),
* ns = cls.createNamespace('....');
* var Sequelize = require('sequelize');
* Sequelize.cls = ns;
* ```
* Note, that CLS is enabled for all sequelize instances, and all instances will share the same namespace
*
* @see {Transaction}
* @param {Object} [options={}]
......@@ -1042,7 +1047,7 @@ module.exports = (function() {
}
var transaction = new Transaction(this, options)
, ns = this.options.namespace;
, ns = Sequelize.cls;
if (autoCallback) {
deprecated('Note: When passing a callback to a transaction a promise chain is expected in return, the transaction will be committed or rejected based on the promise chain returned to the callback.');
......@@ -1075,21 +1080,6 @@ module.exports = (function() {
}
return new Promise(transactionResolver);
} else if (ns) {
var context = ns.createContext();
return ns.bind(function () {
var ret = transaction.prepareEnvironment().return(transaction);
ret.then = function (didFulfill, didReject, didProgress) {
// We manually pass a context here, because the right context has to be available even though the .then callback is not strictly within the same callback chain
didFulfill = ns.bind(didFulfill, context);
return Sequelize.Promise.prototype.then.call(this, didFulfill, didReject, didProgress);
};
return ret;
}, context)();
} else {
return transaction.prepareEnvironment().return(transaction);
}
......@@ -1129,5 +1119,7 @@ module.exports = (function() {
this.connectionManager.close();
};
// Allows the promise to access cls namespaces
Promise.Sequelize = Sequelize;
return Sequelize;
})();
......@@ -119,8 +119,8 @@ Transaction.prototype.prepareEnvironment = function() {
self.connection = connection;
self.connection.uuid = self.id;
if (self.sequelize.options.namespace) {
self.sequelize.options.namespace.set('transaction', self);
if (self.sequelize.constructor.cls) {
self.sequelize.constructor.cls.set('transaction', self);
}
}).then(function () {
return self.begin();
......
......@@ -12,11 +12,14 @@ var chai = require('chai')
chai.config.includeStack = true;
describe(Support.getTestDialectTeaser("Continuation local storage"), function () {
if (current.dialect.supports.transactions) {
describe(Support.getTestDialectTeaser("Continuation local storage"), function () {
before(function () {
this.sequelize = Support.createSequelizeInstance({
namespace: cls.createNamespace('sequelize')
Sequelize.cls = cls.createNamespace('sequelize');
});
after(function () {
delete Sequelize.cls;
});
beforeEach(function () {
......@@ -32,31 +35,17 @@ describe(Support.getTestDialectTeaser("Continuation local storage"), function ()
});
});
var autoCallback = function autoCallback(sequelize, cb) {
return sequelize.transaction(cb);
};
var thenCallback = function thenCallback(sequelize, cb) {
return sequelize.transaction().then(function (t) {
cb().then(function () {
t.commit();
});
});
};
if (current.dialect.supports.transactions) {
[autoCallback, thenCallback].forEach(function (cb) {
describe(cb.name, function () {
describe('context', function () {
it('supports several concurrent transactions', function () {
var t1id, t2id, self = this;
return Promise.join(
cb(this.sequelize, function () {
this.sequelize.transaction(function () {
t1id = self.ns.get('transaction').id;
return Promise.resolve();
}),
cb(this.sequelize, function () {
this.sequelize.transaction(function () {
t2id = self.ns.get('transaction').id;
return Promise.resolve();
......@@ -72,7 +61,7 @@ describe(Support.getTestDialectTeaser("Continuation local storage"), function ()
it('supports nested promise chains', function () {
var self = this;
return cb(this.sequelize, function () {
return this.sequelize.transaction(function () {
var tid = self.ns.get('transaction').id;
return self.User.findAll().then(function () {
......@@ -90,7 +79,7 @@ describe(Support.getTestDialectTeaser("Continuation local storage"), function ()
, transactionSetup = false
, transactionEnded = false;
cb(this.sequelize, function () {
this.sequelize.transaction(function () {
transactionSetup = true;
return Promise.delay(500).then(function () {
......@@ -118,7 +107,7 @@ describe(Support.getTestDialectTeaser("Continuation local storage"), function ()
});
it('does not leak variables to the following promise chain', function () {
return cb(this.sequelize, function () {
return this.sequelize.transaction(function () {
return Promise.resolve();
}).bind(this).then(function () {
expect(this.ns.get('transaction')).not.to.be.ok;
......@@ -129,7 +118,7 @@ describe(Support.getTestDialectTeaser("Continuation local storage"), function ()
describe('sequelize.query integration', function () {
it('automagically uses the transaction in all calls', function () {
var self = this;
return cb(this.sequelize, function () {
return this.sequelize.transaction(function () {
return self.User.create({ name: 'bob' }).then(function () {
return Promise.all([
expect(self.User.findAll({}, { transaction: null })).to.eventually.have.length(0),
......@@ -140,6 +129,4 @@ describe(Support.getTestDialectTeaser("Continuation local storage"), function ()
});
});
});
});
}
});
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!