Prisma란 ORM(Object Relational Mapping)의 한 종류로써, 객체와 데이터 베이스를 연결시켜주는 역할을 한다. 쉽게 설명하자면 server side에서 데이터베이스에 접근할 수 있게 해줘서 server에서 sql query
를 작성할 수 있게 해주는 역할을 한다.
객체지향 프로그래밍 언어(OOP) 언어와 관계형 데이터베이스(RDBMS) 간의 불일치(Impedance Mismatch)를 해소하기 위함이다.
ORM(Object Relational Mapping)을 사용하는 이유는 다음과 같다.
- 생산성 향상 - 데이터베이스 작업을 위해 직접 SQL 쿼리 문을 작성하는 게 아닌 프로그래밍 언어의 객체를 사용하여 데이터베이스와 상호작용할 수 있게 해준다.
- 유지보수성 향상 - ORM을 사용하면 비즈니스 로직과 데이터베이스 액세스 로직을 더 명확하게 분리할 수 있다.
- 객체 지향 프로그래밍과의 일관성 - OOP와 일관된 방식으로 데이터베이스를 조작할 수 있게 해준다. 데이터베이스의 테이블이 객체의 클래스가 되고, 열은 object의 property / 행은 object의 instance가 된다.
요새 코딩을 하면서 깨달은 사실 하나가 있다. 그것은 바로 docs 읽기이다. 진짜 document에는 모든 내용이 담겨있다. prisma를 잘 모른다고 해도 docs를 차근차근 보면서 공부하게 되면 내 것으로 바꿀 수 있다. 잘 모른다고 chatgpt 딸깍
하지말고 Documents를 읽는 습관을 드리자.
$ npm install prisma --save-dev
$ npx prisma
prisma 패키지를 개발 의존성으로 설치하고 npx를 이용하여 prisma를 실행시킨다. npx prisma
를 하게 되면 읽기 싫은 이상한 글이 보일것이다..
하라는 대로 하면 우리는 prisma를 사용할 수 있다.
npx prisma init
먼저 이것부터 실행해보자 그럼
이렇게 보일 것이다. 하나씩 차근차근 해보자.
먼저 .env
에 DATABASE_URL이라는 주소가 있을 것이다. DATABASE_URL = "DB종류://UserName:PW@URL:/DB?schema"
이런 형식으로 바꾸면 될 것이다.
예시 :
DATABASE_URL = "mysql://root:1234@localhost:8080:/mydb?schema=public"
npx prisma generate
를 실행하면, prisma/schema.prisma
파일에 정의된 데이터 모델을 기반으로 Prisma Client를 생성한다. 애플리케이션 코드에서 DB와 상호작용할 수 있는 타입 안전한 쿼리를 제공하는 역할을 한다.
model Todo { id Int @id @default(autoincrement()) todo String isDone Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
나는 이런식으로 Todo 모델을 만들었다.
이후 prisma migrate dev
를 이용해 데이터베이스의 스키마를 선언하고 업데이트한다. migrate는 객체 지향 프로그래밍 언어에서 실제 sql 쿼리를 적용시켜줄 때 사용한다. db를 보면 실제로 Table이 생성된 것을 볼 수 있다.
-- CreateTable
CREATE TABLE `Todo` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`todo` VARCHAR(191) NOT NULL,
`isDone` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
// prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}
클래스가 다른 클래스를 상속할 때 사용한다. 상속 받은 클래스는 부모 클래스의 모든 메서드와 속성을 물려받아 사용할 수 있다. 다중 상속은 허용하지 않으며, 단일 상속만 가능하다.
implement 같은 경우 클래스 내에 interface를 구현할 때 사용된다. 클래스 내에는 inteface로 선언된 property와 method들을 강제로 구현하게 해준다. 다중 구현이 가능하며, 구현 코드를 가지는 게 아닌 형식을 정의하는 데 사용된다.
특징 | extends | implements |
---|---|---|
목적 | 클래스를 상속받아 부모 클래스의 기능을 재사용 | 인터페이스의 규칙을 구현하여 일관된 클래스 구조 유지 |
사용 대상 | 클래스 | 인터페이스 |
상속 및 구현 개수 | 단일 상속 (한 번에 하나의 클래스만 상속 가능) | 다중 구현 (여러 인터페이스를 동시에 구현 가능) |
상속된 요소 | 부모 클래스의 속성과 메서드 모두 상속 | 인터페이스의 메서드 시그니처만 구현 필요 |
코드 재사용성 | 부모 클래스의 코드를 재사용하여 개발 속도 향상 | 클래스가 일관된 인터페이스 구조를 따르도록 강제 |
실제 구현 여부 | 부모 클래스의 실제 구현 코드 포함 | 구현 코드 없음, 인터페이스의 규칙만 정의 |
위 코드로 예를 들면, PrismaService
라는 클래스를 선언하는데, prismaClient
클래스에서 property와 method를 상속 받고, ìnterface OnModuleInit
이라는 타입을 PrismaService
에 강제 할당하게 한다.
prisma.service.ts
는 server에서 DB를 접근할 수 있게 해주는 로직이다.
해당 모듈에서 PrismaService를 사용하게 하기 위해서는 모듈에서 provider에서 의존성 주입을 처리해줘야 한다.
import { Module } from '@nestjs/common';
import { TodosService } from './todos.service';
import { TodosController } from './todos.controller';
import { PrismaService } from 'src/prisma.service';
@Module({
controllers: [TodosController],
// 실제 DB에 접근할 수 있도록 PrismaService를 providers에 주입했다.
providers: [TodosService, PrismaService],
})
export class TodosModule {}
DB에 접근하기 위해 먼저 constructor에 prismaService
를 선언해준다. 이후 우리가 node에서 사용했던 async await를 활용하여 코드를 처리한다. postman과 mysql을 확인해보니 데이터 요청이 잘 들어온다.
// create
// tabwidth가 2인게 너무나도 불편해서 4로 변경함.
async create(createTodoDto: CreateTodoDto): Promise<Todo | null> {
return await this.prismaService.todo.create({
data: {
todo: createTodoDto.todo,
isDone: createTodoDto.isDone,
},
});
}
id / createdAt / updatedAt는 자동생성된다.
todo와 isDone
은 req body
에 넘겨주는 input data에서 뽑아내면 된다.
정답은 항상 Documents에 있다. 또한 GET Method
또한 docs를 참고하여 코드를 작성하였다. get 요청을 처리하는 방법은 크게 3가지 방법이 있다. findMany(), findUnique(), findFirst()
findMany - 해당 table에 있는 모든 데이터를 들고온다.
SELECT * FROM TABLE
findUnique - 해당 table에 있는 모든 데이터를 들고오는데 where 조건에 해당되는 데이터만 들고온다.SELECT * FROM TABLE WHERE ID = *
findFirst - 해당 table에 where 조건에 해당되는 데이터를 들고오는데 createdAt이 최신 인 데이터만 들고온다.SELECT * FROM table WHERE ID = * ORDER BY ... LIMIT 1
async findAll(): Promise<Todo[] | null> {
return await this.prismaService.todo.findMany();
}
async findOne(id: number): Promise<Todo | null> {
return await this.prismaService.todo.findUnique({
where: {
id,
},
});
}
update를 처리하는 방법은 총 2가지이다. update(), updateMany()
update - 해당하는 id의 레코드를 변경하는 것이다.
updateMany - 조건에 부합하는 레코드 전체를 변경하는 것이다.
둘 다UPDATE ... WHERE ...
쿼리문을 갖고 있지만 update는 한 개의 레코드 / updateMany는 여러 개의 레코드를 한꺼번에 바꿀 때 사용한다.
async update(
id: number,
updateTodoDto: UpdateTodoDto,
): Promise<Todo | null> {
return await this.prismaService.todo.update({
where: {
id,
},
data: {
todo: updateTodoDto.todo,
isDone: updateTodoDto.isDone,
},
});
}
delete method 또한 현재 형식과 유사하다. delete(), deleteMany()
위와 동일해서 따로 코드를 작성하진 않겠다. 특이한 점으로는 Prisma를 이용하여 delete를 호출하면 return으로 해당 레코드를 반환한다.
async remove(id: number): Promise<Todo | null> {
return await this.prismaService.todo.delete({
where: {
id,
},
});
} // 신기한 점 delete가 되면 해당 record를 return