사전 지식
관계형 데이터베이스: 테이블과 열로 구성된 데이터 모델을 사용하여 데이터를 저장하고 관리합니다.
ORM(Object Relational Mapping): OOP에서는 데이터를 객체로 표현하고 객체들 간의 관계를 맺고 메서드를 통해 상호작용한다. ORM은 객체와 관계형 데이터베이스 간의 불일치를 해결하기 위해 개발되었습니다. 관계형 데이터베이스를 조작하기 위한 인터페이스를 제공하여 쿼리를 직접 작성하지 않고도 데이터베이스 작업을 수행할 수 있습니다.
Prisma란?
Prisma는 기존 ORM과는 근본적으로 다른 새로운 종류 ORM이며 기존 ORM들과 관련된 문제들로 고통받지 않습니다.결론은 ORM이다.
설치
npm install prisma --save-dev
npm install @prisma/client
yarn add prisma --dev
yarn add @prisma/client
@prisma/client는 자바스크립트 클라이언트 라이브러리로 개발 중에만 사용되는 것이 아니라, 애플리케이션 실행 시에도 필요하기에 일반적인 의존성으로 설치합니다.
시작해 봅시다!
스키마 정의하기
스키마: 데이터베이스에 저장되는 데이터의 구조와 제약 조건을 정의
Prisma 클라이언트 생성
// prisma/schema.prisma
generator client { // Prisma 클라이언트를 생성하는 지시자
provider = "prisma-client-js" // 클라이언트를 생성할 때 사용할 프로바이더를 지정
}
Prisma 스키마는 데이터베이스 테이블과 열을 모델로 정의하고, Prisma 클라이언트를 통해 해당 모델을 사용하여 데이터베이스에 접근하고 조작할 수 있습니다. Prisma는 데이터베이스 스키마를 사용하여 Prisma 클라이언트를 생성합니다. 이 클라이언트는 데이터베이스와 상호작용하기 위한 인터페이스를 제공합니다. 이 인터페이스는 데이터베이스의 테이블, 열, 관계 등을 추상화하고 개발자에게 간편한 방식으로 데이터에 접근하고 조작할 수 있는 기능을 제공합니다.
클라이언트를 사용하면 개발자는 자바스크립트 코드를 사용하여 데이터베이스에 대한 쿼리를 작성하고, 데이터를 생성, 수정, 삭제하고, 데이터 간의 관계를 조작할 수 있습니다. 이를 통해 개발자는 데이터베이스의 복잡한 구조와 직접적인 상호작용을 알 필요 없이 간단하고 일관된 인터페이스를 사용할 수 있습니다.
데이터베이스 소스 설정
datasource db {
provider = "mysql" // 데이터베이스 제공자
url = env("DATABASE_URL") // 데이터베이스 연결 url 설정
}
데이터베이스 연결 url은 민감한 정보이기에 env로 처리하였다.
DATABASE_URL=mysql://root:password:@localhost:3306/schema_name
데이터베이스 모델 정의
이 모델은 User라는 테이블을 나타냅니다.
model User {
id Int @id @default(autoincrement())
firstName String
lastName String
age Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
- @id: primary key로 사용되어야 함을 의미함
- @default(autoincrement()): 이 열이 자동으로 증가되야 함을 의미함
- @default(now()): 이 열이 데이터가 생성될 때 현재 날짜와 시간으로 자동 설정되어야 함을 의미함
- @updatedAt: 이 열 데이터가 업데이트될 때 자동으로 현재 날짜와 시간으로 갱신되어야 함을 의미함
위와 같은 설정을 하였다면 테이블을 DB에 생성해봅시다.
npx prisma db push
db 정의한 모델을 push할 수 있다.
npx prisma generate
yarn prisma generate
Prisma를 사용하여 데이터베이스에 쿼리를 보내고, 모델과 상호작용하는 데 사용되는 코드를 자동으로 생성하는 Prisma 클라이언트가 프로젝트 내에 생성됩니다.
npx prisma migrate dev --name init --create-only
yarn prisma migrate dev --name init --create-only
데이터베이스 스키마를 변경하거나 초기화하는 작업을 의미하는 데이터베이스 마이그레이션을 수행합니다. 해당 마이그레이션을 개발 환경에서 실행하도록 지정했습니다.
- --name init: 이 플래그는 마이그레이션의 이름을 "init"으로 설정합니다. 마이그레이션의 이름은 변경 가능하며, 데이터베이스 스키마 변경을 추적하고 식별하는 데 사용됩니다.
- 이 플래그는 마이그레이션 파일을 생성하고 데이터베이스에 적용하지는 않는 옵션입니다. 따라서, 실제로 데이터베이스에 변경 사항을 적용하지 않고 마이그레이션 파일만 생성합니다. 데이터베이스 초기 설정에 유용합니다.
마이그레이션 파일은 필요에 따라 데이터베이스 스키마를 업데이트하는데 사용될 수 있습니다.
(migrate 명령어를 실행했는데 database Auth에 실패한다면 password뒤의 :를 제거해보세요)
성공적으로 완료되면 Database Table에 마이그레이션 파일이 생성된 것을 볼 수 있습니다.
yarn prisma migrate deploy
마이그레이션 파일의 변경 사항을 데이터베이스에 배포하여 스키마를 업데이트합니다.
유저 테이블이 생성된 것을 볼 수 있습니다.
간단하게 API를 만들어봅시다.
const express = require("express");
const app = express();
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
app.use(express.json());
app.get("/", async (req, res) => {
const allUsers = await prisma.user.findMany();
res.json(allUsers);
});
app.post("/", async (req, res) => {
const newUser = await prisma.user.create({ data: req.body });
res.json(newUser);
});
app.listen(3001, () => console.log(`Server running on port ${3001}`));
Prisma 클라이언트 인스턴스를 생성하여 이 인스턴스로 데이터베이스와 상호작용할 수 있습니다. prisma.user.findMany(); 는 "user" 모델의 모든 레코드를 조회하는 비동기 함수입니다. 해당 함수를 이용해서 user 테이블의 모든 데이터를 받아올 수 있습니다.
prisma.user.create({ data: req.body})는 "user" 모델에 새로운 레코드를 생성하는 비동기 함수입니다. cool!!
app.put("/:id", async (req, res) => {
const id = req.params.id;
const newAge = req.body.age;
const newFirstName = req.body.firstName;
const newLastName = req.body.lastName;
const updatedUser = await prisma.user.update({
where: { id: parseInt(id) },
data: { age: newAge, firstName: newFirstName, lastName: newLastName },
});
res.json(updatedUser);
});
app.delete("/:id", async (req, res) => {
const id = req.params.id;
const deletedUser = await prisma.user.delete({
where: { id: parseInt(id) },
});
res.json(deletedUser);
});
위 코드는 수정과 삭제를 만들었습니다. 이렇게 간단하게 CRUD가 완성되었습니다!!
관계형 데이터베이스에서 서로 다른 두 모델간의 관계는 매우 중요합니다. 데이터베이스와 테이블을 설정하는 과정에서 모델과의 관계를 정확하게 식별하고 적절하게 관리해야 합니다.
두 모델간의 관계를 정의해봅시다.
model House {
id String @id @default(uuid())
address String @unique
wifiPassword String?
owner User @relation(fields: [ownerId], references: [id])
ownerId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
- @default(uuid()): uuid 형식의 보안에 안전한 id를 가짐
- @unique: 중복이 있으면 안됨을 의미함
- owner User @relation(fields: [ownerId], references: [id]): owner 필드가 User 모델의 id 필드와 관계를 맺는다는 것을 나타냅니다. 이 관계는 ownerId 필드의 값이 User 모델의 id 값과 일치해야 함을 의미합니다. ownerId에는 owner 필드가 참조하는 User 모델의 id 값을 저장합니다. 따라서 ownerId 필드의 값은 User 모델의 id 값과 일치해야 House 모델과 User 모델 간의 관계가 유지됩니다.
위 내용을 작성하니 owner User 부분에서 빨간 줄이 나타납니다. 이것을 해결하기 위해 아래의 명령어를 입력해보세요.
yarn prisma format
포맷된 코드를 자세히 보면 에러가 사라짐과 동시에 User 모델에 House[]이 나타났습니다. 이것은 User 모델이 여러개의 House 모델과 관계를 맺을 수 있다는 의미입니다.
model User {
id String @id @default(uuid())
firstName String
lastName String
age Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
houseOwned House[] @relation("HouseOwner")
houseBuilt House[] @relation("HouseBuilder")
}
model House {
id String @id @default(uuid())
address String @unique
wifiPassword String?
owner User @relation("HouseOwner", fields: [ownerId], references: [id])
ownerId String
builtBy User @relation("HouseBuilder", fields: [builtById], references: [id])
builtById String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
관계를 식별하기 위한 이름(라벨)인 "HouseOwner", "HouserBuilder"를 만들었습니다. User 모델이 소유한 집들과 건설한 집들을 구별하기 위해 서로 다른 라벨을 사용하여 관계 설정을 좀 더 구체화하고 명확하게 만들었습니다. 필요한 경우 라벨을 지정하여 관계를 명시적으로 구분할 수 있습니다.
앞서 설명했 명령어들을 통해 delpoy해주면 house 테이블과 foreign key가 생성된 것을 알 수 있습니다.
하지만 post가 제대로 작동하지 않고 아래의 에러가 발생했습니다.
Null constraint violation on the fields: (`id`)
해결책
npx yarn-upgrade-all
yarn add @prisma/client
yarn prisma migrate reset
yarn prisma migrate dev --name init --create-only
yarn prisma migrate deploy
app.post("/house", async (req, res) => {
const newHouse = await prisma.house.create({ data: req.body });
res.json(newHouse);
});
app.get("/house", async (req, res) => {
const allHouse = await prisma.house.findMany({
include: { owner: true, builtBy: true },
});
res.json(allHouse);
});
간단한 post와 get을 만들었습니다. post는 User로부터 id를 받고 있지 않기 때문에 포스트맨에서 직접 할당해야합니다. 여기서 살펴볼 점은 include: { owner: true, builtBy: ture } 입니다.
둘의 차이를 아시겠나요? 둘 다 house 데이터를 반환하고 있습니다. 하지만 오른쪽 같은 경우 builtBy만 제공하고 있죠. 그 이유는 include: { owner: false, builtBy: ture }로 설정하여 owner의 관계 정보는 가져오지 않기 때문입니다.
app.get("/house/:id", async (req, res) => {
const id = req.params.id;
const allHouse = await prisma.house.findUnique({
where: {
id,
},
include: { owner: true, builtBy: true },
});
res.json(allHouse);
});
app.get("/house", async (req, res) => {
const address = req.body.address;
const allHouse = await prisma.house.findUnique({
where: {
address,
},
include: { owner: true, builtBy: true },
});
res.json(allHouse);
});
주어진 조건에 맞는 단일 레코드를 반환하는 findUnique를 이용하여 where 조건을 지정하여 get하는 메서드들 입니다. 위는 id를 통해 id에 해당하는 house를 받아오고, 아래는 address를 사용합니다.
app.post("/house/many", async (req, res) => {
const newHouse = await prisma.house.createMany({ data: req.body });
res.json(newHouse);
});
createMany는 body안의 배열 데이터를 post합니다. house가 생성된 수인 count를 response로 반환합니다.
Filter & Sort
app.get("/house/withFilters", async (req, res) => {
const filteredHouses = await prisma.house.findMany({
where: {
wifiPassword: {
not: null,
},
owner: {
age: {
gte: 50,
},
},
},
orderBy: [
{
owner: {
firstName: "desc",
},
},
],
include: { owner: true, builtBy: true },
});
res.json(filteredHouses);
});
where로 필터링 조건을 지정하여 wifiPassword가 null이 아니고, owner의 age가 50 이상인 집을 찾습니다.
orderBy로 결과를 정렬합니다. 여기서는 owner의 firstName을 내림차순으로 정렬합니다.
이렇게 prisma에 관해 간단(?)하게 알아봤습니다.