Row update with DB Row Lock (SELECT FOR UPDATE)
node.js - Row level locking with Prisma and PostgreSQL - Stack Overflow
postgresql - How to use Postgres / Prisma as a queue, preventing workers from selecting already selected rows - Stack Overflow
Allow for locking of the database.
I realized, that for your specific usecase you could also increment your incrementalReportNumber with an atomic number operation
This avoids the need for a stricter transaction isolation level, because you only run update queries and avoid all read queries:
const report = await prisma.$transaction(async (prisma) => {
const school = await prisma.school.update({
where: { id: schoolId },
data: {
incrementalReportNumber: {
// increment the report number here
increment: 1,
},
},
});
const newReport = await prisma.report.create({
data: {
publicId: school.incrementalReportNumber,
schoolId: school.id,
name: reportName,
},
});
return newReport;
});
I think the issue here is with the transaction isolation level used by Prisma when running the query.
The default transaction isolation level in Postgres is "read committed", this is what Prisma uses and it does not prevent non-repeatable reads.
Non-Repeatable Reads
If you're not sure what non-repeatable reads are, I am adding a good explanation taken from here
"A non-repeatable read is one in which data read twice inside the same transaction cannot be guaranteed to contain the same value. Depending on the isolation level, another transaction could have nipped in and updated the value between the two reads.
Non-repeatable reads occur because at lower isolation levels reading data only locks the data for the duration of the read, rather than for the duration of the transaction"
In your case, the following race condition might be happening due to non-repeatable reads.
- Transaction B: Read school data with report number x and increment to x + 1.
- Transaction A: Read school data with report number x and increment to x + 1.
- Transaction B: Commit report number x + 1 to database.
- Transaction A: Attempt to create report with x+1 report number and fail.
Unfortunately, Prisma does not currently support changing the transaction isolation level. So, you might need to come to some other workaround for this issue (possibly even writing raw SQL code).
We have a feature request for exactly this and I would really urge you to comment your problem over there. That way it would help us track demand for this feature and incentivize adding it quickly.
Transactions in Prisma aren't implemented yet. However there is some issues opened in their github about this:
- Transactions for multiple operations (#74)
- Transactions for long running operations with dependencies between write operations (#3846)
Prisma 2.1.0 released the first experimental version of transactions via Promises.
Here is the official documentation docs
const write1 = prisma.user.create()
const write2 = prisma.post.create()
const write3 = prisma.profile.create()
await prisma.$transaction([write1, write2, write3])
You can use prisma-client instead of prisma-binding.
With prisma-binding, you define the top level resolver, then delegates to prisma for all the nesting.
On the other hand, prisma-client only returns scalar values of a type, and you need to define the resolvers for the relations. Which means you have complete control on what you return, even for nested queries. (See the documentation for an example)
I would suggest you use prisma-client to apply your security filters on the fields.
With the approach you're looking to take, I'd definitely recommend a look at Graphile. It approaches row-level security essentially the same way that you're thinking of. Unfortunately, it seems like Prisma doesn't help you move away from writing traditional REST-style controller methods in this regard.