New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimistic Locking doesn't work for update #2848
Comments
I see that issue 1308 would let me check to see how many rows were updated, which would allow me to safely implement an Optimistic Lock for update. This would be a fine intermediate result. However, I think longer term actually making setLock() with update() work like this would be a more intuitive and reusable API, especially given this is how other ORMs behave. |
Also, doing this via QueryRunner doesn't seem like the best choice to me. This would mean the client needs to walk the object graph and individually find and save every object that is version. |
I stumbled on a reliable way of making this work for Postgres: //The version where clause below prevents an update from occurring if a newer version is already in the database
As discussed in the comment above, using .returning() gives a reliable indicator of whether the update went through. Combining this with the version checking where clause makes an acceptable solution. Having the .setLock() and @Version feature work together to accomplish this would be better, but this is OK for now. |
Can't find proper sample of complex scenario, that include:
Everything should be data safe and not affected by race conditions. Seems, something like this, should work for insert // Transaction manager + lock
getManager().transaction((transactionalEntityManager) => {
return transactionalEntityManager
.createQueryBuilder(User, 'user')
.setLock('pessimistic_write')
.whereInIds(userDataShape.id)
.getOne()
.then((existData) => {
// check that object not exists
if (existData) {
throw new Error('dbInsertErrorDuplicateKey');
}
// create new entity
const newUser = User.create(userDataShape);
// save
return transactionalEntityManager
.save(newUser)
.then((result) => {
// done
});
});
}).catch((err) => {
// err
}); for update: // Transaction manager + lock
getManager().transaction((transactionalEntityManager) => {
return transactionalEntityManager
.createQueryBuilder(User, 'user')
.setLock('pessimistic_write')
.whereInIds(inputDataShape.id)
.getOne()
.then((existData) => {
// check that object exists
if (!existData) {
throw new Error('dbRecordNotFoundById');
}
// check object version
if (existData.dataVersion !== inputDataShape.dataVersion) {
throw new Error('objectVersionOutdated');
}
// update entity
Object.assign(existData, inputDataShape);
// save
return transactionalEntityManager
.save(existData)
.then((result) => {
// done
});
});
}).catch((err) => {
// err
}); |
Thanks @GitStorageOne , but I don't believe this solves the problem. The issue is that save() itself knows nothing about the version and doesn't guard against updating a record that has a later version than what's being passed in. If I took the approach above, with two concurrent transactions, the second in would fail to acquire the write lock. But if I built auto-retry in, a retry would merrily overwrite what's in the table with old version content. The key issue is that the update query must be version aware. The queries generated by save() are not. I'm able to make QueryBuilder update queries aware manually, with where and returning() clauses above. The ultimate solution would be to have save() generate the proper SQL (like mine above) if the entity has a property annotated by @Version. This would be so much better because it could work with a whole object tree. |
I've tested behavior with postgresql. Scenario looks like this:
Lets say, we have many concurrent save operations, with code, that i showed above.
That's true. |
|
Issue type:
[ ] question
[ ] bug report
[ ] feature request
[ ] documentation issue
Database system/driver:
[ ]
cordova
[ ]
mongodb
[ ]
mssql
[ ]
mysql
/mariadb
[ ]
oracle
[x ]
postgres
[ ]
sqlite
[ ]
sqljs
[ ]
react-native
[ ]
expo
TypeORM version:
[x ]
latest
[ ]
@next
[ ]
0.x.x
(or put your version here)Steps to reproduce or a small repository showing the problem:
Hi All,
Trying to use the @Version annotation to implement optimistic locking for ensuring entity updates don't stomp on newer data. I couldn't find an example using QueryRunner along with update(), but I tried it anyway like this:
await transactionalRepository.createQueryBuilder("c")
.setLock("optimistic", caseEntity.version)
.update()
.set(this.createCaseUpdateObj(caseEntity))
.where("id = :id", {id: caseEntity.id})
.execute();
However, this operation succeeds no matter what I pass to setLock for version. I CAN ensure that I don't stomp on newer data by augmenting my query like so:
await transactionalRepository.createQueryBuilder("c")
.update()
.set(this.createCaseUpdateObj(caseEntity))
.where("id = :id", {id: caseEntity.id})
.andWhere("version = :version", {version: caseEntity.version})
.execute();
However, the problem now is I have no way of finding out whether my update was actually applied. To do that, I need to know how many rows where updated by my query, which does not appear to be returned by this method.
Are there plans to make setLock() handle this case for update() (most ORMs support this)? If not, are there plans to return the number of rows updated by an operation like the one above? If so, any idea of timeframe?
I think until then I'd probably need to drop down to lower level postgres driver to safely perform an update operation.
The text was updated successfully, but these errors were encountered: