Prisma - 객체 간의 관계 설정, 1:N, 1:1, N:M
참조: https://dodote10.tistory.com/624
Prisma 객체간의 관계 설정
이번 게시물에서는 DB에서 자주 사용되고 블로그를 통해 많이 소개되었던 1대다(1:N), 1대1(1:1), 다대다(N:M) 관계들을 Prisma는 어떤 인터페이스를 사용해서 이를 설정할 수 있도록 하는지 알아볼것이다.
1 : N (One to Many Relations)
이 관계의 경우는 테이블 A의 하나의 레코드는 테이블 B의 여러 레코드와 관련되어 있다. 다만 테이블 B는 하나의 A의 레코드하고만 연결된다. 위 그림에서 와 같이 사용자는 여러번 주문을 할수 있다. 하지만 주문은 주문자로써의 사용자정보를 하나만 가진다.
1 : 1 (One to One Relations)
한명의 사용자는 하나의 프로필만 가지게 되고 프로필 입장에서도 하나의 사용자만 가지게 된다.
N : M (Many to Many Relations)
테이블 A의 여러 레코드는 테이블B의 여러 레코드와 관련있을수 있으며 테이블B도 테이블A의 여러 레토드와 관련있을 수 있다. 위 그림의 예시에서 처럼 한권의 책을 열러 작가가 쓸수도 있고 작가 한명이 여러개의 책을 쓸 수 도 있다.
Prisma Relationship
1:1 관계 맺기
schema.prisma file
model User{
id Int @id @default(autoincrement())
email String @unique
profile Profile?
}
model Profile{
id Int @id @default(autoincrement())
name String
addr String
phone String
userId Int @unique // foreign key
user User @relation(fields: [userId],references: [id])
}
모델 Profile이 userId(모델 User에 id)를 알게 하려면 위와 같이 추가적인 설정이 필요하다. 그래서 model Profile 제일 마지막에 user속성을 추가하고 Type은 User로 한다. 위 @relation괄호의 fields는 모델 Profile이 참조하고있는 외래키로 userId 속성을 가르키고, references는 모델 User 입장에서 참조를 당한 키로 User의 primary키인 id를 가르킨다.
위 방식은 일대다(1:N) 방식에서도 사용하는 방식이지만 현재는 일대일(1:1)방식에 사용되었으므로 모델 Profile의 userId값을 @unique 표시한다.
prisma가 모델 Profile의 userId속성이 모델 User id에서 참조되었다는 것을 아는 것은 모델 Profile의 user속성을 통해 이루어진다. use칼럼은 실제로 테이블에 칼럼으로 생성되지 않는다. prisma에서 내부 참조용으로만 사용된다.
1:1 관계 맺기 예제 코드
export const getUser = async (req,res) => {
const users = await prisma.user.findMany({
include:{
profile :true
}
});
res.json(users);
}
export const getUserId = async (req, res) => {
const user = await prisma.user.findUnique({
where:{
id:req.params.id
},
include:{
profile:true
}
});
res.json(user);
}
export const createUser = async (req,res) => {
const user = await prisma.user.create({
data : req.body
});
res.json(user);
}
export const createProfile = async (req, res)=> {
// 1. 사용자 정보를 가져오고
const user = await prisma.user.findUniqueOrThrow({
where:{
id: +req.params.id
},
});
// 2. 데이터를 구성하고
const data = {
...req.body,
userId: user.id
}
// 3. 프로파일을 만든다.
const profile = await prisma.profile.create({data});
res.json(profile);
}
1: N 관계 맺기
schema.prisma file
enum ArticleState{
DRAFT
PUBLISHED
}
// user may have one or multiple article and article must be associated to a user
// user은 하나 혹은 여러개의 article을 가질수 있으며 article은 user와 연결되야 한다.
model Article{
id Int @id @default(autoincrement())
title String
content String
state ArticleState
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId Int
user User @relation(fields: [userId],references: [id])
@@map("articles")
}
// users and profiles
// user may have a single profile and profile must be linked to a user
// user는 하나의 profile을 가질수 있고 그렇다면 그것은 user 레코드와 연결된다.
model User{
id Int @id @default(autoincrement())
email String @unique
profile Profile?
articles Article[]
}
1:N의 경우 여러개의 테이블의 레코드를 가지게되는 테이블 쪽이 즉 user와 article의 관계에서 한명의 user는 여러개의 articled을 가질수 있지만 article은 하나의 user밖에 갖지 못하므로 모델 User에서의 article은 Article배열 형태의 type을가지게 된다. 그리고 모델 Article에서 userId도 @unique값을 가지지 않게 된다.
User와 Article 데이터 가져오기
조건
- 모든 user가 가진 각각의 article과 그 갯수 반환받기
- user정보과 PUBLISHED되 article가져오기
export const getArticles = async (req, res) => {
const where = {
state: req.query.state? req.query.state: ('DRAFT'||'PUBLISHED')
};
const articleAndCount = await prisma.user.findMany({
include:{
articles:{
where
},
_count:{
select:{
articles:{
where
}
}
}
}
});
res.json(articleAndCount);
}
특정 User의 조건에 만족하는 Article삭제하기
export const deleteUnPublishedArticle = async (req,res) => {
const user = await prisma.user.findUnique({
where:{
id: +req.params.id
}});
if(!user){
res.status(404).send("해당하는 사용자가 존재하지 않습니다. ")
}
const deletedCount = await prisma.article.deleteMany({
where :{
userId: user.id,
state : req.query.state ? req.query.state : 'DRAFT' || 'PUBLISHED'
}
});
res.json(deletedCount);
}
N:M 관계 맺기
schema.prisma file
model User{
id Int @id @default(autoincrement())
email String @unique
name String
profile Profile?
articles Article[]
}
model Product{
id Int @id @default(autoincrement())
title String
price Float
}
model Tag {
id Int @id @default(autoincrement())
name String
}
product는 여러개의 tag를 가진다. tag역시 하나rk 여러개의 제품과 관련이 있을 수 있다.
Prisma N:M관계의 두가지 유형
암시적 방법에서의 다대다 관계는 피벗테이블이나 조인 테이블이 존재하지 않는다. 때문에 Prima에 의해 백그라운드에서 피벗테이블의 역할을 하는 테이블이 생성된다.
암시적 다대다 관계
schema.prisma file
model Product{
id Int @id @default(autoincrement())
title String
price Float
tag Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String
product Product[]
}
특정 모델이 다른 모델의 primary key를 갖지 않고 서로가 서로의 데이터를 배열형태로 가질수 있다고 schema.prisma 파일을 수정한후 prisma migrate dev 명령어를 터미널에서 실행한다.
터미널에서 명령어를 실행한후 데이터베이스를 확인하면 _ProductToTag라는 테이블이 생성된 것을 확인할 수 있다. 이것이 Prisma가 암시적으로 N:M 관계를 지원하는 방식이다. 이방식을 사용하면 N:M 관계를 이어주는 테이블을 직접 생성하고 관리할 필요 없이 이 작업을 Prisma가 대신 수행해준다.
Product 생성코드
export const createProduct = async (req,res) => {
const products = await prisma.product.create({
data:{
...req.body,
tag :{
create:req.body.tag
}
}
})
res.json(products);
}
POST - /product, request body
{
"title":"test Product with tag",
"price":50,
"tag":[{"name":"cool"},{"name":"cute"}]
}
GET - /product
{
"id": 6,
"title": "test Product with tag",
"price": 50,
"tag": [
{
"id": 7,
"name": "cool"
},
{
"id": 8,
"name": "cute"
}
]
}
명시적 다대다 관계
schema.prisma file
model User{
id Int @id @default(autoincrement())
email String @unique
name String
profile Profile?
articles Article[]
carts Cart[]
}
model Product{
id Int @id @default(autoincrement())
title String
price Float
tag Tag[]
carts Cart[]
}
model Cart{
id Int @id @default(autoincrement())
userId Int
productId Int
user User @relation(fields: [userId], references:[id])
product Product @relation(fields: [productId], references: [id])
quantity Int
}
이번에는 명시적인 다대다 관계를 prisma가 어떻게 관리하는지를 알아보기 위해 모델 Cart를 만든다. Cart는 User에게 종속되어 있고 또 여러개의 제품과 관계를 가진다. 즉 모델 Cart는 User와 Product의 서로의 N:M 관계를 관리하는 조인테이블이되고 User와 Product의 primary key만을 가지는 단순한 테이블 기능만을 가지는 것이아니라 User가 몇개의 Product를 주문할 것인지에대한 quantity 즉 수량에 대한 정보도 가지고 있어야 하기 때문에 prisma에 의해 암시적 조인테이블을 생성하게 하는것이아니라 Cart라는 테이블로 생성되어 명시적으로 관리된다.
Cart 생성 코드
export const createCart = async (req,res) =>{
// 1. 사용자 정보 확인
const user = await prisma.user.findFirstOrThrow(
{
where:{
id : +req.params.userId
}
})
// 2. 제품 정보 얻기
const product = await prisma.product.findFirstOrThrow({
where:{
id: req.body.product
}
})
// 3. 데이터 구조화 하고 cart 데이터 만들기
const cart = await prisma.cart.create({
data:{
quantity: req.body.quantity,
userId: user.id,
productId: product.id
}
});
res.json(cart);
}
GET - /users/:userId/carts
[
{
"id": 1,
"userId": 4,
"productId": 6,
"quantity": 2,
"product": {
"id": 6,
"title": "test Product with tag",
"price": 50
}
},
{
"id": 2,
"userId": 4,
"productId": 4,
"quantity": 3,
"product": {
"id": 4,
"title": "product4",
"price": 50
}
}
]
Prisma - Doc/Reference/Prisma Client API
https://www.prisma.io/docs/orm/reference/prisma-client-reference
Prisma Client API | Prisma Documentation
API reference documentation for Prisma Client.
www.prisma.io
[출처 - Building Production-Ready Apps with Prisma Client for NodeJS, Naimish Verma]