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

Commit 6b59d5be by Ruben Bridgewater

Add postgres feature: lock ... of table and new lock levels

1 parent 15fa7f3a
...@@ -221,7 +221,7 @@ The success listener is called with an array of instances if the query succeeds. ...@@ -221,7 +221,7 @@ The success listener is called with an array of instances if the query succeeds.
| [options.offset] | Number | | | [options.offset] | Number | |
| [options.transaction] | Transaction | | | [options.transaction] | Transaction | |
| [queryOptions] | Object | Set the query options, e.g. raw, specifying that you want raw data instead of built Instances. See sequelize.query for options | | [queryOptions] | Object | Set the query options, e.g. raw, specifying that you want raw data instead of built Instances. See sequelize.query for options |
| [queryOptions.lock] | String | Lock the selected rows in either share or update mode. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. See [transaction.LOCK for an example](https://github.com/sequelize/sequelize/wiki/API-Reference-Transaction#LOCK) | | [queryOptions.lock] | String|Object | Lock the selected rows. Possible options are transaction.LOCK.UPDATE and transaction.LOCK.SHARE. Postgres also supports transaction.LOCK.KEY_SHARE, transaction.LOCK.NO_KEY_UPDATE and specific model locks with joins. See [transaction.LOCK for an example](api/transaction#lock) |
__Aliases:__ all __Aliases:__ all
......
...@@ -36,10 +36,28 @@ Model.findAll({ ...@@ -36,10 +36,28 @@ Model.findAll({
}, { }, {
transaction: t1, transaction: t1,
lock: t1.LOCK.UPDATE, lock: t1.LOCK.UPDATE,
lock: t1.LOCK.SHARE lock: t1.LOCK.SHARE,
lock: t1.LOCK.KEY_SHARE, // Postgres 9.3+ only
lock: t1.LOCK.NO_KEY_SHARE // Postgres 9.3+ only
}) })
``` ```
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!
*** ***
<a name="commit"></a> <a name="commit"></a>
......
...@@ -1387,11 +1387,20 @@ module.exports = (function() { ...@@ -1387,11 +1387,20 @@ module.exports = (function() {
} }
if (options.lock && this._dialect.supports.lock) { if (options.lock && this._dialect.supports.lock) {
if (options.lock === 'SHARE') { var lock = options.lock;
if (typeof options.lock === 'object') {
lock = options.lock.level;
}
if (this._dialect.supports.lockKey && (lock === 'KEY SHARE' || lock === 'NO KEY UPDATE')) {
query += ' FOR ' + lock;
} else if (lock === 'SHARE') {
query += ' ' + this._dialect.supports.forShare; query += ' ' + this._dialect.supports.forShare;
} else { } else {
query += ' FOR UPDATE'; query += ' FOR UPDATE';
} }
if (this._dialect.supports.lockOf && options.lock.of instanceof Model) {
query += ' OF ' + this.quoteTable(options.lock.of.name);
}
} }
query += ';'; query += ';';
......
...@@ -33,6 +33,9 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supp ...@@ -33,6 +33,9 @@ PostgresDialect.prototype.supports = _.merge(_.cloneDeep(Abstract.prototype.supp
}, },
schemas: true, schemas: true,
lock: true, lock: true,
lockOf: true,
lockKey: true,
lockOuterJoinFailure: true,
forShare: 'FOR SHARE', forShare: 'FOR SHARE',
index: { index: {
concurrently: true, concurrently: true,
......
...@@ -58,14 +58,18 @@ Transaction.ISOLATION_LEVELS = { ...@@ -58,14 +58,18 @@ Transaction.ISOLATION_LEVELS = {
* }, { * }, {
* transaction: t1, * transaction: t1,
* lock: t1.LOCK.UPDATE, * lock: t1.LOCK.UPDATE,
* lock: t1.LOCK.SHARE * lock: t1.LOCK.SHARE,
* lock: t1.LOCK.KEY_SHARE, // Postgres 9.3+ only
* lock: t1.LOCK.NO_KEY_UPDATE // Postgres 9.3+ only
* }) * })
* ``` * ```
* @property LOCK * @property LOCK
*/ */
Transaction.LOCK = Transaction.prototype.LOCK = { Transaction.LOCK = Transaction.prototype.LOCK = {
UPDATE: 'UPDATE', UPDATE: 'UPDATE',
SHARE: 'SHARE' SHARE: 'SHARE',
KEY_SHARE: 'KEY SHARE',
NO_KEY_UPDATE: 'NO KEY UPDATE'
}; };
/** /**
......
...@@ -176,6 +176,157 @@ describe(Support.getTestDialectTeaser('Transaction'), function() { ...@@ -176,6 +176,157 @@ describe(Support.getTestDialectTeaser('Transaction'), function() {
}); });
}); });
it('fail locking with outer joins', function() {
var User = this.sequelize.define('User', { username: Support.Sequelize.STRING })
, Task = this.sequelize.define('Task', { title: Support.Sequelize.STRING, active: Support.Sequelize.BOOLEAN })
, self = this;
User.belongsToMany(Task, { through: 'UserTasks' });
Task.belongsToMany(User, { through: 'UserTasks' });
return this.sequelize.sync({ force: true }).then(function() {
return Promise.join(
User.create({ username: 'John'}),
Task.create({ title: 'Get rich', active: false}),
function (john, task1) {
return john.setTasks([task1]);
}).then(function() {
return self.sequelize.transaction(function(t1) {
if (current.dialect.supports.lockOuterJoinFailure) {
return expect(User.find({
where: {
username: 'John'
},
include: [Task]
}, {
lock: t1.LOCK.UPDATE,
transaction: t1
})).to.be.rejectedWith('FOR UPDATE cannot be applied to the nullable side of an outer join');
} else {
return User.find({
where: {
username: 'John'
},
include: [Task]
}, {
lock: t1.LOCK.UPDATE,
transaction: t1
});
}
});
});
});
});
if (current.dialect.supports.lockOf) {
it('supports for update of table', function() {
var User = this.sequelize.define('User', { username: Support.Sequelize.STRING }, { tableName: 'Person' })
, Task = this.sequelize.define('Task', { title: Support.Sequelize.STRING, active: Support.Sequelize.BOOLEAN })
, self = this;
User.belongsToMany(Task, { through: 'UserTasks' });
Task.belongsToMany(User, { through: 'UserTasks' });
return this.sequelize.sync({ force: true }).then(function() {
return Promise.join(
User.create({ username: 'John'}),
Task.create({ title: 'Get rich', active: false}),
Task.create({ title: 'Die trying', active: false}),
function (john, task1) {
return john.setTasks([task1]);
}).then(function() {
return self.sequelize.transaction(function(t1) {
return User.find({
where: {
username: 'John'
},
include: [Task]
}, {
lock: {
level: t1.LOCK.UPDATE,
of: User
},
transaction: t1
}).then(function(t1John) {
// should not be blocked by the lock of the other transaction
return self.sequelize.transaction(function(t2) {
return Task.update({
active: true
}, {
where: {
active: false
},
transaction: t2
});
}).then(function() {
return t1John.save({
transaction: t1
});
});
});
});
});
});
});
}
if (current.dialect.supports.lockKey) {
it('supports for key share', function() {
var User = this.sequelize.define('user', {
username: Support.Sequelize.STRING,
awesome: Support.Sequelize.BOOLEAN
})
, self = this
, t1Spy = sinon.spy()
, t2Spy = sinon.spy();
return this.sequelize.sync({ force: true }).then(function() {
return User.create({ username: 'jan'});
}).then(function() {
return self.sequelize.transaction().then(function(t1) {
return User.find({
where: {
username: 'jan'
}
}, {
lock: t1.LOCK.NO_KEY_UPDATE,
transaction: t1
}).then(function(t1Jan) {
return self.sequelize.transaction().then(function(t2) {
return Promise.join(
User.find({
where: {
username: 'jan'
}
}, {
lock: t2.LOCK.KEY_SHARE,
transaction: t2
}).then(function() {
t2Spy();
return t2.commit();
}),
t1Jan.update({
awesome: true
}, { transaction: t1}).then(function() {
return Promise.delay(2000).then(function () {
t1Spy();
expect(t1Spy).to.have.been.calledAfter(t2Spy);
return t1.commit();
});
})
);
});
});
});
});
});
}
it('supports for share', function() { it('supports for share', function() {
var User = this.sequelize.define('user', { var User = this.sequelize.define('user', {
username: Support.Sequelize.STRING, username: Support.Sequelize.STRING,
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!