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

Commit 3eff6cea by Jan Aagaard Meier

Merge pull request #1777 from sequelize/forUpdate

Support for FOR UPDATE and FOR SHARE queries
2 parents 7961c7ca 45c5c8fa
......@@ -956,6 +956,14 @@ module.exports = (function() {
query = mainQueryItems.join('')
}
if (options.lock && this._dialect.supports.lock) {
if (options.lock === 'SHARE') {
query += ' ' + this._dialect.supports.forShare
} else {
query += ' FOR UPDATE'
}
}
query += ";";
return query
......
......@@ -7,7 +7,9 @@ var MysqlDialect = function(sequelize) {
MysqlDialect.prototype.supports = _.defaults({
'VALUES ()': true,
'LIMIT ON UPDATE':true
'LIMIT ON UPDATE':true,
lock: true,
forShare: 'LOCK IN SHARE MODE'
}, Abstract.prototype.supports)
module.exports = MysqlDialect
......@@ -8,7 +8,9 @@ var PostgresDialect = function(sequelize) {
PostgresDialect.prototype.supports = _.defaults({
'RETURNING': true,
'DEFAULT VALUES': true,
schemas: true
schemas: true,
lock: true,
forShare: 'FOR SHARE',
}, Abstract.prototype.supports)
module.exports = PostgresDialect
......@@ -643,6 +643,7 @@ module.exports = (function() {
* @param {Number} [options.offset]
* @param {Object} [queryOptions] Set the query options, e.g. raw, specifying that you want raw data instead of built Instances. See sequelize.query for options
* @param {Transaction} [queryOptions.transaction]
* @param {String} [queryOptions.lock] Lock the selected rows in either share or update mode. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE
*
* @see {Sequelize#query}
* @return {Promise<Array<Instance>>}
......
......@@ -552,6 +552,8 @@ module.exports = (function() {
})
}
options.lock = queryOptions.lock
var sql = this.QueryGenerator.selectQuery(tableName, options, factory)
queryOptions = Utils._.extend({}, queryOptions, {
include: options.include,
......
......@@ -40,6 +40,26 @@ Transaction.ISOLATION_LEVELS = {
}
/**
* Possible options for row locking. Used in conjuction with `find` calls:
*
* ```js
* t1 // is a transaction
* Model.findAll({
* where: ...
* }, {
* transaction: t1,
* lock: t1.LOCK.UPDATE,
* lock: t1.LOCK.SHARE
* })
* ```
* @property LOCK
*/
Transaction.LOCK = Transaction.prototype.LOCK = {
UPDATE: 'UPDATE',
SHARE: 'SHARE'
}
/**
* Commit the transaction
*
* @return {this}
......
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/support')
, dialect = Support.getTestDialect()
, Transaction = require(__dirname + '/../lib/transaction')
, sinon = require('sinon')
describe(Support.getTestDialectTeaser("Transaction"), function () {
this.timeout(4000);
......@@ -71,4 +74,120 @@ describe(Support.getTestDialectTeaser("Transaction"), function () {
})
})
})
if (dialect !== 'sqlite') {
describe('row locking', function () {
this.timeout(10000);
it('supports for update', function (done) {
var User = this.sequelize.define('user', {
username: Support.Sequelize.STRING,
awesome: Support.Sequelize.BOOLEAN
})
, self = this
, t1Spy = sinon.spy()
, t2Spy = sinon.spy()
this.sequelize.sync({ force: true }).then(function () {
return User.create({ username: 'jan'})
}).then(function () {
self.sequelize.transaction(function (t1) {
return User.find({
where: {
username: 'jan'
}
}, {
lock: t1.LOCK.UPDATE,
transaction: t1
}).then(function (t1Jan) {
self.sequelize.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED
}, function (t2) {
User.find({
where: {
username: 'jan'
},
}, {
lock: t2.LOCK.UPDATE,
transaction: t2
}).then(function () {
t2Spy()
t2.commit().then(function () {
expect(t2Spy).to.have.been.calledAfter(t1Spy) // Find should not succeed before t1 has comitted
done()
})
})
t1Jan.updateAttributes({
awesome: true
}, { transaction: t1}).then(function () {
t1Spy()
setTimeout(t1.commit.bind(t1), 2000)
})
})
})
})
})
})
it('supports for share', function (done) {
var User = this.sequelize.define('user', {
username: Support.Sequelize.STRING,
awesome: Support.Sequelize.BOOLEAN
})
, self = this
, t1Spy = sinon.spy()
, t2FindSpy = sinon.spy()
, t2UpdateSpy = sinon.spy()
this.sequelize.sync({ force: true }).then(function () {
return User.create({ username: 'jan'})
}).then(function () {
self.sequelize.transaction(function (t1) {
return User.find({
where: {
username: 'jan'
}
}, {
lock: t1.LOCK.SHARE,
transaction: t1
}).then(function (t1Jan) {
self.sequelize.transaction({
isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED
}, function (t2) {
User.find({
where: {
username: 'jan'
}
}, { transaction: t2}).then(function (t2Jan) {
t2FindSpy()
t2Jan.updateAttributes({
awesome: false
}, {
transaction: t2
}).then(function () {
t2UpdateSpy()
t2.commit().then(function () {
expect(t2FindSpy).to.have.been.calledBefore(t1Spy) // The find call should have returned
expect(t2UpdateSpy).to.have.been.calledAfter(t1Spy) // But the update call should not happen before the first transaction has committed
done()
})
})
})
t1Jan.updateAttributes({
awesome: true
}, {
transaction: t1
}).then(function () {
t1Spy()
setTimeout(t1.commit.bind(t1), 2000)
})
})
})
})
})
})
})
}
})
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!