7 min read
Original source

Writing transactions with Prisma

As web developers, one of our primary concerns is keeping the integrity of our data. Fortunately, SQL databases come equipped with tools that allow us to…

As web developers, one of our primary concerns is keeping the integrity of our data. Fortunately, SQL databases come equipped with tools that allow us to ensure data accuracy and consistency. You can find the code from this article in this repository. One of the most fundamental examples of when things might go wrong is transferring money from one bank account to another. Let’s say we have two accounts with $1000, and we want to transfer $500 from one account to another. It consists of two steps: taking $500 from one account, adding the same sum to the other account. If the whole operation fails, our database is still intact, and we have the sum of $2000. We can find ourselves in a worse scenario if just half of the above steps run successfully: reducing the first account balance by $500, failing to add the money to the second account because we provided the wrong number. Because of the above, the first account has $500, and the second one remains with $1000. Therefore, a sum of $500 disappeared, and we lost the integrity of our data. Introducing transactions We can solve the above issue using a transaction. It can consist of more than one instruction and can be described with a few properties: Atomicity A transaction either succeeds wholly or entirely fails. Consistency During a transaction, we transition the database from one valid state to another. Isolation More than one transaction can run concurrently without risking an invalid state of our database. In our particular case, the second transaction would see the transferred money in one of the accounts but not both. Durability The changes from the transaction should persist permanently as soon as we commit them. Transactions with PostgreSQL To initiate a transaction block, we need the BEGIN statement. PostgreSQL will execute all queries after that in a single transaction. When we run the COMMIT statement, PostgreSQL stores our changes.BEGIN; UPDATE bank_accounts SET balance = 500 WHERE id = 1; UPDATE bank_accounts SET balance = 1500 WHERE id = 2; COMMIT;Thanks to using a transaction, we can discard the transaction if transferring the money to the second bank account fails for any reason. To do that, we need the ROLLBACK statement. If you want to know more about handling transactions with raw SQL, check out API with NestJS #76. Working with transactions using raw SQL queries Nested writes with Prisma Prisma offers quite a few ways of using transactions. One is through nested writes that perform multiple operations on many related records. In one of the previous articles, we created schemas for users and their addresses. addressSchema.prisma model Address { id Int @default(autoincrement()) @id street String city String country String user User? } userSchema.prisma model User { id Int @id @default(autoincrement()) email String @unique name String password String address Address? @relation(fields: [addressId], references: [id]) addressId Int? @unique posts Post[] }What’s important, we allow the creation of the user and the address through a single API request. To create the user and the address in a single transaction, we can perform a nested write. users.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserDto } from './dto/createUser.dto'; @Injectable() export class UsersService { constructor(private readonly prismaService: PrismaService) {} async create(user: CreateUserDto) { const address = user.address; return this.prismaService.user.create({ data: { ...user, address: { create: address, }, }, include: { address: true, }, }); } // ... }If any of the above operations fail, Prisma rolls back the transaction. For example, if creating the address fails, the user is not added to the database. Bulk operations Another way of affecting multiple entities with Prisma is through bulk operations: deleteMany, updateMany, createMany. With the above methods, we can alter many records of the same type in a single transaction. posts.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class PostsService { constructor(private readonly prismaService: PrismaService) {} // ... deleteMultiplePosts(ids: number[]) { return this.prismaService.post.deleteMany({ where: { id: { in: ids, }, }, }); } } The transaction API in Prisma The above solutions are helpful in specific situations. Besides them, Prisma also offers a generic API for transactions. Sequential operations The first way of using the transactions API is with sequential operations. By passing multiple database operations into the prismaService.$transaction, we can run them sequentially in a transaction. categories.service.ts import { Injectable } from '@nestjs/common'; import CategoryNotFoundException from './exceptions/categoryNotFound.exception'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export default class CategoriesService { constructor(private readonly prismaService: PrismaService) {} async deleteCategoryWithPosts(id: number) { const category = await this.getCategoryById(id); const postIds = category.posts.map((post) => post.id); return this.prismaService.$transaction([ this.prismaService.post.deleteMany({ where: { id: { in: postIds, }, }, }), this.prismaService.category.delete({ where: { id, }, }), ]); } async getCategoryById(id: number) { const category = await this.prismaService.category.findUnique({ where: { id, }, include: { posts: true, }, }); if (!category) { throw new CategoryNotFoundException(id); } return category; } // ... }For example, if something goes wrong when deleting the category, the posts are not removed from the database. The transaction either fully succeeds or completely fails. We can take the above a step further and use the deleteMultiplePosts we’ve created before. categories.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { PostsService } from '../posts/posts.service'; @Injectable() export default class CategoriesService { constructor( private readonly prismaService: PrismaService, private readonly postsService: PostsService, ) {} async deleteCategoryWithPosts(id: number) { const category = await this.getCategoryById(id); const postIds = category.posts.map((post) => post.id); return this.prismaService.$transaction([ this.postsService.deleteMultiplePosts(postIds), this.prismaService.category.delete({ where: { id, }, }), ]); } // ... }The crucial part is that the deleteMultiplePosts method is not marked with the async keyword. The prismaService.$transaction method expects an array of Prisma.PrismaPromise, not regular promises. Because of that, we can’t use the deleteCategory method with prismaService.$transaction, which is a bit unfortunate. Interactive transactions The above solution is perfectly fine if the operations in our transactions don’t affect each other. Sometimes, however, we need more control. To perform interactive transactions, we must pass a function as an argument to the prismaService.$transaction method. Its argument is an instance of a Prisma client. Each use of this client is encapsulated in a transaction. The deleteMany method we’ve used before in this article does not throw an error if one of the entities is not deleted. Let’s write an interactive transaction that changes that.import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class PostsService { constructor(private readonly prismaService: PrismaService) {} deleteMultiplePosts(ids: number[]) { return this.prismaService.$transaction(async (transactionClient) => { const deleteResponse = await transactionClient.post.deleteMany({ where: { id: { in: ids, }, }, }); if (deleteResponse.count !== ids.length) { throw new NotFoundException('One of the posts cold not be deleted'); } }); } // ... }Prisma commits the transaction when it reaches the end of our function passed to the prismaService.$transaction method. If there is any error along the way, Prisma rolls it back. Interactive transactions are great if we want to operate on the result of a part of our transaction. In the above example, we throw an error if not all posts have been deleted. To do that, we need to check the result of a part of our transaction. We wouldn’t be able to do that using the sequential transactions approach. Summary In this article, we’ve discussed the idea of transactions and how to use them with Prisma. When doing that, we’ve compared various solutions, such as nested writes, bulk operations, and the transactions API. We’ve also used both the sequential operations approach and the interactive transactions. All of the above equips us with solutions for many different use cases we might encounter in our applications. The post API with NestJS #104. Writing transactions with Prisma appeared first on Marcin Wanago Blog - JavaScript, both frontend and backend.

Writing transactions with Prisma | NestJS.io