[20240216 TIL] ORM과 Prisma

Haizel·2024년 2월 18일
3
post-thumbnail

01. ORM(Object Relational Mapping)


① 영속성(Persistence)

영속성이란 데이터를 생성한 프로그램이 종료되어도, "데이터는" 사라지지 않는 데이터의 특성을 말한다.

  • 영속성을 갖지 않는 데이터는 단지 메모리에서만 존재하기 때문에 프로그램 종료 시 모두 삭제된다.

📌 Object Persistence(영구적인 객체)

  • 메모리 상의 데이터를 영구적으로 저장하기 위해 파일 시스템, 관계형 데이터베이스 혹은 객체형 데이터 베이스 등을 통해 영속성을 부여한다.

  • 이때 프로그램의 아키텍처에서 데이터의 영속성을 부여해주는 계층을 Persistence Layer라고 하는데, 이때 영속성 부여를 위해 사용하는 것이 바로 Persistence framework이다.
  • 그리고 Persistence Framework는 SQL Mapper와 ORM으로 나눌 수 있다.

② ORM(Object Relational Mapping)

ORMDataBaseWeb Aplication의 연결을 객체지향적으로 도와주는 역할을 한다.!

📌 ORM이란

ORM(Object Relational Mapping)은 객체로 연결해준다라는 의미로, 어플리케이션과 데이터베이스를 연결할 때, SQL 언어가 아닌, 어플리케이션 개발언어로 데이터베이스를 접근할 수 있게 해주는 툴이다.

ORM은 SQL 문법 대신 어플리케이션의 개발언어를 그대로 사용할 수 있게 하므로, 개발 언어의 일관성가독성을 높여준다는 장점이 있다.


👉 쉽게 말하자면,

DB (SQL 언어) ------ ORM Tools -------> 개발 언어 (Java, JS, JS, C 등)
  • SQL 언어로 작성된 DB 데이터를 내가 개발하고 있는 어플리케이션의 개발언어에서 가져와 사용할 수 있도록 일종의 변환(통역) 역할을 해주는 셈이다.
  • ORM은 어플리케이션을 개발하는 개발언어에 종속된다.

이게 무슨 말인가 하면, ORM은 도구(Tools)로 언어에 따른 여러 종류가 있는데, 아래서 설명할 Prisma를 사용한 이유는 내가 개발한 어플리케이션의 환경이 TS이기 때문이다.

  • Prisma는 TS(JS) 개발 언어에서 SQL 문법을 사용할 수 있도록 만들어진 ORM 툴이다.
  • 결국 한 줄로 설명하면, ORM은 DB와 개발 언어 사이에 위치한 레이어(미들웨어)이다.

02. Prisma


① Prisma 란?

Prisma는 데이터베이스 작업을 간단하게 만들어주는 오픈 소스 ORM(Object-Relational Mapper)이다.

다른 ORM과는 다른 Prisma의 특징은 무엇일까?

  • Prisma는 데이터베이스 쿼리에 대해 유형이 안전한 코드를 생성하므로 👉 오류를 줄일 수 있고, 코드를 유지보수하기도 편리하다.
  • Prisma는 시간이 지남에 따라 데이터베이스의 스키마를 간단하게 변경할 수 있는 강력한 내장기능인 마이그레이션 시스템(migrate)을 제공한다. 👉 이를 통해 항상 웹 애플리케이션과 데이터데이스를 호환할 수 있다.
  • prima는 복잡한 쿼리를 간단하게 생성할 수 있는 generate라는 강력한 쿼리 빌더가 포함되어 있어 👉 오류 방지에 도움된다.
  • prisma는 studio 명령어를 통해 웹 브라우저에서 가시적으로 데이터를 테이블 형태로 보고 편집할 수 있는 UI를 제공한다. 👉 덕분에 코드를 작성하지 않고도 명시적으로 데이터를 확인할 수 있다.

② Prisma Set-up

❶ Install

npm install prisma @prisma/client
npx prisma init
npm install prisma typescript ts-node @types/node --save-dev

# 만약 prisma 디렉터리를 src 하위로 옮긴다면,  package.json에 아래 내용을 추가
"prisma": { 
	"schema": "src/prisma/schema.prisma" 
}

❷ Update environment variable

DATABASE_URL="postgresql://username:password@hostname:5432/dbName?schema=schemaName"

❸ Instantiating Prisma Client

import { PrismaClient } from "@prisma/client"; 

const prismaClient = new PrismaClient(); 

❹ Modeling

  • prisma.schema 파일 안에 작성
model User {
id               Int            @id     @default(autoincrement()) 
name         String 
createdAt DateTime @default(now()) }

❺ Migrate

npx prisma migrate dev

❻ generate

  • 자동완성을 위해 prisma.schema에 작성한 model의 타입을 만들어주는 역할(제너레이터) 👉 코드 안전성 부여
 npx prisma generate

❼ studio

  • web에서 데이터들의 테이블 UI를 보여주는 CLI
npx prisma studio

// &  prisma studio와 현재 개발 서버를 동시에 띄워야 한다.
npm run dev

03. Association (연관성)


① One-To-One Relationship (1 : 1)

일대일(1-1) 관계에서는 참조할 테이블의 키를 외래키(FK)로 가져온다.


② One-To-many Relationship (1 : n)


③ many-To-many Relationship (m : m)

다수-다수관계일 때에는 중간에 중계 테이블을 만든다. (이때 통상적으로 양측 테이블의 이름을 합쳐 camelCase로 짓는다.)
그리고 각 테이블은 새로 만든 중계 테이블과 join해 정보를 가져온다.



04. Practice to Use


  • 파일 구조
📦 src  
 ┣ 📂 contexts  
 ┃ ┣ 📂 users  
 ┃ ┃ ┣ 📜 users.controller.ts  
 ┃ ┃ ┗ 📜 users.service.ts  
 ┃ ┗ 📜 index.ts  
 ┣ 📂 prisma  
 ┃ ┣ 📂 migrations  
 ┃ ┣ 📜 client.prisma.ts  
 ┃ ┗ 📜 schema.prisma  
 ┗ 📜 app.ts
  📜  .env

⓿ 초기 세팅

  1. Product 및 express set-up
  2. app.ts
import bodyParser from "body-parser";
import express from "express";
import controllers from "./contexts";

const app = express();
const port = 포트번호;
const jsonParser = bodyParser.json();

app.use(jsonParser);
app.use(controllers);

app.listen(port, () => {
	console.log("**----------------------------------**");
	console.log(" ========== Server is On!!! ========= ");
	console.log("**----------------------------------**");
});

① prisma

❶ schema.prisma

generator client {
	provider = "prisma-client-js"
}

datasource db {
	provider  = "postgresql"
	url            = env("DATABASE_URL")
}

model User {
	id                                 Int                    @id @default(autoincrement())
	email                           String               @unique
	encryptedPassword  String
	profile                         UserProfile?
	createdAt                   DateTime          @default(now())
}

model UserProfile {
	id                          Int              @id @default(autoincrement())
	nickname             String?
	name                    String?
	gender                 String?
	age                       Int?
	phoneNumber     String?
	address                String?
	user                      User         @relation(fields: [userId], references: [id])
	userId                   Int            @unique
}

❷ client.prisma.ts

import { PrismaClient } from "@prisma/client";

const prismaClient = new PrismaClient();

export default prismaClient;

② users.service.ts

❗️TIP 1
서버에서 에러가 뜨면(400, 404 등) 서버가 꺼지게 된다. 그럼 매번 재시동을 해줘야 하는데, 이문제를 해결하는 방법이 있다.
👉 세번째 인자인 next()를 사용하면 된다.

❗️TIP 2
res: Response<Omit<User, "encryptedPassword">> 안에 omit을 여러개 사용하고 싶다면 👉 | 를 사용하면 된다.

❶ Import

import { User } from "@prisma/client";
import { hash } from "bcrypt";
import { NextFunction, Request, Response } from "express";
import prismaClient from "../../prisma/client.prisma";

❷ createUser

interface userInfoType = {
	email: string;
	password: string;
	nickname: string;
	name: string;
	gender: string;
	age: number;
}

const createUser = async(
	req:Request<never, unknown, userInfoType>,
	res: Response<Omit <User, "encryptedPassword>>,
	next : NextFunction
) => {
	try {
	const { email, password, nickname, name, gender, age } = req.body;
	if (!email.trim()) throw new Error("No email");
	if (!password.trim()) throw new Error("No password");

	// 요청헤더로 받은 password 해싱
	const encryptedPassword = await hash(password, 12);
	const user = await prismaClient.user.create({
		data : {
			email,
			encryptedPassword,
			profile: { 
				create: { 
					nickname, name, gender, age
				}
			 }
		}
	});

	res.json(user);	
	} catch (e) {
	next(e)
	}
};

❸ getUsers

const getUsers = async (_: Request, res: Response) => {
	const users = await prismaClient.user.findMany();
	res.json(users);
}

❹ getUser

const getUser = async (
	req: Request<{ userId : string }>,
	res: Response
) => {
	const parsedUserId = Number(req.params.userId);
	if (isNaN(parsedUserId)) throw new Error("error");

	const user = await prismaClient.user.findUnique({
		where : { id  :  parsedUserId }.
		select : {
			id : true,
			email : true,
			created : true,
			profile: true,
		},
		include : {
		//! SQL의 JOIN 역할 👉 만약 특정 cols만 가져오고 싶다면 안에 객체를 넣으면 됨
		profile: true
		}
	});
	
	res.json(user);
}

❺ updateUser

const UpdateUser = async(
	req: Reqeust<{ userId : string },
	Omit<User, "encryptedPassword">,
	{ email : string }>,
	res: Response
) => {
	const parsedUserId = Number(req.params.userId);
	if (isNaN(parsedUserId)) throw new Error("error");

	const { email } = req.body;

	const user = await prismaClient.user.update({
		where : { id : parsedUserId },
		data : { email },
		select : {
			id : true, email : true, createdAt : true
		} // 특정 cols만 가져올 것
	});

	if (!user) throw new Error("유저 없음");

	res.json(user);
}

❻ deleteUser

const deleteUser = async(
	req: Request<{ userId : string }>,
	res : Response
) => {
	const parsedUserId = Number(req.params.userId);
	if (isNaN(parsedUserId)) throw new Error("error");

	const user = await prismaClient.user.delete({
		where : { id :  parsedUserId }
	})
	
	res.json(user);
}

❼ Export

const usersService = {
	createUser,
	getUsers,
	getUser,
	updateUser,
	deleteUser
}

export default usersService;

③ users.controller.ts

import { Router } from "express";
import usersService from "./users.service";

const usersController = Router();

usersController.post("/", usersService.createUser);
usersController.get("/", usersService.getUsers);
usersController.get("/:userId", usersService.getUser);
usersController.put("/:userId", usersService.updateUser);
usersController.delete("/:userId", usersService.deleteUser);

export default usersController;

④ controllers

import { Router } from "express";
import usersController from "./users/users.controller";

const controllers = Router();

controllers.use("/users", usersController);

export default controllers;


📂 출처

NextJS + TypScript + Prisma

profile
한입 크기로 베어먹는 개발지식 🍰

0개의 댓글

관련 채용 정보