-
Prisma - Transactions과 다양한 부가기능FrameWork/ORM 2024. 4. 17. 10:35
Prisma에서 트랜잭션과 다양한 부가기능
Prisma - Doc/PrismaClient/Queries
https://www.prisma.io/docs/orm/prisma-client/queries/transactions
트랜잭션은 결과적으로 성공 또는 작동하지 않음이 보장되는 읽기/쓰기 작업이다. 트랜잭션이 상자속에 이루어진다고 상상해보자 격리된 상자속에서 SQL 문을 실행하는 것과 같다. 따라서 트랜잭션은 모든 명령문이 성공적으로 실행되거나 실행될 것임을 보장한다.
트랜잭션의 기본적인 설명
시나리오
은행계좌를 사용하는 userA와 userB가 있다고 가정해보자 userA는 userB에게 돈 1000원을 보내고 싶어 한다. 이때 첫번째 작업으로 user1의 계좌에 1000이 들어있는지 확인해야 한다. 이것이 확인되면 userA는 1000을 이체할 준비가 된 것이다. 두번째로 userA계정에서 -1000원을 하고 userB의 계정에 +1000을 한다. 이때 일부 네트워크에 오류가 있거나 해당 SQL 쿼리 명령문 자체에 문제가 존재할 수 있다.
이때 이 일련의 과정을 실패할 것이다. 따라서 정상적으로 userA의 계청에서 1,000이 차감되고, userB의 계정에서 1,000이 추가되지 않을수 있다.
실패의 경우
- userA의 1000원이 차감되지 않고 userB에게 1000원이 추가된 경우
- userA의 1000원이 참가되었는데 userB에게 1000원이 추가되지 않은 경우
위와 같은 경우 아예 모든 작업을 무효화해야 한다 즉 쿼리의 수행을 rollback해야한다. 따라서 트랜잭션은 일련의 작업이 성공하지 않으면 아예 작업을 수행하기 전으로 모든 데이터를 되돌려 버린다.
두가지 유형의 Transactions
Sequential transactions(연속 트랜잭션)
export const getArticlesWithTransaction = async (req, res) =>{ const [articles,count] = await prisma.$transaction([ prisma.article.findMany({ where:{ state:"DRAFT" } }), prisma.article.count() ]) res.json({articles,count}); }
위코드에서 $transaction()메소드는 매개변수로 배열을 갖는다. 이 배열에 들어 가는 값은 순차적으로 구현하려고 하는 쿼리 시퀀스가 된다. 이 쿼리중에 하나의 쿼리라도 실패하게 된다고 하면 배열에 들어간 모든 쿼리가 모두 rollback된다.
Interactive Transactions(상호작용 트랜잭션)
export const createUsersWithInteractive = async (req,res) =>{ const user = await prisma.$transaction( async (tx)=>{ let user = await tx.user.findFirst({ where:{ email: req.body.email } }); if(user){ throw new Error('해당 email은 이미 가입한 회원이 존재합니다.'); } user = await tx.user.create({ data:{ email: req.body.email, name : req.body.name } }); const profile = await tx.profile.create({ data:{ ...req.body.profile, userId:user.id } }) return {...user, profile} }); res.json(user); }
연속트랜잭션에서 $transaction()메소드에 배열을 전달했던 것과 달리 비동기 함수를 전달한다. 이 동기 함수 내부에 수행하게 될 모든 쿼리가 위치하게 될 것이다. 위 코드 예시에서는 먼저 user가 가입할려고 하는 email이 이미 가입된적 있는 email인지 확인하고 가입된적 있는 경우 예외를 발생시킨다.
Nested writes (중첩쓰기, 계층적 쓰기)
schema.prisma
model Seller{ id Int @id @default(autoincrement()) email String @unique name String products Product[] } model Product{ id Int @id @default(autoincrement()) title String price Float sellerId Int tag Tag[] carts Cart[] seller Seller @relation(fields: [sellerId],references: [id]) }
모델 User와 모델 Article이 존재할때 두가지 데이터는 각각 생성된다. 하지만 특정 게시판에 사이트의 회원이 아닌 User가 사용자가 Article을 작성할수 있는 권한이 있다할때 User와 Article의 정보가 동시에 성될 것이다. 이때 이러한 데이터 생성을 가능하도록 하는 것이 중첩된 쿼리이다. 정리하자면 둘 이상의 모델에 대해 write 쿼리 작업을 수행하는 것이다.
POST - /users/articles, req.body
{ "email":"test12@example.com", "name":"yoyo", "article":[ { "title":"bnana", "content":"sweet banana", "state":"PUBLISHED" }, { "title":"kiwi", "content":"kiwi bird", "state":"DRAFT" }, { "title":"blueberry", "content":"heaty blueberry", "state":"PUBLISHED" } ] }
중첩쓰기 예제 코드
export const createArticleTogetherWithUser = async(req,res) => { const userAndArticle = await prisma.user.create({ data:{ email : req.body.email, name: req.body.name, article:{ create: req.body.article } } }); res.json(userAndArticle); }
중첩된 쓰기에서는 모든 쿼리가 단일 트랜잭션에서 실행된다. 즉 위 코드에서 새로운 user를 생성하는 것에 실패하면 article데이터는 생성되지 않는다.
Raw SQL query(로우 쿼리 사용하기)
export const getArticleByUserIdWithRawQuery = async (req, res)=> { const articles = await prisma.$queryRaw`SELECT * FROM articles AS a JOIN User AS u On a.userId = u.id WHERE a.userId=${req.params.id}` res.json(articles); }
prisma는 $queryRaw라는 메서드로 로우쿼리를 사용할수 있도록 지원한다.
https://www.prisma.io/docs/orm/prisma-client/queries/raw-database-access/raw-queries
위와 같이 템플릿 리터럴을 사용해도 prisma는 $queryRaw 메서드를 통해 SQL injection으로 부터 안전한 쿼리를 생성하는것을 공식문서를 통해 확인할 수 있다.
Custom Validation(사용자정의 유효성검증)
➕ 추천 - 사용자 유효성 검사를 위한 모듈
Zod.js를 사용한 유효성 검사 예제
import { PrismaClient } from "@prisma/client"; import {z} from 'zod'; const ArticleCreateSchema = z.object({ title: z.string().max(10), constent: z.string().max(1000), state: z.enum(['DRAFT','PUBLISHED']) }) const prisma = new PrismaClient({ log:['query'] }).$extends({ query:{ article:{ create:({args, query}) =>{ args.data = ArticleCreateSchema.parse(args.data) return query(args) } } } }) export const createArticleWithUserId = async (req, res) =>{ try{ const user = await prisma.user.findFirst({where:{ id: +req.params.id }}) if(!user){ throw Error("해당하는 사용자가 존재하지 않습니다."); } const data = { ... req.body, userId:user.id } const article = await prisma.article.create({ data }); res.json(article); }catch(err){ res.status(403).json(err) } }
prisma는 Zod.js 라이브러리를 사용하여 데이터 유효성 검사를 수행한다. Zod.js는 TypeScript와 JavaScript에 대해 데이터 검증 침 파싱을 위한 라이브러리로, 타입 안전성을 제공하며 실행시 데이터 유효성을 체크할 수 있게 한다.
우선 ArticleCreateSchema라는 변수명으로 Article 데이커 새성을 위한 데이터 스키마를 정의한다.
그다음 PrismaClient()생성자에 $extends 메서드를 붙여 PrismaClient를 확장하여 article.create메소드를 재정의한다.
- create({args,query}) : 게시글 새성 요청을 처리하는 메서드이다. args는 클라이언트로 부터 받은 입력 데이터를 포함하며, query는 실제 데이터베이스 쿼리르 실행하는 함수이다.
- ArticleCreateSchema.parse(args.data) : 입력된 파라미터 args.data를 Zod를 사용하여 정의한 스키마를 사용하여 파싱하고 유효성을 검증하고 수행한다. 유효성을 검사를 통과하지 못하면 예외가 발생한다.
- return query(args) : 검증이 통과된 데이터를 사용하여 실제 데이터베이스 쿼리를 실행한다.
POST - users/:id/articles,request.body
{ "title":"testArticle using schema", "content":"using using schema", "state":"DRAFT" }
Zod.js를 이용해서 사용자 정의 유효성 검사를 적용한 article.create 메서드에 위와 같은 request.body를 보내면 아래와 같은 오류를 반환하는것을 확인할수 있다.
반환 되는 오류
{ "issues": [ { "code": "too_big", "maximum": 10, "type": "string", "inclusive": true, "exact": false, "message": "String must contain at most 10 character(s)", "path": [ "title" ] } ], "name": "ZodError" }
Computed fields(동적 계산 필드)
https://www.prisma.io/docs/orm/prisma-client/queries/computed-fields
계산 필드역시 PrismaClient에 $extends()메서드를 사용해주어야 한다.
동적 계산 필드 예제
const prisma = new PrismaClient({ log:['query'] }).$extends({ result:{ profile:{ deliverySenderInfo:{ need:{ name: true, addr: true, phone: true }, compute:(profile) => { const {name, addr, phone} = profile; return `${name} ${addr} ${phone}`; } } } } }) export const getSenderInfoByUserId = async(req,res) =>{ const deliverySenderInfo = await prisma.profile.findFirst({ where:{ userId: +req.params.id } }); res.json(deliverySenderInfo); }
- result : 예약어로 computed field로 어떤 결과를 return 받을지 해당 객체를 통해 정의한다.
- profile : 위 예제에서 사용되는 객체이고 데이터로 어떤 객체가 대상이 되는냐에 따라 원하는 해당 객체가 prisma에 정의된 이름을 넣는다.
- deliverySenderInfo : computed field를 통해 return 받을 가상 컬름의 이름이다.
- need : 위 예시에서는 객체 혹은 테이블에서 profile 어떤 컬럼을 조합해서 deliverySenderInfo를 만들지 객체 형태로 명시한다.
- compute : 계산 하는 함수로 profile 테이블의 컬럼의 요소들을 특정연산 혹은 계산을 통해 사용자가 원하는 값을 만들고 return한다.
POST - /user/9/snder/info
{ "id": 5, "name": "yoyo", "addr": "Jeju", "phone": "01099998888", "userId": 9, "deliverySenderInfo": "yoyo Jeju 01099998888" }
[출처 - Building Production-Ready Apps with Prisma Client for NodeJS, Naimish Verma]
'FrameWork > ORM' 카테고리의 다른 글
Sequelize - 실전활용(Update,Delete,Relation설정) (0) 2024.04.24 Prisma - 객체 간의 관계 설정, 1:N, 1:1, N:M (0) 2024.04.16 Prisma - 🐠시작하기 환경설정, 쿼리 로깅, CRUD (0) 2024.04.12 Sequelize - 시퀄라이즈 시작 (설정,모델만들기,관계설정) (0) 2024.03.21 ORM - 영속성 관리(플러시, 준영속) (1) 2024.02.29