prisma (지속 업데이트)

catmaker·2024년 10월 20일

library

목록 보기
5/13
// /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;

declare?

  • declare는 이 변수나 함수가 전역에 존재한다는 것을 명시하는 키워드이다.

globalThis?

  • globalThis는 전역 객체를 가리키는 상수이다.

코드 진행 순서

  • 전역 스코프에 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 클래스의 인스턴스

왜 개발 환경에서만?

  • 프로덕션 환경에서는 서버 재시작이 덜 빈번하고, 메모리 관리가 더 중요하기 때문이다.

다른 파일에서의 사용

  • prismadb 객체를 다른 파일에서 import하여 사용할 때 재사용이 이루어진다.
// 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와 db 연동하기

  • db는 neon db를 사용해서 진행했다.

왜 neon db를 선택했는지?

  • 일단 기본적인 무료티어를 지원해준다.
  • 서버리스 아키텍처 서버 관리 대신 비즈니스 로직에 집중할 수 있고, 사용한 만큼만 비용을 지불한다. 최소한의 설정으로 빠른 시작이 가능하기에 선택했다.
// 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");

관계 모드 설정하기

  • prisma 모드는 Prisma가 관계를 관리하는 방식을 지정한다.
  • 외래 키 제약 조건을 데이터베이스 수준에서 강제하지 않는다.
relationMode = "prisma";

foreignKeys 는 데이터베이스 수준에서 외래 키 제약 조건을 사용한다.

none 은 prisma가 관계를 관리하지 않고 개발자가 직접 관계를 관리해야 한다.

스키마 모델 정의하기

model Store {
  id        String   @id @default(uuid())
  name      String
  userId    String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
  • @id 는 이 필드가 테이블의 기본 키 (primary Key)임을 나타낸다.
  • @default 는 필드의 기본 값을 지정한다. 새 레코드 생성 시 값을 명시적으로 제공하지 않으면 기본 값이 사용된다.
  • uuid() 는 각 id에 대해 고유한 문자열을 자동으로 생성한다. 충돌 가능성이 극히 낮아 분산 시스템에서 유용하다.
  • @default(now()) 는 now() 함수는 현재 날짜와 시간을 반환한다. createdAt 필드에 레코드 생성 시점이 자동으로 기록된다.
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])
}
  • 관계 이름 "BillboardStore" 은 관계의 고유 이름이다. 양쪽 모델에서 같은 이름을 사용하여 관계를 명확히 식별한다. 동일한 모델 간에 여러 관계가 있을 때 특히 유용하다.
  • fields: [store] 는 현재 모델(빌보드)에서 관계를 위해 사용되는 필드를 지정한다.
  • references: [id] 는 참조되는 모델(Store)에서 어떤 필드가 참조되는지 지정한다. 여기서는 Store 모델의 id 필드가 참조된다.

    여기서는 relation으로 store와의 관계를 정의한다. "BillboardStore"란 이름으로 우리 관계를 정의하고 Billboard 모델의 storeId 필드는 Store 모델의 id 필드를 참조해서 사용할거다.

@relation 은 때로는 선택사항이고 때로는 필수이다.

  1. 다른 모델과의 관계
    다른 모델과의 관계에서는 relation 이름이 선택사항이다.
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])
}
  1. 같은 모델과의 "여러" 관계
    같은 모델과 여러 관계를 맺을 때는 relation 이름이 필수이다.
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")
}
  • 다른 모델과의 관계는 선택사항이며 모델 자체가 다르므로 Prisma가 자동으로 구분이 가능하다.
  • 같은 모델과의 여러 관계는 relation이 필수이며 같은 모델 간의 여러 관계를 구분하기 위해 필요하다.

onDelete: Cascade

데이터베이스의 참조 무결성을 유지하기 위한 옵션이다.

  • 상위 레코드 (ex. Product)가 삭제될 때 연결된 하위 레코드 (ex. Image)도 자동으로 함께 삭제된다.
  • 제품은 삭제되었는데 이미지는 남아있는 "고아" 레코드가 발생할 수 있고 참조 무결성이 깨질 수 있기 때문에 사용한다.
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])
}

createMany

  • 여러 레코드를 한 번에 생성한다.
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);

[]

[]는 여러 개가 연결될 수 있음을 나타낸다.

@@index

데이터베이스의 인덱스를 생성하는 Prisma의 지시어이다. 목적은 데이터베이스의 쿼리 성능을 향상시키며 특정 컬럼(storeId)를 기반으로 한 검색을 빠르게 만든다.

include

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개 있다고 가정했을 때

  1. 인덱스 없이는 최악의 경우 백만개의 레코드를 모두 확인해야한다. 평균적으로 50만개의 레코드를 확인해야 알 수 있다.
  2. 인덱스 있을 때는 인덱스 구조(B-트리)를 사용하여 로그 시간에 해당 storeId의 위치를 찾는다. (약 20번)
    그 후 필요한 100개의 레코드만 직접 접근하여 가져온다.

데이터베이스에 스키마 변경사항을 적용하는 순서

npx prisma generate
npx prisma db push

push와 migrate의 차이

  • push는 히스토리를 생성하지 않고 변경 사항을 추적하지 않는다. 그래서 롤백이 어렵고 변경 공유가 어렵다. 속도는 일반적으로 더 빠르고 개발 초기, 프로토타이핑에 적합하다.

  • migrate는 SQL 마이그레이션 파일을 생성하고 관리한다. 각 변경을 별도 파일로 기록, 버전 관리가 가능하며 이전 버전으로 쉽게 롤백이 가능하다. 마이그레이션 파일을 통해 변경 사항 공유가 용이하며 속도는 파일을 생성하기 때문에 약간 더 느리다. 프로덕션 환경이나 팀 프로젝트에 적합하다.

Primary Key는 데이터베이스 테이블에서 각 레코드를 고유하게 식별하는 필드 또는 필드의 조합이다.

고유성 : 테이블 내에서 중복될 수 없다.
불변성 : 한번 설정되면 변경이 어렵다.
필수값 : NULL이 될 수 없다.
인덱싱 : 자동으로 인덱스가 생성되어 검색 속도가 빠르다.
관계 정의 : 다른 테이블과의 관계를 정의할 때 사용한다.

prisma client?

Prisma가 자동으로 생성하는 TS/JS 라이브러리이다.

무슨 역할을 하는데?

애플리케이션 코드와 데이터베이스 사이의 인터페이스 역할을 한다.
데이터베이스 쿼리를 실행(CRUD)
데이터베이스 스키마에 기반한 타입-안전한 API를 제공한다.

장점이 뭔데?

SQL을 직접 작성하지 않고도 데이터베이스 작업을 가능하게 해준다.
코드 자동완성과 타입 체크를 제공한다.

쉽게 prisma client는 개발자가 데이터베이스와 쉽고 안전하게 상호작용할 수 있게 해주는 도구이다.

prisma 모드를 선택한 이유는?

  • 유연한 스키마 관리
  • prisma의 자동화된 관계 관리 기능을 활용하기 위해

왜 MySQL도 있고 nosql도 있는데 굳이 PostgreSQL을 선택했는지?

  • 진행하는 프로젝트 상 동시 접속이 많이 일어날 것으로 예상한다. (예측) 그런면에서 MySQL보다 우수한 성능을 보인다.
  • MongoDB의 장점인 문서 저장 기능도 JSON 타입으로 제공한다.
  • 오픈소스 생태계가 크다.
  • MySQL 보다 더 다양하고 복잡한 데이터 타입을 지원한다. (배열, JSON 등)

Prisma의 쿼리 메서드

  • findFirst는 조건에 맞는 첫 번째 레코드를 반환한다.
    조건에 맞는 레코드가 없으면 null을 반환한다.
  const store = await prismadb.store.findFirst({
    where: {
      id: params.storeId,
    },
  });
  • findUnique는 유니크 필드로 단일 레코드를 찾는다.
   const user = await prisma.user.findUnique({
     where: {
       email: 'user@example.com'
     }
   });
  • findMany는 조건에 맞는 모든 레코드를 반환한다.
   const activeUsers = await prisma.user.findMany({
     where: {
       isActive: true
     },
     take: 10
   });
  • create는 새 레코드를 생성한다.
   const newUser = await prisma.user.create({
     data: {
       email: 'newuser@example.com',
       name: 'New User'
     }
   });
  • update는 기존 레코드를 업데이트한다.
   const updatedUser = await prisma.user.update({
     where: { id: 1 },
     data: { name: 'Updated Name' }
   });
  • delete는 기존 레코드를 삭제한다.
   const deletedUser = await prisma.user.delete({
     where: { id: 1 }
   });
  • upsert는 레코드가 존재하면 업데이트하고, 없으면 생성한다.
   const upsertUser = await prisma.user.upsert({
     where: { email: 'user@example.com' },
     update: { name: 'Updated Name' },
     create: { email: 'user@example.com', name: 'New User' }
   });
  • count는 조건에 맞는 레코드의 수를 반환한다.
   const userCount = await prisma.user.count({
     where: { isActive: true }
   });
  • groupBy는 그룹화된 결과를 반환한다.
   const groupedUsers = await prisma.user.groupBy({
     by: ['country'],
     _count: { country: true },
     having: { country: { _count: { gt: 100 } } }
   });
  • orderBy는 데이터를 정렬하는데 사용되는 옵션이다.
const billboards = await prismadb.billboard.findMany({
  orderBy: {
  	createdAt: "desc"
  }
});
  1. createdAt : 정렬 기준이 되는 필드 이 경우는 createdAt 필드를 기준으로 정렬한다.
  2. "desc" 정렬 방향을 나타낸다. "descending"의 약자로 내림차순 정렬 (오름차순은 "asc")
profile
'왜?'

0개의 댓글