// /lib/prismadb.ts
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined
}
const prismadb = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalThis.prisma = prismadb;
전역 스코프에 prisma라는 변수가 존재한다고 선언
PrismaClient 타입 또는 undefined 타입을 가질 수 있는 변수이다.
이렇게 선언하면 전역 스코프에 prisma라는 변수가 있을 수도 있고, 없을 수도 있음을 명시한다.
왜 이렇게 선언하냐?
1. Next.js와 같은 프레임워크에서는 코드 변경 시 자동으로 서버를 재시작하는 Hot Reloading 기능을 사용하는데, 이 과정에서 새로운 PrismaClient 인스턴스가 계속 생성되면서 메모리 누수가 발생할 수 있기 때문
2. 데이터베이스 연결과 같은 리소스는 일반적으로 어플리케이션 전체에서 하나의 인스턴스만 유지하는 것이 좋다.
3. prisma 변수가 존재하지 않을 수 있는 상황도 고려한다.
4. 기존 인스턴스가 있으면 그것을 사용하고, 없으면 새로 생성한다. 그리고 개발 환경에서는 globalThis.prisma에 저장하여 다음 Hot Reloading 시에도 재사용할 수 있도록 한다.
이러한 이유로 싱글톤 패턴을 사용하여 전역 스코프에 하나의 PrismaClient 인스턴스를 생성하고 공유하는 것이 좋다.
인스턴스란 객체 지향 프로그래밍의 개념으로 인스턴스는 클래스를 바탕으로 생성된 구체적인 실체(객체)를 말한다. 클래스가 설계도라면 인스턴스는 그 설계도를 바탕으로 만들어진 실제 제품이다.
class Car {
constructor(public brand: string) {}
}
const myCar = new Car("Toyota"); // myCar는 Car 클래스의 인스턴스
// user.ts
import prismadb from '../../lib/prismadb'
export default async function handler(req, res) {
const users = await prismadb.user.findMany()
res.json(users)
}
// post.ts
import prismadb from '../../lib/prismadb'
export default async function handler(req, res) {
const posts = await prismadb.post.findMany()
res.json(posts)
}
이 두 파일은 같은 prismadb 인스턴스를 사용한다. 첫 번째 import에서 인스턴스가 생성되고, 이후의 import에서는 이미 생성된 인스턴스를 재사용한다.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
relationMode = "prisma"
// uncomment next line if you use Prisma <5.10
// directUrl = env("DATABASE_URL_UNPOOLED")
}
.env에 neondb에서 제공하는 DATABASE_URL을 넣어주기
provider = "postgresql";
url = env("DATABASE_URL");
relationMode = "prisma";
foreignKeys 는 데이터베이스 수준에서 외래 키 제약 조건을 사용한다.
none 은 prisma가 관계를 관리하지 않고 개발자가 직접 관계를 관리해야 한다.
model Store {
id String @id @default(uuid())
name String
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Billboard {
id String @id @default(uuid())
storeId String
store Store @relation("BillboardStore", fields: [storeId], references: [id])
label String
imageUrl String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storeId])
}
여기서는 relation으로 store와의 관계를 정의한다. "BillboardStore"란 이름으로 우리 관계를 정의하고 Billboard 모델의 storeId 필드는 Store 모델의 id 필드를 참조해서 사용할거다.
model Category {
id String @id @default(uuid())
// Store 모델과의 관계
storeId String
store Store @relation(fields: [storeId], references: [id])
// Billboard 모델과의 관계
billboardId String
billboard Billboard @relation(fields: [billboardId], references: [id])
}
model User {
id String @id @default(uuid())
// Post 모델과의 두 가지 다른 관계
writtenPosts Post[] @relation("WrittenPosts") // 작성한 포스트
likedPosts Post[] @relation("LikedPosts") // 좋아요한 포스트
}
model Post {
id String @id @default(uuid())
// User 모델과의 두 가지 다른 관계
authorId String
author User @relation("WrittenPosts", fields: [authorId], references: [id])
likedBy User[] @relation("LikedPosts")
}
데이터베이스의 참조 무결성을 유지하기 위한 옵션이다.
model Image {
id String @id @default(uuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
url String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
}
images: {
createMany: {
data: [...images.map((image: { url: string }) => image)],
},
}
배열이 이렇다면 prisma는 이를 데이터베이스에
images = [
{ url: "image1.jpg" },
{ url: "image2.jpg" },
{ url: "image3.jpg" }
]
이런 식으로 처리한다.
INSERT INTO images (url, productId) VALUES
('image1.jpg', productId),
('image2.jpg', productId),
('image3.jpg', productId);
[]는 여러 개가 연결될 수 있음을 나타낸다.
데이터베이스의 인덱스를 생성하는 Prisma의 지시어이다. 목적은 데이터베이스의 쿼리 성능을 향상시키며 특정 컬럼(storeId)를 기반으로 한 검색을 빠르게 만든다.
include는 데이터 조회 시 관계를 맺고 있는 테이블의 데이터도 가져온다.
const categories = await prismadb.category.findMany({
where: {
storeId: params.storeId,
},
include: {
billboard: true,
}, // 데이터 조회 시 빌보드 데이터도 함께 조회
orderBy: {
createdAt: "desc",
},
});
왜 storeId에 인덱스를 추가하나요?
storeId는 외래 키(foreign key)로 사용된다.
외래 키에 인덱스를 추가하는 것은 일반적인 데이터베이스 최적화 기법이다.
구문 설명
@@은 모델 수준의 속성을 나타낸다.
index는 인덱스를 생성한다는 의미이다.
[storeId]는 인덱스를 적용할 필드를 지정한다.
인덱스가 없을 때
1. 데이터베이스는 Billboard 테이블의 모든 레코드를 처음부터 끝까지 순차적으로 검사한다.
2. 각 레코드마다 storeId 값을 확인하여 원하는 값과 일치하는지 비교한다.
3. 일치하는 모든 레코드를 결과에 포함시킨다.
인덱스가 있을 때
1. 데이터베이스는 먼저 storeId에 대한 인덱스를 확인한다.
2. 인덱스는 storeId 값을 기준으로 정렬된 구조를 가지고 있어, 원하는 값을 빠르게 찾을 수 있다.
3. 인덱스에서 해당 storeId 값의 위치를 찾으면, 그에 해당하는 실제 데이터의 위치를 바로 알 수 있다.
4. 데이터베이스는 이 정보를 사용하여 필요한 레코드들만 직접 접근하여 가져온다.
구체적인 예시
테이블에 백만개의 레코드가 있고, 특정 stordId를 가진 레코드가 100개 있다고 가정했을 때
npx prisma generate
npx prisma db push
push는 히스토리를 생성하지 않고 변경 사항을 추적하지 않는다. 그래서 롤백이 어렵고 변경 공유가 어렵다. 속도는 일반적으로 더 빠르고 개발 초기, 프로토타이핑에 적합하다.
migrate는 SQL 마이그레이션 파일을 생성하고 관리한다. 각 변경을 별도 파일로 기록, 버전 관리가 가능하며 이전 버전으로 쉽게 롤백이 가능하다. 마이그레이션 파일을 통해 변경 사항 공유가 용이하며 속도는 파일을 생성하기 때문에 약간 더 느리다. 프로덕션 환경이나 팀 프로젝트에 적합하다.
Primary Key는 데이터베이스 테이블에서 각 레코드를 고유하게 식별하는 필드 또는 필드의 조합이다.
고유성 : 테이블 내에서 중복될 수 없다.
불변성 : 한번 설정되면 변경이 어렵다.
필수값 : NULL이 될 수 없다.
인덱싱 : 자동으로 인덱스가 생성되어 검색 속도가 빠르다.
관계 정의 : 다른 테이블과의 관계를 정의할 때 사용한다.
Prisma가 자동으로 생성하는 TS/JS 라이브러리이다.
애플리케이션 코드와 데이터베이스 사이의 인터페이스 역할을 한다.
데이터베이스 쿼리를 실행(CRUD)
데이터베이스 스키마에 기반한 타입-안전한 API를 제공한다.
SQL을 직접 작성하지 않고도 데이터베이스 작업을 가능하게 해준다.
코드 자동완성과 타입 체크를 제공한다.
쉽게 prisma client는 개발자가 데이터베이스와 쉽고 안전하게 상호작용할 수 있게 해주는 도구이다.
const store = await prismadb.store.findFirst({
where: {
id: params.storeId,
},
});
const user = await prisma.user.findUnique({
where: {
email: 'user@example.com'
}
});
const activeUsers = await prisma.user.findMany({
where: {
isActive: true
},
take: 10
});
const newUser = await prisma.user.create({
data: {
email: 'newuser@example.com',
name: 'New User'
}
});
const updatedUser = await prisma.user.update({
where: { id: 1 },
data: { name: 'Updated Name' }
});
const deletedUser = await prisma.user.delete({
where: { id: 1 }
});
const upsertUser = await prisma.user.upsert({
where: { email: 'user@example.com' },
update: { name: 'Updated Name' },
create: { email: 'user@example.com', name: 'New User' }
});
const userCount = await prisma.user.count({
where: { isActive: true }
});
const groupedUsers = await prisma.user.groupBy({
by: ['country'],
_count: { country: true },
having: { country: { _count: { gt: 100 } } }
});
const billboards = await prismadb.billboard.findMany({
orderBy: {
createdAt: "desc"
}
});
- createdAt : 정렬 기준이 되는 필드 이 경우는 createdAt 필드를 기준으로 정렬한다.
- "desc" 정렬 방향을 나타낸다. "descending"의 약자로 내림차순 정렬 (오름차순은 "asc")