ORM이란 Object Relational Mapping을 의미한다. 객체와 관계형 데이터베이스의 데이터를 연결하는 것으로, ORM을 통해 프로그래밍 과정에서 RDBMS를 더욱 편리하게 조작, 관리할 수 있다.
const typeOrmModuleOptions = {
useFactory: async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => ({
namingStrategy: new SnakeNamingStrategy(),
type: 'mysql',
host: configService.get('DB_HOST'), // process.env.DB_HOST
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_NAME'),
entities: [생성한 entity 주입],
synchronize: false, // sync 여부 (sync 시, 소스코드 상 DB 구조 관련 내용이 RDBMS에 반영된다)
autoLoadEntities: true,
logging: false, // logging 여부 (logging 시, IDE에서 TypeORM이 번역한 SQL문을 로그로 확인할 수 있다.)
keepConnectionAlive: true,
timezone: 'local',
charset: 'utf8mb4',
}),
inject: [ConfigService],
};
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync(typeOrmModuleOptions),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
orm.js
require("dotenv").config()
const typeorm = require("typeorm")
const dataSource = new typeorm.DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.USER_NAME,
password: process.env.PASSWORD,
database: process.env.DATABASE,
synchronize: false,
logging: false,
entities: [
require("./entity/simulation.data"),
],
migrations: ["migrations/*.js"],
migrationsDir: ["migrations"],
migrationsTableName: "migration",
})
// const puuidController = require("./data/puuId/puuId.controller")
module.exports = {
async connect() {
await dataSource
.initialize()
.then(function () {
console.log("분석용 연결 완료")
})
.catch(function (error) {
console.log("Error: ", error)
})
},
async close() {
await dataSource.destroy().then(() => {
console.log("분석용 연결 해제")
})
},
dataSource,
}
app.js
require("dotenv").config()
const db = require("./orm")
//데이터베이스 연결
db.connect()
commonEntity
import {
CreateDateColumn,
DeleteDateColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { IsString, IsUUID } from 'class-validator';
import { Exclude } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
export class CommonEntity {
@ApiProperty()
@IsUUID()
@IsString()
@PrimaryGeneratedColumn('uuid')
id: string;
@ApiProperty()
@CreateDateColumn({
type: 'timestamp' /* timestamp with time zone */,
})
createdAt: Date;
@ApiProperty()
@UpdateDateColumn({ type: 'timestamp' })
updatedAt: Date;
@ApiProperty()
@Exclude()
@DeleteDateColumn({ type: 'timestamp' })
deletedAt?: Date | null;
}
------------------------------
userEntity
import { OmitType } from '@nestjs/swagger';
import { IsString, IsUUID } from 'class-validator';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity({
name: 'USER',
})
export class UserEntity extends OmitType(CommonEntity, ['id']) {
@IsUUID()
@IsString()
@PrimaryGeneratedColumn('uuid')
userId: string;
@Column({ type: 'varchar', nullable: false })
nickname: string;
@Column({ type: 'varchar', nullable: true })
bio: string;
}
NOW()
}라는 구조로 SQL문을 직접적으로 적용해주었다. var EntitySchema = require("typeorm").EntitySchema
module.exports = new EntitySchema({
name: "combination",
tableName: "combination",
columns: {
id: {
type: 'varchar',
primary: true,
generated: 'uuid',
},
createdAt: {
type: 'timestamp',
require: true,
default: () => { return `NOW()` }
},
updatedAt: {
type: 'timestamp',
require: true,
default: () => { return `NOW()` }
},
mainChampName: {
type: "varchar",
require: true
},
subChampName: {
type: "varchar",
require: true
},
sampleNum: {
type: 'int',
require: true,
default: 0
},
},
})
userEntity
import { OmitType } from '@nestjs/swagger';
import { IsString, IsUUID } from 'class-validator';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity({
name: 'USER',
})
export class UserEntity extends OmitType(CommonEntity, ['id']) {
@IsUUID()
@IsString()
@PrimaryGeneratedColumn('uuid')
userId: string;
@Column({ type: 'varchar', nullable: false })
nickname: string;
@Column({ type: 'varchar', nullable: true })
bio: string;
@OneToMany(
() => CommentEntity,
(commentEntity: CommentEntity) => commentEntity.userId,
{
cascade: true, // cascade 설정, 설정 시 user 데이터의 변동 사항이 commentEntity의 연결된 데이터에도 적용된다.
},
)
comment: CommentEntity;
}
--------------------
commentEntity
import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { UserEntity } from 'src/user/entities/user.entity';
@Entity({
name: 'COMMENT',
})
export class CommentEntity extends CommonEntity {
@Column({ type: 'varchar', nullable: false })
category: string;
@Column({ type: 'varchar', nullable: false })
content: string;
@Column({ type: 'int', nullable: false, default: 0 })
reportNum: number;
@ManyToOne(() => UserEntity, (user: UserEntity) => user.userId, {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
eager: true, // eager loading 설정, 설정 시에 comment table select를 하면 연결된 user table 데이터가 모두 조회된다.
})
@JoinColumn([
{
name: 'userId', // commentEntity의 foreinKey
referencedColumnName: 'userId', // 부모 entity인 userEntity에서 참조할 column
},
])
userId: UserEntity;
}
champEntity
var EntitySchema = require("typeorm").EntitySchema
module.exports = new EntitySchema({
name: "CHAMP", // Will use table name `category` as default behaviour.
tableName: "CHAMP", // Optional: Provide `tableName` property to override the default behaviour for table name.
columns: {
champId: {
type: "varchar",
primary: true,
},
},
relations: {
combination: {
target: "COMBINATION_STAT",
type: "one-to-many",
cascade: true,
eager: true,
},
}
}
)
combinationStatEntity
var EntitySchema = require("typeorm").EntitySchema
module.exports = new EntitySchema({
name: "COMBINATION_STAT", // Will use table name `category` as default behaviour.
tableName: "COMBINATION_STAT", // Optional: Provide `tableName` property to override the default behaviour for table name.
columns: {
id: {
type: "varchar",
primary: true,
generated: "uuid",
},
created_at: {
type: "timestamp",
require: true,
default: () => {
return `NOW()`
},
},
updated_at: {
type: "timestamp",
require: true,
default: () => {
return `NOW()`
},
},
deleted_at: {
type: "timestamp",
require: false,
default: null,
},
mainChampId: {
type: "varchar",
require: true,
},
subChampId: {
type: "varchar",
require: true,
},
version: {
type: 'varchar',
require: true
}
},
relations: {
mainChampId: {
target: 'CHAMP',
type: 'many-to-one',
joinColumn: {
name: 'mainChampId', // combinatioNStatEntity의 foreignKey column
referencedColumnName: 'champId' // 부모 entity인 champEntity에서 참조할 column
},
},
subChampId: {
target: 'CHAMP',
type: 'many-to-one',
joinColumn: {
name: 'subChampId',
referencedColumnName: 'champId'
},
}
}
})
**SELECT**
userRepository.find({
select: {
firstName: true,
lastName: true,
},
where: {
firstName: 'example'
}
})
= select firstName, lastName from user where firstName = 'example'
userRepository.find({
select: {
firstName: true,
lastName: true,
},
where: {
firstName: 'example'
}
})
= select firstName, lastName from user where firstName = 'example'
**관계가 설정된 TABLE까지 SELECT**
userRepository.find({
relations: {
profile: true,
photos: true,
videos: true,
},
})
= SELECT * FROM "user"
LEFT JOIN "profile" ON "profile"."id" = "user"."profileId"
LEFT JOIN "photos" ON "photos"."id" = "user"."photoId"
LEFT JOIN "videos" ON "videos"."id" = "user"."videoId"
** 이 때, profile, photos, video가 각 entity에서 loading option이 eager/lazy 여부에 따라서 조회되는 데이터는 달라진다.
ex1. profile이 eager일 경우 => 별도로 profileEntity와 그 안에서 select할 column을 지정하지 않더라도
userEntity 데이터 조회 시(userRepository.find()) profile의 모든 데이터가 조회됨.
ex2. profile이 lazy인 경우 => profileEntity를 userEntity 조회 시 함께 조회하려면, 추가적으로 profileEntity를 요청해야함.
ex2. 코드 예시
const companies: Company = await this.companyRepository.findOne(
companyId
);
const employees: Employee[] = await companies.employee;
return companies;
=> company와 employee가 lazy loading으로 설정된 경우, company와 관계 설정된 employee의 데이터를 조회하려면,
위처럼 company를 조회한 이후, company에 있는 employee 관련 foreignkey를 한번 더 조회해야한다.
**CREATE**
await userRepository.insert({
firstName: "Timber",
lastName: "Timber",
})
**UPDATE**
await userRepository.update(1, { firstName: "Rizzrak" })
// executes UPDATE user SET firstName = Rizzrak WHERE id = 1
**DELETE**
await repository.delete([1, 2, 3]) // delete by primary id
await repository.delete({ firstName: "Timber" }) // delete by specific condition
**SELECT**
await this.commentsRepository
.createQueryBuilder('comment') // 'comment' alias로 commentEntity를 사용할 것임을 선언
.leftJoinAndSelect('comment.userId', 'user') // foreignKey 이용하여 관계설정된 table 'user' alias로 연결
.leftJoinAndSelect('comment.champId', 'champ')
.leftJoinAndSelect('comment.summonerName', 'summoner')
.select([ // 조회할 column 선택
'comment.content',
'comment.id',
'user.userId AS userId',
'user.profileImg',
'user.nickname',
'champ.id',
'comment.reportNum',
'comment.createdAt',
'comment.category',
'comment.summonerName',
'summoner.summonerName',
])
.where(option) // 조건 명시
.orderBy({ // 데이터 정렬 명시
'comment.createdAt': 'DESC',
})
.getMany(); // 1개 조회시 getOne()
}
** CREATE **
await this.commentsRepository
.createQueryBuilder()
.insert()
.into(CommentEntity) // 데이터 주입할 테이블 명시
.values(value) // 주입할 데이터
.execute(); // 쿼리 종료 선언
** UPDATE **
await commentsRepository
.createQueryBuilder()
.update(CommentEntity) // 데이터 변경할 테이블 명시
.set({ reportNum: data.reportNum + 1 }) // 변경할 데이터
.where('id = :id', { id }) // 데이터 변경하는 조건
.execute(); // 쿼리 종료 선언
** DELETE **
await commentsRepository
.createQueryBuilder()
.delete()
.from(CommentEntity) // 데이터 삭제할 테이블 명시
.where('id = :id', { id: data.id }) // 삭제할 조건 1
.andWhere('userId = :userId', { userId }) // 삭제할 조건 2 (orWhere)
.execute(); // 쿼리 종료 선언
import { getManager, getConnection } from 'typeorm'
async deleteUserAndBoards(userId: number) {
await getManager().transaction(async (transactionalEntityManager) => {
await this.boardRepository.deleteBoardsByUserId(transactionalEntityManager, userId)
await this.userRepository.deleteUserByUserId(transactionalEntityManager, userId)
}).catch((err) => {
throw err
})
}
import { getManager, getConnection } from 'typeorm'
async deleteUserAndBoards(userId: number) {
const queryRunner = await getConnection().createQueryRunner()
await queryRunner.startTransaction()
try {
await this.boardRepository.deleteBoardsByUserId(queryRunner.manager, userId)
await this.userRepository.deleteUserByUserId(queryRunner.manager, userId)
await queryRunner.commitTransaction();
} catch(err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
데이터를 한 위치에서 다른 위치로, 한 형식에서 다른 형식으로 또는 한 애플리케이션에서 다른 애플리케이션으로 이동하는 프로세스
즉, 데이터베이스의 형상을 안전하게 변경/이동하는 작업을 의미한다.const dataSource = new typeorm.DataSource({
type: "mysql",
host: process.env.DB_HOST,
port: process.env.DB_PORT,
username: process.env.USER_NAME,
password: process.env.PASSWORD,
database: process.env.DATABASE,
synchronize: false,
logging: false,
entities: [
require("./entity/simulation.data"),
],
migrations: ["migrations/*.js"], // 마이그레이션 파일 경로
migrationsDir: ["migrations"], // 마이그레이션 폴더명
migrationsTableName: "migration", // 실제 DB에 생성될 migration 테이블 이름
})
1) 마이그레이션 파일 생성
typeorm migration:create 경로 (경로에 TS 파일로 마이그레이션 파일 생성)
typeorm migration:create 경로 -o (경로에 JS 파일로 마이그레이션 파일 생성)
2) 마이그레이션 코드 작성
- 다음과 같은 예시처럼 작성될 수 있다.
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UserTableCreate1615829427764 implements MigrationInterface {
up은 migration commit을 위한 코드, down은 migration rollback을 위한 코드를 작성하면 된다.
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "user" (
id INT(11) PRIMARY KEY NOT NULL AUTOINCREMENT,
email VARCHAR(20) NOT NULL UNIQUE,
password VARCHAR(200) NOT NULL,
name VARCHAR NOT NULL,
birthday VARCHAR NOT NULL,
phone VARCHAR NOT NULL,
createAt Date NOT NULL DEFAULT CURRENT_TIMESTAMP() ,
updatedAt Date NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP(),
)COLLATE='utf8_bin'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "user"`);
}
}
3) 마이그레이션 up 실행
typerom migration:run -d 데이터베이스 경로
typeorm-ts-node-commonjs migration:run -d 데이터베이스 경로 (JS 환경에서)
=> run 실행 시 실제 DB의 migration table에 생성한 마이그레이션 코드를 데이터로 삽입한다.
=> pending 상태에 있는 migration을 실행하여, 마이그레이션 코드에 따라 데이터베이스 변경을 실행한다.
4) 마이그레이션 down 실행
typeorm migration:revert -d 데이터베이스 경로
typeorm-ts-node-commonjs migration:revert -d 데이터베이스 경로 (JS 환경에서)
=> 이미 실행된 마이그레이션을 다시 rollback시킨다.
=> 여러 마이그레이션을 rollback할 경우, 해당 명령어를 여러번 실행시켜야 한다.
c.f) typeorm migration:generate
- 소스코드 상에서 entity 모델과 실제 DB의구조를 비교하여, 실제 DB에서 업데이트 되어야 하는 차이점을 자동적으로 마이그레이션 코드로
작성해주는 명령어이다.
- 기존 컬럼의 데이터 타입 변경과 같이 기존 컬럼을 변경하는 사항의 경우, 기존 컬럼의 데이터를 DROP한 뒤에 재생성하는 특징이 있다.
- 이에, 안전한 migration을 위해, 실제 운영중인 DB에서는 권장되지 않는 옵션이라 할 수 있겠다.