728x90
반응형
- 2024-09-15
.env
파일 설정하기. 아이디는 자동으로 postgres
로 정해지고 아래에 [password]
는 자신이 설정한 비밀번호를 넣어주고 (비번에 특수문자가 들어간 경우 encodeURIComponent kipid's blog :: Encode/Unescape and Decode/Escape URI Component 함수로 한번 처리해 준 뒤 넣어줘야 함. #, ? 같은게 URL 에선 특수하게 쓰이니...), [database_name]
에는 사용할 DB name 을 입력해준다. 없는 DB 라도 prisma 가 자동으로 생성해주니 이전에 만들었던 DB 를 이용하는 경우라면 오타가 안나게 조심할 것.
./prisma/schema.prisma
가 자동으로 생성되면 초기화 완성.
Domain Specific Language: DSL (스키마 정의 전용 언어, 나름의 문법) 사용.
```[.scrollable]
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```/
## Schcema.prisma 설정
VS code 에선 Ctrl+Shift+P
후 명령어로 Format Document 를 눌러서 자동 formatting 을 해주면 좋음. (바로가기 단축키는 Shift+Alt+F
)
```[.scrollable]
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
firstName String
lastName String
address String
age Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([firstName, lastName]) // 여러 필드의 조합이 unique 해야 하는 경우 @@unique 어트리뷰트를 사용할 수 있습니다. @@unique 어트리뷰트는 특정 필드에 종속된 어트리뷰트가 아니기 때문에 모델 아래 부분에 씁니다.
}
// Int, Float, Boolean <- RDBMS
// Nullable NULL 이 들어갈 수 있다. - 선택적 필드
// required -> Non-nullable NULL 이 들어갈 수 없다. - 필수 필드
enum Category {
FASHION
BEAUTY
SPORTS
ELECTRONICS
HOME_INTERIOR
HOUSEHOLD_SUPPLIES
KITCHENWARE
}
model Product {
id String @id @default(uuid())
name String
description String?
category Category
price Float
stock Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```/
새로운 field 를 추가할 땐, @default([value]) 를 주거나 optional field (Nuallable) 로 만든 뒤 값을 수동으로 채우고 다시 required field (Non-nullable) 로 바꿔줘야 한다.
./migrations directory 에 Schema 를 바꾼 기록이 모두 남아있다. 지우지 않는것이 좋음.
## migration
```[.scrollable]
npx prisma migrate dev
```/
Schema 를 변경한 뒤에는 항상 실행해줘야 함. Dev 환경에서만 사용하길 권함 https://www.prisma.io/docs/orm/prisma-migrate/workflows/development-and-production.
## GUI (Graphical User Interface): npx prisma studio
```[.scrollable]
npx prisma studio
```/
## CRUD (Create Retrieve Update Delete)
```[.scrollable]
import express from 'express';
import { PrismaClient } from '@prisma/client';
import * as dotenv from 'dotenv';
dotenv.config();
const prisma = new PrismaClient(); // PostgreSQL 에 접속할 수 있게 해주는 client 객체. 자동완성이 잘 되어 있으니 잘 활용할 것.
const HttpStatus = Object.freeze({
SUCCESS: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
SERVER_ERROR: 500,
});
const app = express();
app.use(express.json());
const asyncHandler = (handler) => {
return (async function (req, res) {
try {
await handler(req, res);
}
catch (err) {
console.log(err.name);
console.log(err.message);
if (err.name === "ValidationError") {
res.status(HttpStatus.BAD_REQUEST).send({message: err.message});
}
else if (err.name === "CastError") {
res.status(HttpStatus.NOT_FOUND).sned({message: err.message});
}
else {
res.status(HttpStatus.SERVER_ERROR).send({message: err.message});
}
}
});
};
app.get('/users', asyncHandler(async (req, res) => {
console.log("req.query: ", req.query);
const { offset = 0, limit = 10, order = "newest" } = req.query || {};
let orderBy;
switch (order) {
case "oldest":
orderBy = { createdAt: 'asc' };
break;
case "newest":
default:
orderBy = { createdAt: 'desc' };
}
const users = await prisma.user.findMany({
orderBy,
skip: parseInt(offset),
take: parseInt(limit),
});
res.send(users);
}));
app.get('/users/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const user = await prisma.user.findUnique({
where: { id },
});
res.send(user);
}));
app.post('/users', asyncHandler(async (req, res) => {
const user = await prisma.user.create({ data: req.body });
res.status(201).send(user);
}));
app.patch('/users/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const user = await prisma.user.update({
where: { id },
data: req.body,
});
await user.update(req.body);
res.send(user);
}));
app.patch('/users', asyncHandler(async (req, res) => {
const users = await prisma.user.update({
where: req.params,
data: req.body,
});
await users.update(req.body);
res.send(users);
}));
app.delete('/users/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
await prisma.user.delete({
where: { id },
});
res.sendStatus(204);
}));
/*********** products ***********/
app.get('/products', asyncHandler(async (req, res) => {
const { offset = 0, limit = 10, order = "newest", category } = req.query;
let orderBy;
switch (order) {
case "oldest":
orderBy = { createdAt: 'asc' };
break;
case "newest":
default:
orderBy = { createdAt: 'desc' };
}
const products = await prisma.product.findMany({
orderBy,
skip: parseInt(offset),
take: parseInt(limit),
where: category ? { category } : {},
});
res.send(products);
}));
app.get('/products/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const product = await prisma.product.findUnique({
where: { id },
});
res.send(product);
}));
app.post('/products', asyncHandler(async (req, res) => {
const product = await prisma.product.create({ data: req.body });
res.status(201).send(product);
}));
app.patch('/products/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const product = await prisma.product.update({
where: { id },
data: req.body,
});
res.send(product);
}));
app.delete('/products/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
await prisma.product.delete({
where: { id },
});
res.sendStatus(204);
}));
app.listen(process.env.PORT || 3000, () => console.log('Server Started'));
```/
### Request 보내보기
VS code extension 중 뭘 깔아야 이게 실행되는 거였더라? (REST Client https://github.com/Huachao/vscode-restclient 인가?) =ㅇ=;; 아무튼 다음과 같은 .http
파일을 만들면 CRUD 를 테스트 할 수 있음.
```[.scrollable]
GET http://localhost:3000/users
###
GET http://localhost:3000/users?order=newest&offset=1&limi=15
###
GET http://localhost:3000/users/6f3182a9-c20b-4c8b-aefd-c1b2f2fc35d5
###
POST http://localhost:3000/users
Content-Type: application/json
{
"email": "yjkim@example.com",
"firstName": "유진",
"lastName": "김",
"address": "충청북도 청주시 북문로 210번길 5",
"age": 23
}
###
PATCH http://localhost:3000/users/6f3182a9-c20b-4c8b-aefd-c1b2f2fc35d5
Content-Type: application/json
{
"address": "서울특별시 강남구 무실로 234번길 45-6"
}
###
PATCH http://localhost:3000/users?order=newest
Content-Type: application/json
{
"address": "서울특별시 강남구 무실로 234번길 45-6"
}
###
DELETE http://localhost:3000/users/6f3182a9-c20b-4c8b-aefd-c1b2f2fc35d5
```/
## DB seeding: npx prisma db seed
```[.scrollable]
{
"dependencies": {
"@prisma/client": "^5.19.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"is-email": "^1.0.2",
"is-uuid": "^1.0.2",
"superstruct": "^1.0.3"
},
"devDependencies": {
"nodemon": "^3.0.1",
"prisma": "^5.19.1"
},
"type": "module",
"scripts": {
"dev": "nodemon app.js",
"start": "node app.js"
},
"prisma": {
"seed": "node prisma/seed.js"
}
}
```/
와 같이 package.json 을 설정해 주고.
prisma/seed.js file 을 다음과 같이 만들어주자.
```[.scrollable]
import { PrismaClient } from '@prisma/client';
import { USERS, PRODUCTS } from './mock.js';
const prisma = new PrismaClient();
async function main() {
await prisma.user.deleteMany();
await prisma.user.createMany({
data: USERS,
skipDuplicates: true,
});
await prisma.product.deleteMany();
await prisma.product.createMany({
data: PRODUCTS,
skipDuplicates: true,
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
```/
그러면 다음과 같은 명령어로 DB seeding 을 할 수 있다.
```[.scrollable]
npx prisma db seed
```/
## findMany()
다음과 같은 설정들을 사용할 수 있다.
```[.scrollable]
function asyncHandler(handler) {
return async function (req, res) {
try {
await handler(req, res);
} catch (e) {
if (
e.name === 'StructError' ||
e instanceof Prisma.PrismaClientValidationError
) {
res.status(400).send({ message: e.message });
} else if (
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === 'P2025'
) {
res.sendStatus(404);
} else {
res.status(500).send({ message: e.message });
}
}
};
}
/*********** users ***********/
app.get('/users', asyncHandler(async (req, res) => {
const { offset = 0, limit = 10, order = 'newest' } = req.query;
let orderBy;
switch (order) {
case 'oldest':
orderBy = { createdAt: 'asc' };
break;
case 'newest':
default:
orderBy = { createdAt: 'desc' };
}
const users = await prisma.user.findMany({
orderBy,
skip: parseInt(offset),
take: parseInt(limit),
// include: {
// userPreference: {
// select: {
// receiveEmail: true,
// }
// }
// },
select: {
email: true,
userPreference: {
select: {
receiveEmail: true,
}
},
},
});
res.send(users);
}));
```/
include 와 select 는 동시에 못쓴다고 하니 주의.
### .findUnique(), .findUniqueOrThrow(), findFirst(), upsert(), .count()
Client method 는 https://www.prisma.io/docs/orm/reference/prisma-client-reference#model-queries 참조.
not, in, contains, startsWith 같은 다양한 비교 연산자 사용 가능.
#### AND (여러 필터 조건을 모두 만족해야 하는 경우): where 안에 프로퍼티를 여러 개 쓰면 됩니다.
```[.scrollable]
// category가 'FASHION'이면서 name에 '나이키'가 들어가는 Product들 필터
const products = await prisma.product.findMany({
where: {
category: 'FASHION',
name: {
contains: '나이키',
},
},
});
```/
#### OR (여러 필터 조건 중 하나만 만족해도 되는 경우): OR 연산자를 사용하면 됩니다.
```[.scrollable]
// name에 '아디다스'가 들어가거나 '나이키'가 들어가거는 Product들 필터
const products = await prisma.product.findMany({
where: {
OR: [
{
name: {
contains: '아디다스',
},
},
{
name: {
contains: '나이키',
},
},
],
},
});
```/
#### NOT (필터 조건을 만족하면 안 되는 경우): NOT 연산자를 사용하면 됩니다.
```[.scrollable]
// name에 '삼성'이 들어가지만 'TV'는 들어가지 않는 Product들 필터
const products = await prisma.product.findMany({
where: {
name: {
contains: '삼성',
},
NOT: {
name: {
contains: 'TV',
},
},
},
});
```/
#### 필터 조건에 대한 자세한 내용은 다음을 참고.
참고: https://www.prisma.io/docs/orm/reference/prisma-client-reference#filter-conditions-and-operators
## Superstruct Types 와 Refinements
참조: Types https://docs.superstructjs.org/api-reference/types, Refinements https://docs.superstructjs.org/api-reference/refinements
아래와 같이 작성.
superstruct 라이브러리의 .string(), .number(), .integer(), .boolean(), .define(), .object(), .enums(), .array(), .partial() 타입들로 틀을 정의하고 .size(), .min(), .max() 함수로 제약을 추가.
```[.scrollable]
import * as s from 'superstruct';
import isEmail from 'is-email';
import isUuid from 'is-uuid';
const CATEGORIES = [
'FASHION',
'BEAUTY',
'SPORTS',
'ELECTRONICS',
'HOME_INTERIOR',
'HOUSEHOLD_SUPPLIES',
'KITCHENWARE',
];
const STATUSES = ['PENDING', 'COMPLETE'];
const Uuid = s.define('Uuid', (value) => isUuid.v4(value));
export const CreateUser = s.object({
email: s.define('Email', isEmail),
firstName: s.size(s.string(), 1, 30),
lastName: s.size(s.string(), 1, 30),
address: s.string(),
userPreference: s.object({
receiveEmail: s.boolean(),
}),
});
export const PatchUser = s.partial(CreateUser);
export const CreateProduct = s.object({
name: s.size(s.string(), 1, 60),
description: s.string(),
category: s.enums(CATEGORIES),
price: s.min(s.number(), 0),
stock: s.min(s.integer(), 0),
});
export const PatchProduct = s.partial(CreateProduct);
export const CreateOrder = s.object({
userId: Uuid,
orderItems: s.size(
s.array(
s.object({
productId: Uuid,
unitPrice: s.min(s.number(), 0),
quantity: s.min(s.integer(), 1),
})
),
1,
Infinity
),
});
export const PatchOrder = s.object({
status: s.enums(STATUSES),
});
export const CreateSavedProducts = s.object({
});
```/
### 데이터를 비교할 때는 assert() 함수를 사용.
```[.scrollable]
import { assert } from 'superstruct';
import { CreateUser } from './structs.js';
// ...
app.post('/users', async (req, res) => {
assert(req.body, CreateUser); // CreateUser 형식이 아니라면 오류 발생
// ...
});
```/
### 오류 처리
```[.scrollable]
import { PrismaClient, Prisma } from '@prisma/client';
// ...
function asyncHandler(handler) {
return async function (req, res) {
try {
await handler(req, res);
} catch (e) {
if (
e instanceof Prisma.PrismaClientValidationError ||
e.name === 'StructError'
) {
res.status(400).send({ message: e.message });
} else if (
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === 'P2025'
) {
res.sendStatus(404);
} else {
res.status(500).send({ message: e.message });
}
}
};
}
// ...
app.post('/users', asyncHandler(async (req, res) => {
assert(req.body, CreateUser);
// ...
}));
```/
e.name === 'StructError': Superstruct 객체와 형식이 다를 경우 발생
e instanceof Prisma.PrismaClientValidationError: 데이터를 저장할 때 모델에 정의된 형식과 다른 경우 발생 (Superstruct로 철저히 검사하면 이 상황은 잘 발생하지 않지만 안전성을 위해 둘 다 검사)
e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025': 객체를 찾을 수 없을 경우 발생
## 1:1 관계, 1:N 관계, N:M 관계
### 일대다 관계
```[.scrollable]
model User {
// ...
orders Order[]
}
model Order {
// ...
user User @relation(fields: [userId], references: [id])
userId String
}
```/
### 일대일 관계
```[.scrollable]
model User {
// ...
userPreference UserPreference?
}
model UserPreference {
// ...
user User @relation(fields: [userId], references: [id])
userId String @unique
}
```/
### 다대다 관계
```[.scrollable]
model User {
// ...
savedProducts Product[]
}
model Product {
// ...
savedUsers User[]
}
```/
### 최소 카디널리티
```[.scrollable]
model User {
// ...
orders Order[]
}
model Order {
// ...
user User @relation(fields: [userId], references: [id])
userId String
}
```/
```[.scrollable]
model User {
// ...
orders Order[]
}
model Order {
// ...
user User? @relation(fields: [userId], references: [id])
userId String?
}
```/
### onDelete 설정하기
참조: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions
```[.scrollable]
model Order {
// ...
user User @relation(fields: [userId], references: [id], onDelete: ...)
userId String
}
```/
Cascade: userId 가 가리키는 유저가 삭제되면 기존 데이터도 삭제됩니다.
Restrict: userId 를 통해 유저를 참조하는 주문이 하나라도 있다면 유저를 삭제할 수 없습니다.
SetNull: userId 가 가리키는 유저가 삭제되면 userId 를 NULL 로 설정합니다. user 와 userId 모두 옵셔널해야 합니다.
SetDefault: userId 가 가리키는 유저가 삭제되면 userId 를 디폴트 값으로 설정합니다. userId 필드에 @default()를 제공해야 합니다.
관계 필드와 foreign key 가 필수일 경우 Restrict 가 기본값이고 옵셔널할 경우 SetNull 이 기본값입니다.
### Create, Update connected entities/tables.
```[.scrollable]
/* create */
const postBody = {
email: 'yjkim@example.com',
firstName: '유진',
lastName: '김',
address: '충청북도 청주시 북문로 210번길 5',
userPreference: {
receiveEmail: false,
},
};
const { userPreference, ...userFields } = postBody;
const user = await prisma.user.create({
data: {
...userFields,
userPreference: {
create: userPreference,
},
},
include: {
userPreference: true,
},
});
console.log(user);
```/
```[.scrollable]
/* update */
const id = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';
const patchBody = {
email: 'honggd2@example.com',
userPreference: {
receiveEmail: false,
},
};
const { userPreference, ...userFields } = patchBody;
const user = await prisma.user.update({
where: { id },
data: {
...userFields,
userPreference: {
update: userPreference,
},
},
include: {
userPreference: true,
},
});
console.log(user);
```/
### 관련된 객체 연결, 연결 해제하기
관련된 객체 연결.
```[.scrollable]
const userId = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';
const productId = 'c28a2eaf-4d87-4f9f-ae5b-cbcf73e24253';
const user = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
connect: {
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
console.log(user);
```/
관련된 객체 연결 해제.
```[.scrollable]
const userId = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';
const productId = 'c28a2eaf-4d87-4f9f-ae5b-cbcf73e24253';
const user = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
disconnect: {
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
console.log(user);
```/
종합
```[.scrollable]
app.patch('/users/:id/saved-products', asyncHandler(async (req, res) => {
assert(req.body, PostSavedProducts);
const { id: userId } = req.params;
const { productId } = req.body;
const user = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
connect: {
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
res.send(user);
}));
app.patch('/users/:id/unsave-product', asyncHandler(async (req, res) => {
assert(req.body, PostSavedProducts);
const { id: userId } = req.params;
const { productId } = req.body;
const user = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
disconnect: {
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
res.send(user);
}));
```/
## 비즈니스 로직 (Business Logic): $transaction
Order 와 product stock 과의 로직 완성하기. $transaction: Do All or Nothing.
```[.scrollable]
app.post('/orders', asyncHandler(async (req, res) => {
assert(req.body, CreateOrder);
const { userId, orderItems } = req.body;
const productIds = orderItems.map((orderItem) => orderItem.productId);
const products = await prisma.product.findMany({
where: { id: { in: productIds } },
});
function getQuantity(productId) {
const orderItem = orderItems.find(
(orderItem) => orderItem.productId === productId
);
return orderItem.quantity;
}
// 재고 확인
const isSufficientStock = products.every((product) => {
const { id, stock } = product;
return stock >= getQuantity(id);
});
if (!isSufficientStock) {
throw new Error('Insufficient Stock');
}
const [order] = await prisma.$transaction([
prisma.order.create({
data: {
userId,
orderItems: {
create: orderItems,
},
},
include: {
orderItems: true,
},
}),
...orderItems.map(({ productId, quantity }) => {
return prisma.product.update({
where: { id: productId },
data: {
stock: {
decrement: quantity,
},
},
});
})
]);
res.status(201).send(order);
}));
```/
## RRA
728x90
반응형