백엔드 개발 (2)

이찬영·2024년 11월 3일

관계형 데이터베이스의 활용

나는 관계형 데이터베이스를 Prisma를 사용할것이다. (PostgreSQL)
prisma가 postgresql을 사용하기 위해 먼저 초기화를 해야한다.

npx prisma init --datasource-provider postgresql

초기화를 하게 되면, 갖가지 파일들이 생기는데, 그 중 먼저 .env파일을 보면

DATABASE_URL="postgresql://<postgres>:<PASSWORD>@localhost:5432/<database_name>?schema=public"

이 자동으로 생성될것이다.
<postgres>는 윈도우 사용자라면 postgres를 넣어주면 되고,
<password>는 설정한 비밀번호를 넣어주면 되며,
<database_name>은 내가 사용할 데이터베이스의 이름을 넣어주면 된다. 만약 해당하는 이름이 없으면 자동으로 만들어준다.

모델 만들기

모델은 테이블이라고 보면 된다. 첫글자는 꼭 대문자로 작성해주어야한다.
모델은 항상 3가지 파트로 구성되어 있다.

필드이름
필드타입
어트리뷰트

model User {
	id String @id @default(uuid())  #id를 uuid로 기본 id값을 갖겠다는 뜻 #혹시 숫자로 시작하는 id값을 갖고 싶다면,필드타입을 Int로 바꾸고 uuid자리에 autoincrement()를 넣으면 된다.
    name String
    email String @unique # @unique는 db에서 중복되면 안되는 값일때 사용한다.
    address String?  # 옆에 ?를 붙여주면 옵셔널한 필드가 된다.
}

prisma Schema의 추가기능

enum
유저의 멤버십타입을 저장한다고 가정했을때, 멤버십은 BASIC과 PREMIUM두가지 타입이 있는데, 이렇게 필드의 값이 몇가지 정해진 값 중 하나일땐 enum을 사용하면 된다.

model User {
	//...
    membership Membership @default(BASIC)
}

enum Membership {
	BASIC
    PREMIUM
}

enum값은 보통 대문자를 사용하고, '필드의 타입'을 enum이름으로 사용하면 된다. @default어트리뷰트도 사용할 수 있다.

@@unique
여러필드의 조합이 unique해야하는 경우, @@unique어트리뷰트를 사용할 수 있다.
@@unique어트리뷰트는 특정 필드에 종속된 어트리뷰트가 아니기 때문에 모델 아랫부분에 쓴다.

model User {
	id String @id @default(uuid())
    email String @unique
    firstName String
    lastName String
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    
    @@unique([firstName, lastName])
}

위 처럼 정의하면 firstName과 lastName의 조합이 unique해야하게 된다.
예를 들어,

id : "abc"
firstName : 길동
lastName : 홍

id : "def"
firstName : 길동
lastName : 홍

#이렇게된다면, 삽입이 불가능하게 된다.

마이그레이션

모델 코드를 데이터베이스에 반영하는 과정을 마이그레이션이라고 한다.
npx prisma migrate dev
혹시 마이그레이션 커맨드에서 에러가 난다면,
window + s키를 눌러 psql을 실행하고, 로그인한 뒤,
CREATE DATABASE example_db;
커맨드를 입력해주면, 마이그레이션이 가능해질것이다.

또, prisma에서 제공하는 prisma studio라는 툴을 이용하면, 간단히 데이터를 살펴보거나 목데이터를 쉽게 넣을 수 있다.
npx prisma studio

테이블에 데이터가 있을때, 마이그레이션을 하는법은 우선 어플리케이션이 발전해서 새로운 필드를 추가해야할때, 기존에 있던 유저들의 필드값은 비어있게 된다. 그래서 바로 마이그레이션은 안되고, 추가할 필드를 옵셔널한 필드. 즉, ex)Int?를 해줌으로써, 마이그레이션이 가능해질것이다.

Prisma Client와 데이터베이스 CRUD

우선 prisma를 자바스크립트에서 쓰기 위해선 사전 작업이 필요하다.

import {PrismaClient} from '@prisma/client'

const prisma = new PrismaClient();

prisma의 연산은 prisma.modelname.method()의 형태를 띠고 있다.

where : 필터를 할때 자주 쓰임
data : 데이터를 전달할 때 자주 쓰임

app.patch('users/:id', async(req, res) => {
	const {id} = req.params;
    const user = await prisma.user.update({
    	where : {
        	id : id
        },
      	data : req.body,
    });
  	res.status(201).send(user);
});

자주 쓰이는 메소드

GET : .findMany(), .findUnique()
POST : .create()
PATCH : .update()
DELETE : .delete()
등이 있다. 이 외에도 유용한 메소드들이 다수 존재한다.

쿼리 파라미터 처리하기

app.get('/users', 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 : orderBy,
      	skip : +offset, //skip은 몇개를 건너뛸건지
      	take : +limit,  //take는 몇개까지 보여줄건지
    });
  	res.send(users);
});

PRIMARY KEY와 FOREIGN KEY

primary key와 foreign key는 데이터베이스에서 테이블간의 관계를 정의하고, 데이터의 무결성을 유지하는데 중요한 역할을 한다.

Primary key(기본키)

primary key는 테이블에서 각 행을 고유하게 식별할 수 있는 특별한 열이다.
테이블에서 하나의 primary key만 설정할 수 있으며, 이 키를 통해 각 행이 고유하게 구분된다.

특징

  • 고유성 : primary key는 중복될 수 없으며, 각 행을 유일하게 식별한다.
  • Null값을 불허 : primary key는 절대 null값을 가질 수 없다.
    보통 모델의 id가 primary key이다.

Foreign key(외래키)

foreign key는 한 테이블의 열이 다른테이블의 primary key를 참조하는 키이다. 이를 통해서 두 테이블간에 관계를 정의할 수 있으며, 데이터의 일관성을 유지하는 역할을 한다.
foreign key를 사용하면 데이터가 연결된 방식으로 보관된다.

특징

  • 참조 : foreign key는 다른 테이블의 primary key를 참조한다.
  • 데이터 무결성 유지 : foreign key로 참조된 데이터가 삭제되면, 참조 무결성 문제가 발생할 수 있다.

[일대다관계 정의하기] - prisma

일대다 관계는 한 개체(예 : 사용자) 가 여러 다른 개체 (예 : 게시글)와 연결된 경우를 의미한다. 예를들어, 한 사용자는 여러 게시글을 작성할 수 있지만, 각 게시글은 특정 사용자에게만 속해있다.
Prisma Schema(일대다 관계)

model User {
	id String @id @default(uuid())
    name String
    posts Post[]   // User가 여러개의 Post를 가질 수 있음
}

model Post {
	id String @id @default(uuid())
    title String
    content String
    userId String
    user User @relation(fileds: [userId], references: [id])
}

fileds: [userId] 는 Post모델에서 User와 연결하기 위해 사용할 외래키 필드를 정한다. 여기서는 userId필드를 사용하여 User의 id와 연결하겠다는것을 의미한다.

references: [id] 는 fileds에 지정된 userId필드가 어떤 필드를 참조하는지 명시한다. 여기서는 User모델의 id필드를 참조하겠다는것을 의미한다.

즉, 예제에서 말하는것은
User모델은 여러 Post를 가질 수 있다.
Post모델은 userId필드를 통해 User와 연결된다.

[다대다관계 정의하기] - prisma

다대다 관계에서는 두 개체가 서로 여러 관계를 가질 수 있다. 예를 들어 한 학생이 여러 과목을 들을 수 있고, 각 과목도 여러 학생을 포함할 수 있다.
Prisma Schema (다대다 관계)

model Student {
	id String @id @default(uuid())
    name String 
    courses Course[] @relation("StudentCourse")
}

model Course {
	id String @id @default(uuid())
    title String
    students Student[] @relation("StudentCourse")
}

예제에서 말하는것은
Student모델은 여러 Course를 가질 수 있으며
Couse모델도 여러 Student를 가질 수 있다.

[일대일관계 정의하기] - prisma

사용자(User)와 사용자설정(UserPreference)이 각각 하나씩 존재하는 경우를 예로 들어본다.
각 사용자는 자신의 고유한 설정을 하나씩만 가질 수 있고, 반대로 각 설정은 특정 사용자에게만 속해있다.

Prisma Schema (일대일 관계)

model User {
	id String @id @default(uuid())
    email String @unique
    name String
    userPreference UserPreference? @relation(fields: [userPreferenceId], references : [id])
    userPreferenceId String?
}

model UserPreference {
	id String @id @default(uuid())
    reveiveEmail Boolean
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
    user User? @relation(fields: [userId], references: [id])
    userId String? @unique
}

위 예시에서
?로 옵셔널하게 만든 이유는 필요에 따라 존재하지 않는 값으로 설정할 수 있기 때문이다.
즉, User가 반드시 UserPreference를 가질 필요가 없고, User가 생성될때, UserPreference가 없는 상태로 시작할 수 있다.
User모델은 하나의 UserPreference를 가질 수 있고, 반대로 UserPreference는 하나의 User와만 연결된다.

User모델에 userPreferenceId라는 필드를 추가하고, 이 필드를 외래키로 UserPreference모델의 id필드를 참조하게 설정했다.
UserPreference모델에도 userId라는 필드를 추가하고, 이 필드가 User의 id를 참조하도록 설정했다.

여기서 가장 중요한것은 userId필드에 @unique를 설정한것이다.
이 속성 덕분에 UserPreference모델의 userId필드가 고유한 값만 가지게 되며, 이를 통해 각 사용자가 오직 하나만의 설정을 가지게 된다.

간단히 말해서, 사람과 주민등록번호와 같은것이다.

[일대일 관계가 유용한 경우] :

  • 민감한 데이터가 별도 테이블에 저장되어야 할때
  • 일부 데이터를 별도로 관리하고 싶을때

relation의 onDelete

relation옵션에 onDelete도 추가할 수 있다.
onDelete : 연결된 데이터가 삭제됐을때, 기존 데이터를 어떻게 처리할 것인지 명령
onDelete옵션은 rlation필드가 필수라면, Restrict가 기본값이고, 옵셔널하다면, setNull이 기본값이다.

onDelete

  • Cascade : foreign key가 가리키는 데이터가 삭제되면, 기존 데이터도 삭제
  • Restrict : 특정 데이터를 참조하는 데이터들이 있으면, 데이터를 삭제하지 못함
  • SetNull : foreign key가 가리키는 데이터가 삭제되면, foreign key를 NULL로 설정(이 옵션은 옵셔널하게 설정되어 있어야만 사용가능)
  • SetDefault : foreign key가 가리키는 데이터가 삭제되면, foreign key를 디폴트값으로 설정
    (이 옵션은 foreign key 필드에 default값이 명시되어 있어야 한다.)

관련된 객체 조회하기 - prisma

include옵션을 쓰면 된다.

const users = await prisma.user.findMany({
	skip : +offset,
  	take : +limit,
  	include : {
    	userPreference : {
        	select : {
            	receiveEmail : true
            }
        }
    }
});

userPreference의 reveiveEmail만 true, 즉 reveiveEmail만 조회한다.
select를 사용하지 않는다면, userPreference전부를 반환한다.

트랜젝션 - prisma

prisma에서 트랜잭션은 여러 데이터베이스 작업을 하나의 단위로 묶어서 실행하고, 모든 작업이 성공했을때만 데이터베이스에 반영하도록 하는 기능이다. 만약 트랜잭션 내의 작업중에 하나라도 실패하면, 전체 작업이 취소(롤백)된다.

prisma에서는 prisma.$transaction()메소드를 사용하여 트랜잭션을 수행한다.
이 메소드는 작업들을 배열 형태로 묶어 한번에 실행한다.

트랜잭션 사용법

await prisma.$transaction(async (prisma) => {
	//여러 작업 수행
});

이 구조 안에서 여러 prisma쿼리를 실행하면 하나의 트랜잭션으로 처리된다.
간단한 예제로 사용자와 주문을 동시에 생성하는 과정에서 한 작업이라도 실패하면 모든 작업을 취소하려고 한다.

const result = await prisma.$transaction(async (prisma) => {
	//1. 새로운 사용자 생성
  	const user = await prisma.user.create({
    	data : {
        	req.body
        }
    })
    
   	//2. 방금 생성된 사용자와 연결된 주문 추가
    const order = await prisma.order.create({
    	data : {
        	req.body
        }
    });
  //3. 두 작업이 모두 성공하면 결과 반환
  return {user, order};
})

0개의 댓글