티스토리 뷰
TypeORM이란?
■ NodeJS와 같은 환경에서 실행할수 있는 ORM이며 TypeScript및 JavaScript와 함께 사용할수 있다.
■ 항상 최신 JavaScript기능을 지원하고 몇 개의 테이블이 있는 작은 응용 프로그램에서 여러 데이터베이스가 있는
대규모 엔터프라이즈 응용 프로그램에 이르기 까지 데이터베이스를 사용하는 모든 종류의 응용프로그램을 개발하는데
도움이 되는 추가 기능을 제공한다.
■ 현재 존재하는 다른 모든 JavaScript ORM 과 달리 Active Record 및 Data Mapper패턴을 모두 지원한다.
■ 고품질의 느슨하게 결합된 확장 가능하고 유지 관리 가능한 애플리케이션을 가장 생산적인 방식으로 작성 가능.
• 유지관리에서 도움이 되며, 이는 대규모 앱에서 더 효과적.
• 저장소 라는 별도의 클래스에서 모든 쿼리 메서드를 정의하고 저장소를 사용하여 개체를 저장, 제거 및 로드가능.
• 모델 대신 저장소 내의 데이터베이스에 액세스 하는 접근 방식
모델
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
@Column()
isActive: boolean
}
저장소
const userRepository = dataSource.getRepository(User)
// 데이터 저장
const user = new User()
user.firstName = "Timber"
user.lastName = "Saw"
user.isActive = true
await userRepository.save(user)
// 데이터 삭제
await userRepository.remove(user)
// 데이터 읽기
const users = await userRepository.find({ skip: 2, take: 5 })
const newUsers = await userRepository.findBy({ isActive: true })
const timber = await userRepository.findOneBy({
firstName: "Timber",
lastName: "Saw",
})
• 작업을 단순하게 유지하는 데 도움이 되며 작은 앱에서도 잘 작동함.
• 단순하므로 항상 더 나은 유지관리를 할수가 있음
• 모델 자체 내에서 모든 쿼리 메서드를 정의하고 모델 메서드를 사용하여 객체를 저장, 제거 및 로드 가능
• 모델 내에서 데이터베이스에 액세스하는 접근방식
모델
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
@Column()
isActive: boolean
static findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany()
}
}
사용
const timber = await User.findByName("Timber", "Saw")
NestJS에서 사용하기
1. 루트 디렉토리로 이동한다음 패키지 설치하기
npm i @nestjs/typeorm typeorm mysql2
// Node.js에서 .env역할을 하는 Nest.js의 @nestjs/config 패키지 설치
// .env의 스키마(형식)를 강제할 수 있는 joi패키지설치
npm i @nestjs/config joi
2. app.module.ts
import Joi from 'joi';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Post } from './post/entities/post.entity';
import { PostModule } from './post/post.module';
const typeOrmModuleOptions = {
useFactory: async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => ({
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [Post],
synchronize: configService.get('DB_SYNC'),
logging: true,
}),
inject: [ConfigService],
};
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().required(),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
DB_SYNC: Joi.boolean().required(),
}),
}),
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
PostModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
• typeOrmModuleOptions
- TypeORM 모듈의 옵션을 의미
- TypeORM 설정을 하드 코딩하지 않고 동적으로 .env파일에서 설정을 불러와 주입하기 위한 방법.
- useFactory
° TypeOrmModule.forRootAsync()함수에서 설정 객체를 동적으로 생성하기 위해 사용.
- inject: [ConfigService]
° useFactory에서 사용할 ConfigService를 DI하는것.
• ConfigModule
- isGlobal : true
° 전역적으로 사용함을 의미
- validationSchema
° 여기에서 정의한 스키마대로 .env가 정의되어있지 않으면 서버는 실행하지 않는다.
→ DB_NAME같은 경우 값이 누락되어 있거나 해당 값이 문자열이 아닌 다른 값이라면 실행 X
• TypeOrmModule.forRootAsync
- 동적으로 TypeORM 설정을 주입할 때 사용
- 환경 변수나 다른 설정 서비스를 통해 데이터베이스 연결 정보를 가져올 때 유용
3. post.entity.ts
import { IsNumber, IsString } from 'class-validator';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity({
name: 'posts',
})
export class Post {
@PrimaryGeneratedColumn()
id: number;
@IsString()
@Column('varchar', { length: 50, nullable: false })
title: string;
@IsString()
@Column('varchar', { length: 1000, nullable: false })
content: string;
@IsNumber()
@Column('int', { select: false, nullable: false })
password: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt?: Date;
}
• @Entity
- @Entity 어토네이션은 해당 클래스가 어떤 테이블에 매핑이 되는지를 나타내는 어노테이션
• @PrimaryGeneratedColumn
- PK에 해당하는 컬럼에 사용하는 어노테이션
• @Column, @DateColumn
- 기본 컬럼과 날짜에 해당하는 컬럼에 사용하는 어노테이션
• 비밀번호 컬럼의 select: false설정
- 일반적인 조회로는 비밀번호를 얻어올 수 없도록하기 위함.
- select절로 특정하지 않으면 비밀번호는 갖고 올 수 없게 하여 민감한 정보를 최대한 보호
• @CreateDateColumn, @UpdateDateColumn, @DeleteDateColumn
- 레코드가 생성된 날짜 자동기록, 레코드가 수정된 날짜 자동기록, 레코드가 삭제된 날짜 자동기록
- @DeleteDateColumn은 엔티티가 삭제된 순간 실제로 삭제되는것이아닌 논리적으로 삭제(soft delete)가 되는것
- 즉, deleteAT ≠ NULL 인 게시물을 가져오게 되면 삭제된것과 같은 효과는 나타낸다.
4. post.modules.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Post } from './entities/post.entity';
import { PostController } from './post.controller';
import { PostService } from './post.service';
@Module({
imports: [TypeOrmModule.forFeature([Post])],
controllers: [PostController],
providers: [PostService],
})
export class PostModule {}
• 특정 모듈 서비스에서 사용하고 싶은 리포지토리가 있다면 @Module데코레이터의 import속성에 반드시 넣어줘야
DI가 원활하게 됨.
• TypeOrmModule.forFeature([])
- 특정 엔티티 또는 여러 엔티티를 해당 모듈에 등록하는데 사용.
5. post.service.ts
import _ from 'lodash';
import { Repository } from 'typeorm';
import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { RemovePostDTO } from './dto/remove-post.dto';
import { Post } from './entities/post.entity';
@Injectable()
export class PostService {
private articles: { id: number; title: string; content: string }[] = [];
private articlePasswords = new Map<number, number>();
constructor(
@InjectRepository(Post) private postRepository: Repository<Post>, // 리포지토리를 주입
) {}
async create(createPostDto: CreatePostDto) {
return (await this.postRepository.save(createPostDto)).id;
// return하는 이유 : 사용자가 게시글을 작성한 후 해당 게시글의 상세 페이지로 리다이렉션하거나, 추가 작업을 수행하기 위해 생성된 게시글의 ID가 필요할 수 있기 때문
}
async findAll() {
// 간단한 함수이고 반환되는 값의 타입이 명확하거나 자명한 경우에는 반환 타입을 명시하지 않을 수 있다.
// this.postRepository의 find 함수를 사용하면 특정 조건에 매칭되는 모든 게시물 목록을 Promise 배열로 반환
// 즉, findAll 함수는 async 함수가 되어야 하며 반환값에는 await
return await this.postRepository.find({
where: { deletedAt: null },
select: ['id', 'title', 'updatedAt'],
});
}
async findOne(id: number) {
// id가 number 타입으로 변환할 수 없는 경우에는 컨트롤러에서 전달되는 +id 값이 NaN이 되기 때문에 해당 방어 로직이필요
if (_.isNaN(id)) {
throw new BadRequestException('게시물 ID가 잘못되었습니다.');
}
return await this.postRepository.findOne({
where: { id, deletedAt: null },
select: ['title', 'content', 'updatedAt'],
});
}
async update(id: number, updatePostDto: UpdatePostDto) {
if (_.isNaN(id)) {
throw new BadRequestException('게시물 ID가 잘못되었습니다.');
}
const { content, password } = updatePostDto;
// 엔티티코드에서 select:false를 했기 때문에 명시적으로 select를 하지 않으면 password 값을 받아올 수 없다.
const post = await this.postRepository.findOne({
select: ['password'],
where: { id },
});
// import 문에서 참조하는 lodash를 사용하여 _.isNil이라는 함수를 부르고 해당 함수로 article의 NULL 여부를 체크
if (_.isNil(post)) {
throw new NotFoundException('게시물을 찾을 수 없습니다.');
}
if (!_.isNil(post.password) && post.password !== password) {
throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
}
await this.postRepository.update({ id }, { content }); // 해당하는 id의 content를 수정.
}
async remove(id: number, removePostDto: RemovePostDTO) {
if (_.isNaN(id)) {
throw new BadRequestException('게시물 ID가 잘못되었습니다.');
}
const { password } = removePostDto;
const post = await this.postRepository.findOne({
select: ['password'],
where: { id },
});
if (_.isNil(post)) {
throw new NotFoundException('게시물을 찾을 수 없습니다.');
}
if (!_.isNil(post.password) && post.password !== password) {
throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
}
return this.postRepository.softDelete({ id }); // softDelete: DB상에서 지워지는 것이 아니라 deletedAt에 지워진 날짜만 마킹되는 것
}
}
• @InjectRepository()어노테이션을 사용하여 리포지토리를 주입할수 있다.
• 리포지토리에는 일반리포지토리와 커스텀 리포지토리가 있다.
• 일반 리포지토리로 데이터베이스 연산이 부족하다면 일반 리포지토리를 상속한 커스텀 리포지토리를 작성하여 사용가능.
'프로그래밍 기초 > TypeScript & NestJS' 카테고리의 다른 글
nest개발환경 (0) | 2024.03.14 |
---|---|
TypeScript 고급 타입 (0) | 2024.03.13 |
TypeScript 기초 다지기 (1) | 2024.03.12 |